表单

¥Forms

Preact 中的表单的工作方式与 HTML 和 JS 中的表单相同:你渲染控件、附加事件监听器并提交信息。

¥Forms in Preact work in the same way as they do in HTML & JS: you render controls, attach event listeners, and submit information.



基本表单控件

¥Basic Form Controls

通常,你会希望在应用中收集用户输入,这就是 <input><textarea><select> 元素的用武之地。这些元素是 HTML 和 Preact 中表单的通用构建块。

¥Often you'll want to collect user input in your application, and this is where <input>, <textarea>, and <select> elements come in. These elements are the common building blocks of forms in HTML and Preact.

输入(文本)

¥Input (text)

首先,我们将创建一个简单的文本输入字段,它将在用户输入时更新状态值。我们将使用 onInput 事件来监听输入字段值的变化并根据每次击键更新状态。然后,此状态值将在 <p> 元素中渲染,以便我们可以看到结果。

¥To get started, we'll create a simple text input field that will update a state value as the user types. We'll use the onInput event to listen for changes to the input field's value and update the state per-keystroke. This state value is then rendered in a <p> element, so we can see the results.

class BasicInput extends Component {
  state = { name: '' };

  onInput = e => this.setState({ name: e.currentTarget.value });

  render(_, { name }) {
    return (
      <div class="form-example">
        <label>
          Name:{' '}
          <input onInput={this.onInput} />
        </label>
        <p>Hello {name}</p>
      </div>
    );
  }
}
Run in REPL

输入(复选框和单选按钮)

¥Input (checkbox & radio)

class BasicRadioButton extends Component {
  state = {
    allowContact: false,
    contactMethod: ''
  };

  toggleContact = () => this.setState({ allowContact: !this.state.allowContact });
  setRadioValue = e => this.setState({ contactMethod: e.currentTarget.value });

  render(_, { allowContact }) {
    return (
      <div class="form-example">
        <label>
          Allow contact:{' '}
          <input type="checkbox" onClick={this.toggleContact} />
        </label>
        <label>
          Phone:{' '}
          <input type="radio" name="contact" value="phone" onClick={this.setRadioValue} disabled={!allowContact} />
        </label>
        <label>
          Email:{' '}
          <input type="radio" name="contact" value="email" onClick={this.setRadioValue} disabled={!allowContact} />
        </label>
        <label>
          Mail:{' '}
          <input type="radio" name="contact" value="mail" onClick={this.setRadioValue} disabled={!allowContact} />
        </label>
        <p>
          You {allowContact ? 'have allowed' : 'have not allowed'} contact {allowContact && ` via ${this.state.contactMethod}`}
        </p>
      </div>
    );
  }
}
Run in REPL

选择

¥Select

class MySelect extends Component {
  state = { value: '' };

  onChange = e => {
    this.setState({ value: e.currentTarget.value });
  }

  render(_, { value }) {
    return (
      <div class="form-example">
        <select onChange={this.onChange}>
          <option value="A">A</option>
          <option value="B">B</option>
          <option value="C">C</option>
        </select>
        <p>You selected: {value}</p>
      </div>
    );
  }
}
Run in REPL

基本表单

¥Basic Forms

虽然裸输入很有用,你可以用它们走得更远,但我们经常会看到我们的输入发展成能够将多个控件组合在一起的形式。为了帮助管理这一点,我们转向 <form> 元素。

¥Whilst bare inputs are useful and you can get far with them, often we'll see our inputs grow into forms that are capable of grouping multiple controls together. To help manage this, we turn to the <form> element.

为了演示,我们将创建一个包含两个 <input> 字段的新 <form> 元素:一个用于用户的名字,一个用于他们的姓氏。我们将使用 onSubmit 事件来监听表单提交并使用用户的全名更新状态。

¥To demonstrate, we'll create a new <form> element that contains two <input> fields: one for a user's first name and one for their last name. We'll use the onSubmit event to listen for the form submission and update the state with the user's full name.

class FullNameForm extends Component {
  state = { fullName: '' };

  onSubmit = e => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    this.setState({
      fullName: formData.get("firstName") + " " + formData.get("lastName")
    });
    e.currentTarget.reset(); // Clear the inputs to prepare for the next submission
  }

  render(_, { fullName }) {
    return (
      <div class="form-example">
        <form onSubmit={this.onSubmit}>
          <label>
            First Name:{' '}
            <input name="firstName" />
          </label>
          <label>
            Last Name:{' '}
            <input name="lastName" />
          </label>
          <button>Submit</button>
        </form>
        {fullName && <p>Hello {fullName}</p>}
      </div>
    );
  }
}
Run in REPL

注意:虽然看到将每个输入字段链接到组件状态的 React 和 Preact 表单很常见,但这通常是不必要的,而且会变得笨拙。作为一条非常宽松的经验法则,在大多数情况下,你应该更喜欢使用 onSubmit`FormData` API,仅在需要时使用组件状态。这降低了组件的复杂性并可能跳过不必要的重新渲染。

¥Note: Whilst it's quite common to see React & Preact forms that link every input field to component state, it's often unnecessary and can get unwieldy. As a very loose rule of thumb, you should prefer using onSubmit and the `FormData` API in most cases, using component state only when you need to. This reduces the complexity of your components and may skip unnecessary rerenders.

受控和非受控组件

¥Controlled & Uncontrolled Components

在谈论表单控件时,你可能会遇到术语 "受控组件" 和 "不受控制的组件"。这些术语指的是表单控件值是否由组件明确管理。通常,你应尽可能尝试使用不受控组件,DOM 完全能够处理 <input> 的状态:

¥When talking about form controls you may encounter the terms "Controlled Component" and "Uncontrolled Component". These terms refer to whether or not the form control value is explicitly managed by the component. Generally, you should try to use Uncontrolled Components whenever possible, the DOM is fully capable of handling <input>'s state:

// Uncontrolled, because Preact doesn't set the value
<input onInput={myEventHandler} />;

但是,在某些情况下,你可能需要对输入值施加更严格的控制,在这种情况下,可以使用受控组件。

¥However, there are situations in which you might need to exert tighter control over the input value, in which case, Controlled Components can be used.

// Controlled, because Preact sets the value
<input value={myValue} onInput={myEventHandler} />;

Preact 在受控组件方面存在一个已知问题:Preact 需要重新渲染来控制输入值。这意味着如果你的事件处理程序不以某种方式更新状态或触发重新渲染,则输入值将不受控制,有时会与组件状态不同步。

¥Preact has a known issue with controlled components: rerenders are required for Preact to exert control over input values. This means that if your event handler doesn't update state or trigger a rerender in some fashion, the input value will not be controlled, sometimes becoming out-of-sync with component state.

其中一个有问题的情况的示例如下:假设你有一个输入字段,其长度应限制为 3 个字符。你可能有一个如下所示的事件处理程序:

¥An example of one of these problematic situations is as such: say you have an input field that should be limited to 3 characters. You may have an event handler like the following:

const onInput = (e) => {
  if (e.currentTarget.value.length <= 3) {
    setValue(e.currentTarget.value);
  }
}

问题在于输入不满足该条件的情况:因为我们不运行 setValue,所以组件不会重新渲染,而且因为组件不会重新渲染,所以输入值无法正确控制。但是,即使我们确实向该处理程序添加了 else { setValue(value) },Preact 也足够智能,可以检测到值何时未更改,因此它不会重新渲染组件。这让我们使用 `refs` 来弥合 DOM 状态和 Preact 状态之间的差距。

¥The problem with this is in the cases where the input fails that condition: because we don't run setValue, the component doesn't rerender, and because the component doesn't rerender, the input value is not correctly controlled. However, even if we did add a else { setValue(value) } to that handler, Preact is smart enough to detect when the value hasn't changed and so it will not rerender the component. This leaves us with `refs` to bridge the gap between the DOM state and Preact's state.

有关 Preact 中受控组件的更多信息,请参阅 Jovi De Croock 的 受控输入

¥For more information on controlled components in Preact, see Controlled Inputs by Jovi De Croock.

以下是如何使用受控组件来限制输入字段中的字符数的示例:

¥Here's an example of how you might use a controlled component to limit the number of characters in an input field:

class LimitedInput extends Component {
  state = { value: '' }
  inputRef = createRef(null)

  onInput = (e) => {
    if (e.currentTarget.value.length <= 3) {
      this.setState({ value: e.currentTarget.value });
    } else {
      const start = this.inputRef.current.selectionStart;
      const end = this.inputRef.current.selectionEnd;
      const diffLength = Math.abs(e.currentTarget.value.length - this.state.value.length);
      this.inputRef.current.value = this.state.value;
      // Restore selection
      this.inputRef.current.setSelectionRange(start - diffLength, end - diffLength);
    }
  }

  render(_, { value }) {
    return (
      <div class="form-example">
        <label>
          This input is limited to 3 characters:{' '}
          <input ref={this.inputRef} value={value} onInput={this.onInput} />
        </label>
      </div>
    );
  }
}
Run in REPL