Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

表单与输入

Forms and Inputs

表单和表单输入是交互式应用的重要组成部分。在 Leptos 中,有两种与输入交互的基本模式。如果你熟悉 React、SolidJS 或类似的框架,你可能会认出它们:使用**受控 (controlled)非受控 (uncontrolled)**输入。

Forms and form inputs are an important part of interactive apps. There are two basic patterns for interacting with inputs in Leptos, which you may recognize if you’re familiar with React, SolidJS, or a similar framework: using controlled or uncontrolled inputs.

受控输入

Controlled Inputs

在 “受控输入” 中,框架控制着输入元素的状态。在每个 input 事件上,它都会更新一个持有当前状态的本地信号,这反过来又会更新输入的 value 属性。

In a "controlled input," the framework controls the state of the input element. On every input event, it updates a local signal that holds the current state, which in turn updates the value prop of the input.

有两点重要事项需要记住:

There are two important things to remember:

  1. input 事件在元素(几乎)每次更改时触发,而 change 事件则在输入框失去焦点时(或多或少)触发。你可能想要的是 on:input,但我们赋予你选择的自由。

  2. value 特性 (attribute) 仅设置输入的初始值,也就是说,它仅在用户开始输入之前更新输入。在此之后,value 属性 (property) 会继续更新输入。出于这个原因,你通常希望设置 prop:value。(对于 <input type="checkbox"> 上的 checkedprop:checked 也是如此。)

  3. The input event fires on (almost) every change to the element, while the change event fires (more or less) when you unfocus the input. You probably want on:input, but we give you the freedom to choose.

  4. The value attribute only sets the initial value of the input, i.e., it only updates the input up to the point that you begin typing. The value property continues updating the input after that. You usually want to set prop:value for this reason. (The same is true for checked and prop:checked on an <input type="checkbox">.)

let (name, set_name) = signal("Controlled".to_string());

view! {
    <input type="text"
        // 添加 :target 让我们能够以类型安全的方式访问触发事件的元素
        // adding :target gives us typed access to the element
        // that is the target of the event that fires
        on:input:target=move |ev| {
            // .value() 返回 HTML 输入元素的当前值
            // .value() returns the current value of an HTML input element
            set_name.set(ev.target().value());
        }

        // `prop:` 语法允许你更新 DOM 属性 (property),
        // 而不是特性 (attribute)。
        // the `prop:` syntax lets you update a DOM property,
        // rather than an attribute.
        prop:value=name
    />
    <p>"Name is: " {name}</p>
}

为什么需要 prop:value

Why do you need prop:value?

网络浏览器是现存最普遍、最稳定的图形用户界面渲染平台。在其存在的三十年里,它们还保持了令人难以置信的向后兼容性。不可避免地,这意味着存在一些怪癖。

Web browsers are the most ubiquitous and stable platform for rendering graphical user interfaces in existence. They have also maintained an incredible backwards compatibility over their three decades of existence. Inevitably, this means there are some quirks.

一个奇怪的怪癖是 HTML 特性 (attributes) 和 DOM 元素属性 (properties) 之间存在区别。也就是说,从 HTML 解析并可以使用 .setAttribute() 在 DOM 元素上设置的 “特性 (attribute)”,与作为该解析后的 HTML 元素的 JavaScript 类表示的一个字段的 “属性 (property)” 之间存在区别。

One odd quirk is that there is a distinction between HTML attributes and DOM element properties, i.e., between something called an “attribute” which is parsed from HTML and can be set on a DOM element with .setAttribute(), and something called a “property” which is a field of the JavaScript class representation of that parsed HTML element.

<input value=...> 的情况下,设置 value 特性 (attribute) 被定义为设置输入的初始值,而设置 value 属性 (property) 则设置其当前值。通过打开 about:blank 并在浏览器控制台中逐行运行以下 JavaScript,可能会更容易理解这一点:

In the case of an <input value=...>, setting the value attribute is defined as setting the initial value for the input, and setting value property sets its current value. It may be easier to understand this by opening about:blank and running the following JavaScript in the browser console, line by line:

// 创建一个输入框并将其追加到 DOM

// create an input and append it to the DOM const el = document.createElement("input"); document.body.appendChild(el);

el.setAttribute("value", "test"); // 更新输入框

// updates the input

el.setAttribute("value", "another test"); // 再次更新输入框

// updates the input again

// 现在去输入框里输入:删除一些字符等。

// now go and type into the input: delete some characters, etc.

el.setAttribute("value", "one more time?");

// 什么都不应该改变。现在设置 “初始值” 不起任何作用

// nothing should have changed. Setting the "initial value" does nothing now

// 然而...

// however... el.value = "But this works";


许多其他前端框架混淆了特性 (attributes) 和属性 (properties),或者为输入框创建了一个能正确设置值的特殊情况。也许 Leptos 也应该这样做,但目前,我更倾向于让用户最大限度地控制他们是在设置特性还是属性,并尽力教育人们了解实际的浏览器底层行为,而不是将其掩盖。

Many other frontend frameworks conflate attributes and properties, or create a special case for inputs that sets the value correctly. Maybe Leptos should do this too, but for now, I prefer giving users the maximum amount of control over whether they’re setting an attribute or a property, and doing my best to educate people about the actual underlying browser behavior rather than obscuring it.

使用 bind: 简化受控输入

Simplifying Controlled Inputs with bind:

坚持 Web 标准以及明确区分 “从信号读取” 和 “写入信号” 是好事,但以这种方式创建受控输入有时可能看起来像是不必要的样板代码。

Adherence to Web standards and a clear division between “reading from a signal” and “writing to a signal” are good, but creating controlled inputs in this way can sometimes seem like more boilerplate than is really necessary.

Leptos 还包含一个特殊的 bind: 语法,用于自动将信号绑定到输入。它们的作用与上面的 “受控输入” 模式完全相同:创建一个更新信号的事件监听器,以及一个从信号读取的动态属性。你可以将 bind:value 用于文本输入,bind:checked 用于复选框,以及 bind:group 用于单选按钮组。

Leptos also includes a special bind: syntax for inputs that allows you to automatically bind signals to inputs. They do exactly the same thing as the “controlled input” pattern above: create an event listener that updates the signal, and a dynamic property that reads from the signal. You can use bind:value for text inputs, bind:checked for checkboxes, and bind:group for radio button groups.

let (name, set_name) = signal("Controlled".to_string());
let email = RwSignal::new("".to_string());
let favorite_color = RwSignal::new("red".to_string());
let spam_me = RwSignal::new(true);

view! {
    <input type="text"
        bind:value=(name, set_name)
    />
    <input type="email"
        bind:value=email
    />
    <label>
        "Please send me lots of spam email."
        <input type="checkbox"
            bind:checked=spam_me
        />
    </label>
    <fieldset>
        <legend>"Favorite color"</legend>
        <label>
            "Red"
            <input
                type="radio"
                name="color"
                value="red"
                bind:group=favorite_color
            />
        </label>
        <label>
            "Green"
            <input
                type="radio"
                name="color"
                value="green"
                bind:group=favorite_color
            />
        </label>
        <label>
            "Blue"
            <input
                type="radio"
                name="color"
                value="blue"
                bind:group=favorite_color
            />
        </label>
    </fieldset>
    <p>"Your favorite color is " {favorite_color} "."</p>
    <p>"Name is: " {name}</p>
    <p>"Email is: " {email}</p>
    <Show when=move || spam_me.get()>
        <p>"You’ll receive cool bonus content!"</p>
    </Show>
}

非受控输入

Uncontrolled Inputs

在 “非受控输入” 中,浏览器控制输入元素的状态。我们不连续更新信号来持有其值,而是使用 NodeRef 在我们想要获取其值时访问输入。

In an "uncontrolled input," the browser controls the state of the input element. Rather than continuously updating a signal to hold its value, we use a NodeRef to access the input when we want to get its value.

在这个例子中,我们仅在 <form> 触发 submit 事件时通知框架。请注意 leptos::html 模块的使用,它为每个 HTML 元素提供了大量类型。

In this example, we only notify the framework when the <form> fires a submit event. Note the use of the leptos::html module, which provides a bunch of types for every HTML element.

let (name, set_name) = signal("Uncontrolled".to_string());

let input_element: NodeRef<html::Input> = NodeRef::new();

view! {
    <form on:submit=on_submit> // on_submit 在下面定义
        <input type="text"
            value=name
            node_ref=input_element
        />
        <input type="submit" value="Submit"/>
    </form>
    <p>"Name is: " {name}</p>
}

到目前为止,这个视图应该是显而易见的。注意两点:

The view should be pretty self-explanatory by now. Note two things:

  1. 与受控输入示例不同,我们使用 value(而不是 prop:value)。这是因为我们只是设置输入的初始值,并让浏览器控制其状态。(我们也可以使用 prop:value。)

  2. 我们使用 node_ref=... 来填充 NodeRef。(旧示例有时使用 _ref。它们是同一回事,但 node_ref 有更好的 rust-analyzer 支持。)

  3. Unlike in the controlled input example, we use value (not prop:value). This is because we’re just setting the initial value of the input, and letting the browser control its state. (We could use prop:value instead.)

  4. We use node_ref=... to fill the NodeRef. (Older examples sometimes use _ref. They are the same thing, but node_ref has better rust-analyzer support.)

NodeRef 是一种响应式智能指针:我们可以使用它来访问底层的 DOM 节点。当元素渲染时,它的值将被设置。

NodeRef is a kind of reactive smart pointer: we can use it to access the underlying DOM node. Its value will be set when the element is rendered.

let on_submit = move |ev: SubmitEvent| {
    // 阻止页面重载!
    // stop the page from reloading!
    ev.prevent_default();

    // 在这里,我们将从输入中提取值
    // here, we'll extract the value from the input
    let value = input_element
        .get()
        // 事件处理程序只有在视图挂载到 DOM 之后才能触发,
        // 所以 `NodeRef` 将会是 `Some`
        // event handlers can only fire after the view
        // is mounted to the DOM, so the `NodeRef` will be `Some`
        .expect("<input> should be mounted")
        // `leptos::HtmlElement<html::Input>` 为 `web_sys::HtmlInputElement` 实现了 `Deref`。
        // 这意味着我们可以调用 `HtmlInputElement::value()` 来获取输入的当前值。
        // `leptos::HtmlElement<html::Input>` implements `Deref`
        // to a `web_sys::HtmlInputElement`.
        // this means we can call`HtmlInputElement::value()`
        // to get the current value of the input
        .value();
    set_name.set(value);
};

我们的 on_submit 处理程序将访问输入的值并使用它来调用 set_name.set()。要访问存储在 NodeRef 中的 DOM 节点,我们可以简单地将其作为函数调用(或使用 .get())。这将返回 Option<leptos::HtmlElement<html::Input>>,但我们知道该元素已经挂载(否则你怎么能触发这个事件!),所以在这里解包是安全的。

Our on_submit handler will access the input’s value and use it to call set_name.set(). To access the DOM node stored in the NodeRef, we can simply call it as a function (or using .get()). This will return Option<leptos::HtmlElement<html::Input>>, but we know that the element has already been mounted (how else did you fire this event!), so it's safe to unwrap here.

然后我们可以调用 .value() 来从输入中获取值,因为 NodeRef 让我们能够访问到正确类型的 HTML 元素。

We can then call .value() to get the value out of the input, because NodeRef gives us access to a correctly-typed HTML element.

查看 web_sysHtmlElement 以了解有关使用 leptos::HtmlElement 的更多信息。此外,请参阅本页末尾完整的 CodeSandbox 示例。

Take a look at web_sys and HtmlElement to learn more about using a leptos::HtmlElement. Also, see the full CodeSandbox example at the end of this page.

特殊情况:<textarea><select>

Special Cases: <textarea> and <select>

有两个表单元素往往会以不同的方式引起混淆。

Two form elements tend to cause some confusion in different ways.

<textarea>

<input> 不同,<textarea> 元素在 HTML 中不支持 value 特性。相反,它通过其 HTML 子节点中的纯文本节点接收初始值。

Unlike <input>, the <textarea> element does not support a value attribute in HTML. Instead, it receives its initial value as a plain text node in its HTML children.

因此,如果你想在服务器端渲染一个初始值,并且让该值在浏览器中也具有响应性,你可以既给它传递一个初始文本节点作为子节点,又使用 prop:value 来设置其当前值。

So if you’d like to server render an initial value, and have the value also react in the browser, you can both pass it an initial text node as a child and use prop:value to set its current value.

view! {
    <textarea
        prop:value=move || some_value.get()
        on:input:target=move |ev| some_value.set(ev.target().value())
    >
        {some_value}
    </textarea>
}

<select>

同样地,<select> 元素可以通过 <select> 本身上的 value 属性来控制,这将选中具有该值的 <option>

The <select> element can likewise be controlled via a value property on the <select> itself, which will select whichever <option> has that value.

let (value, set_value) = signal(0i32);
view! {
  <select
    on:change:target=move |ev| {
      set_value.set(ev.target().value().parse().unwrap());
    }
    prop:value=move || value.get().to_string()
  >
    <option value="0">"0"</option>
    <option value="1">"1"</option>
    <option value="2">"2"</option>
  </select>
  // 一个循环切换选项的按钮
  // a button that will cycle through the options
  <button on:click=move |_| set_value.update(|n| {
    if *n == 2 {
      *n = 0;
    } else {
      *n += 1;
    }
  })>
    "Next Option"
  </button>
}

受控 vs 非受控表单 CodeSandbox

点击打开 CodeSandbox。

Click to open CodeSandbox.

CodeSandbox 源代码 CodeSandbox Source
use leptos::{ev::SubmitEvent};
use leptos::prelude::*;

#[component]
fn App() -> impl IntoView {
    view! {
        <h2>"Controlled Component"</h2>
        <ControlledComponent/>
        <h2>"Uncontrolled Component"</h2>
        <UncontrolledComponent/>
    }
}

#[component]
fn ControlledComponent() -> impl IntoView {
    // 创建一个信号来持有该值
    // create a signal to hold the value
    let (name, set_name) = signal("Controlled".to_string());

    view! {
        <input type="text"
            // 每当输入改变时触发事件
            // 在事件后添加 :target 让我们可以通过 ev.target() 访问到类型正确的元素
            // fire an event whenever the input changes
            // adding :target after the event gives us access to
            // a correctly-typed element at ev.target()
            on:input:target=move |ev| {
                set_name.set(ev.target().value());
            }

            // `prop:` 语法允许你更新 DOM 属性 (property),
            // 而不是特性 (attribute)。
            //
            // 重要提示:`value` *特性* 仅设置初始值,直到你做出更改。
            // `value` *属性* 设置当前值。
            // 这是 DOM 的一个怪癖;不是我发明的。
            // 其他框架掩盖了这一点;我认为让你访问真实的浏览器工作方式更重要。
            //
            // tl;dr: 为表单输入使用 prop:value
            // the `prop:` syntax lets you update a DOM property,
            // rather than an attribute.
            //
            // IMPORTANT: the `value` *attribute* only sets the
            // initial value, until you have made a change.
            // The `value` *property* sets the current value.
            // This is a quirk of the DOM; I didn't invent it.
            // Other frameworks gloss this over; I think it's
            // more important to give you access to the browser
            // as it really works.
            //
            // tl;dr: use prop:value for form inputs
            prop:value=name
        />
        <p>"Name is: " {name}</p>
    }
}

#[component]
fn UncontrolledComponent() -> impl IntoView {
    // 导入 <input> 的类型
    // import the type for <input>
    use leptos::html::Input;

    let (name, set_name) = signal("Uncontrolled".to_string());

    // 我们将使用 NodeRef 来存储对输入元素的引用
    // 这将在创建元素时被填充
    // we'll use a NodeRef to store a reference to the input element
    // this will be filled when the element is created
    let input_element: NodeRef<Input> = NodeRef::new();

    // 当表单发生 `submit` 事件时触发
    // 这会将 <input> 的值存储在我们的信号中
    // fires when the form `submit` event happens
    // this will store the value of the <input> in our signal
    let on_submit = move |ev: SubmitEvent| {
        // 阻止页面重载!
        // stop the page from reloading!
        ev.prevent_default();

        // 在这里,我们将从输入中提取值
        // here, we'll extract the value from the input
        let value = input_element.get()
            // 事件处理程序只有在视图挂载到 DOM 之后才能触发,
            // 所以 `NodeRef` 将会是 `Some`
            // event handlers can only fire after the view
            // is mounted to the DOM, so the `NodeRef` will be `Some`
            .expect("<input> to exist")
            // `NodeRef` 为 DOM 元素类型实现了 `Deref`
            // 这意味着我们可以调用 `HtmlInputElement::value()` 来获取输入的当前值。
            // `NodeRef` implements `Deref` for the DOM element type
            // this means we can call`HtmlInputElement::value()`
            // to get the current value of the input
            .value();
        set_name.set(value);
    };

    view! {
        <form on:submit=on_submit>
            <input type="text"
                // 在这里,我们使用 `value` *特性* 仅设置初始值,
                // 让浏览器在此之后维护状态
                // here, we use the `value` *attribute* to set only
                // the initial value, letting the browser maintain
                // the state after that
                value=name

                // 将此输入的引用存储 in `input_element`
                // store a reference to this input in `input_element`
                node_ref=input_element
            />
            <input type="submit" value="Submit"/>
        </form>
        <p>"Name is: " {name}</p>
    }
}

// 这个 `main` 函数是应用的入口点
// 它只是将我们的组件挂载到 <body>
// 因为我们将其定义为 `fn App`,我们现在可以在模板中以 <App/> 的形式使用它
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
    leptos::mount::mount_to_body(App)
}