跳到主要内容

3 篇博文 含有标签「form」

查看所有标签

HTML表单提交的两种方式:为什么有些按钮不触发 onSubmit

· 阅读需 4 分钟
Claude
Anthropic Claude
1adybug
子虚伊人

以下内容均为 Claude 4 sonnet 生成

在前端开发中,我们经常会遇到表单提交的问题。今天就来深入探讨一个看似简单却容易让人困惑的问题:为什么有些按钮能触发表单的 onSubmit 事件,而有些却不能?

问题场景

让我们先看一个具体的例子:

import { FC, FormEvent, useRef } from "react"

const App: FC = () => {
const form = useRef<HTMLFormElement>(null)

function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
console.log("表单已提交!")
}

return (
<div>
<form ref={form} onSubmit={onSubmit}>
<input name="name" type="text" />
<button type="submit">内部提交按钮</button>
</form>
<button onClick={() => form.current?.submit()}>外部提交按钮</button>
</div>
)
}

运行这段代码你会发现:

  • 点击"内部提交按钮":控制台输出"表单已提交!"
  • 点击"外部提交按钮":控制台没有任何输出

这是为什么呢?

原理解析

方式一:标准表单提交(触发 onSubmit)

<button type="submit">内部提交按钮</button>

当我们点击这个按钮时,发生了以下过程:

  1. 浏览器识别到这是一个 type="submit" 的按钮
  2. 浏览器查找该按钮所属的表单
  3. 浏览器触发表单的 submit 事件
  4. 我们的 onSubmit 事件处理函数被调用
  5. 如果没有 preventDefault(),表单会被实际提交

这是标准的HTML表单提交流程,完全符合Web标准。

方式二:程序化提交(不触发 onSubmit)

<button onClick={() => form.current?.submit()}>外部提交按钮</button>

当我们点击这个按钮时:

  1. 执行 onClick 处理函数
  2. 调用 form.submit() 方法
  3. 表单被直接提交,但不触发 submit 事件
  4. onSubmit 处理函数不会被调用

关键点:根据HTML规范,程序化调用 HTMLFormElement.submit() 方法会绕过表单验证和事件触发机制。

为什么会有这种设计?

这种设计是有意为之的:

  1. 性能考虑:程序化提交通常用于自动化场景,跳过事件处理可以提高性能
  2. 避免无限循环:如果在 onSubmit 中调用 form.submit(),可能导致无限递归
  3. 明确区分:用户操作和程序操作应该有不同的行为模式

实际应用中的解决方案

方案一:手动调用事件处理函数

<button onClick={(e) => {
if (form.current) {
// 手动调用 onSubmit 处理函数
const syntheticEvent = {
...e,
currentTarget: form.current,
target: form.current,
preventDefault: () => {}
}
onSubmit(syntheticEvent as FormEvent<HTMLFormElement>)
}
}}>外部提交按钮</button>

方案二:使用 requestSubmit()(推荐)

<button onClick={() => form.current?.requestSubmit()}>外部提交按钮</button>

requestSubmit() 是HTML5的新方法,它:

  • 会触发 submit 事件
  • 会执行表单验证
  • 行为类似于点击提交按钮

注意requestSubmit() 的浏览器兼容性比 submit() 稍差,在一些旧版本浏览器中不支持。

方案三:模拟点击提交按钮

const submitButtonRef = useRef<HTMLButtonElement>(null)

// 在JSX中
<form ref={form} onSubmit={onSubmit}>
<input name="name" type="text" />
<button ref={submitButtonRef} type="submit">内部提交按钮</button>
</form>
<button onClick={() => submitButtonRef.current?.click()}>外部提交按钮</button>

这种方式通过模拟点击来触发标准的表单提交流程。

最佳实践建议

  1. 优先使用标准提交按钮:在表单内使用 type="submit" 的按钮
  2. 需要程序化提交时优先考虑 requestSubmit():它更符合标准行为
  3. 做好兼容性处理:如果需要支持旧版本浏览器,提供降级方案
  4. 保持一致的用户体验:确保所有提交方式都有相同的验证和处理逻辑

总结

表单提交看似简单,实则涉及Web标准、浏览器实现和用户体验的多个层面。理解 submit()requestSubmit() 的区别,以及它们与事件处理机制的关系,能帮助我们写出更健壮的表单处理代码。

记住这个核心原则:用户触发的提交会触发事件,程序触发的提交通常不会。掌握了这一点,你就能更好地控制表单的提交行为了。

Antd Design 的 Form 中自定义受控组件的使用

· 阅读需 2 分钟
1adybug
子虚伊人

自定义或第三方的表单控件,也可以与 Form 组件一起使用。只要该组件遵循以下的约定:

  1. 提供受控属性 value 或其它与 valuePropName 的值同名的属性。

  2. 提供 onChange 事件或 trigger 的值同名的事件。

  3. 转发 ref 或者传递 id 属性到 dom 以支持 scrollToField 方法。

import { ChangeEvent, FC, forwardRef } from "react"

import { Button, Form, Input } from "antd"
import { useForm } from "antd/es/form/Form"
import FormItem from "antd/es/form/FormItem"
import { useInputState } from "soda-hooks"

type Info = {
name?: string
id?: string
}

type InfoItemProps = {
value?: Info
onChange?: (value: Info) => void
}

const InfoItem = forwardRef<HTMLDivElement, InfoItemProps>((props, ref) => {
const { value, onChange } = props

// 推荐使用 soda-hooks 的 useInputState
const [name, setName] = useInputState(value?.name)

const [id, setId] = useInputState(value?.id)

function onNameChange(e: ChangeEvent<HTMLInputElement>) {
setName(e.target.value)
// 将变化之后的 value 传递给外部,优先级为 state < props < 最新的变化的 value
onChange?.({ id, ...value, name: e.target.value })
}

function onIdChange(e: ChangeEvent<HTMLInputElement>) {
setId(e.target.value)
onChange?.({ name, ...value, id: e.target.value })
}

return (
<div ref={ref}>
<Input value={name} onChange={onNameChange} />
<Input value={id} onChange={onIdChange} />
</div>
)
})

type FormData = {
info: Info
}

const App: FC = () => {
const [form] = useForm<FormData>()

return (
<Form<FormData> form={form} onFinish={console.dir}>
<FormItem<FormData> name="info">
<InfoItem />
</FormItem>
<FormItem<FormData>>
<Button onClick={() => form.setFieldsValue({ info: undefined })}>Reset</Button>
</FormItem>
<FormItem<FormData>>
<Button htmlType="submit">Submit</Button>
</FormItem>
</Form>
)
}

export default App
提示
  1. onChange 是会一直变化的,所以需要获取到最新值
  2. onChangeform.setFieldsValue 一样,都是同步的

Ant Design 的 Form 中 requiredMark 的使用

· 阅读需 2 分钟
1adybug
子虚伊人

之前一直都是使用 css 来实现必填组件的 * 号的隐藏,没想到 Ant Design 官方提供了修改的方法:

import React, { FC, Fragment, useState } from "react"

import { InfoCircleOutlined } from "@ant-design/icons"
import { Button, Form, Input, Radio, Tag } from "antd"
import { useForm } from "antd/es/form/Form"
import FormItem from "antd/es/form/FormItem"

type RequiredMark = boolean | "optional" | "customize"

const customizeRequiredMark = (label: React.ReactNode, { required }: { required: boolean }) => (
<Fragment>
{required ? <Tag color="error">Required</Tag> : <Tag color="warning">optional</Tag>}
{label}
</Fragment>
)

const App: FC = () => {
const [form] = useForm()
const [requiredMark, setRequiredMarkType] = useState<RequiredMark>("optional")

const onRequiredTypeChange = ({ requiredMarkValue }: { requiredMarkValue: RequiredMark }) => {
setRequiredMarkType(requiredMarkValue)
}

return (
<Form
form={form}
layout="vertical"
initialValues={{ requiredMarkValue: requiredMark }}
onValuesChange={onRequiredTypeChange}
requiredMark={requiredMark === "customize" ? customizeRequiredMark : requiredMark}
>
<FormItem label="Required Mark" name="requiredMarkValue">
<Radio.Group>
<Radio.Button value>Default</Radio.Button>
<Radio.Button value="optional">Optional</Radio.Button>
<Radio.Button value={false}>Hidden</Radio.Button>
<Radio.Button value="customize">Customize</Radio.Button>
</Radio.Group>
</FormItem>
<FormItem label="Field A" required tooltip="This is a required field">
<Input placeholder="input placeholder" />
</FormItem>
<FormItem
label="Field B"
tooltip={{
title: "Tooltip with customize icon",
icon: <InfoCircleOutlined />,
}}
>
<Input placeholder="input placeholder" />
</FormItem>
<FormItem>
<Button type="primary">Submit</Button>
</FormItem>
</Form>
)
}

export default App

requiredMark 有四种可以传递的值:

  1. true 默认值

  2. false 不显示是否必填

  3. "optional" 在可选表单项的 label 后面添加 (可选)

  4. (label: ReactNode, { required }: { required: boolean }) => ReactNode 自定义渲染函数