<Form/> 组件
The <Form/> Component
链接和表单有时看起来完全不相关。但事实上,它们的工作方式非常相似。
Links and forms sometimes seem completely unrelated. But, in fact, they work in very similar ways.
在纯 HTML 中,有三种导航到另一个页面的方式:
In plain HTML, there are three ways to navigate to another page:
-
一个链接到另一个页面的
<a>元素:使用GETHTTP 方法导航到其href属性中的 URL。 -
An
<a>element that links to another page: Navigates to the URL in itshrefattribute with theGETHTTP method. -
一个
<form method="GET">:使用GETHTTP 方法导航到其action属性中的 URL,并将来自输入框的表单数据编码在 URL 查询字符串中。 -
A
<form method="GET">: Navigates to the URL in itsactionattribute with theGETHTTP method and the form data from its inputs encoded in the URL query string. -
一个
<form method="POST">:使用POSTHTTP 方法导航到其action属性中的 URL,并将来自输入框的表单数据编码在请求体(body)中。 -
A
<form method="POST">: Navigates to the URL in itsactionattribute with thePOSTHTTP method and the form data from its inputs encoded in the body of the request.
既然我们有了客户端路由管理器,我们就可以在不重新加载页面的情况下进行客户端链接导航,也就是说,不需要服务器与客户端之间的完整往返。同理,我们也可以用同样的方式进行客户端表单导航。
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
路由管理器提供了一个 <Form> 组件,它的工作方式类似于 HTML 的 <form> 元素,但使用客户端导航而不是完整的页面刷新。<Form/> 同时支持 GET 和 POST 请求。使用 method="GET" 时,它将导航到编码在表单数据中的 URL。使用 method="POST" 时,它将发起一个 POST 请求并处理服务器的响应。
The router provides a <Form> component, which works like the HTML <form> element, but uses client-side navigations instead of full page reloads. <Form/> works with both GET and POST requests. With method="GET", it will navigate to the URL encoded in the form data. With method="POST" it will make a POST request and handle the server’s response.
<Form/> 为我们将在后续章节中看到的 <ActionForm/> 和 <MultiActionForm/> 等组件奠定了基础。但它本身也支持一些强大的模式。
<Form/> provides the basis for some components like <ActionForm/> and <MultiActionForm/> that we’ll see in later chapters. But it also enables some powerful patterns of its own.
例如,假设你想创建一个搜索框,它在用户搜索时实时更新搜索结果,无需重新加载页面,但同时也将搜索内容存储在 URL 中,以便用户可以复制并粘贴链接与他人分享结果。
For example, imagine that you want to create a search field that updates search results in real time as the user searches, without a page reload, but that also stores the search in the URL so a user can copy and paste it to share results with someone else.
事实证明,我们到目前为止学到的模式使得实现这一点非常容易。
It turns out that the patterns we’ve learned so far make this easy to implement.
async fn fetch_results() {
// 一些用于获取搜索结果的异步函数
// some async function to fetch our search results
}
#[component]
pub fn FormExample() -> impl IntoView {
// 响应式地访问 URL 查询字符串
// reactive access to URL query strings
let query = use_query_map();
// 搜索内容存储为 ?q=
// search stored as ?q=
let search = move || query.read().get("q").unwrap_or_default();
// 由搜索字符串驱动的资源(Resource)
// a resource driven by the search string
let search_results = Resource::new(search, |_| fetch_results());
view! {
<Form method="GET" action="">
<input type="search" name="q" value=search/>
<input type="submit"/>
</Form>
<Transition fallback=move || ()>
/* 渲染搜索结果 */
/* render search results */
{todo!()}
</Transition>
}
}
每当你点击“提交(Submit)”时,<Form/> 都会“导航”到 ?q={search}。但由于这种导航是在客户端完成的,所以不会有页面闪烁或重新加载。URL 查询字符串发生了变化,这触发了 search 的更新。因为 search 是 search_results 资源的源信号,这会触发 search_results 重新加载其资源。在加载新结果期间,<Transition/> 会继续显示当前的搜索结果。加载完成后,它会切换到显示新结果。
Whenever you click Submit, the <Form/> will “navigate” to ?q={search}. But because this navigation is done on the client side, there’s no page flicker or reload. The URL query string changes, which triggers search to update. Because search is the source signal for the search_results resource, this triggers search_results to reload its resource. The <Transition/> continues displaying the current search results until the new ones have loaded. When they are complete, it switches to displaying the new result.
这是一个很棒的模式。数据流极其清晰:所有数据从 URL 流向资源,最后进入 UI。应用程序的当前状态存储在 URL 中,这意味着你可以刷新页面或将链接发给朋友,它会完全按照你的预期显示。一旦我们引入服务器端渲染,这个模式也将被证明具有很强的容错性:因为它在底层使用了 <form> 元素和 URL,所以即使在客户端没有加载 WASM 的情况下,它也能很好地工作。
This is a great pattern. The data flow is extremely clear: all data flows from the URL to the resource into the UI. The current state of the application is stored in the URL, which means you can refresh the page or text the link to a friend and it will show exactly what you’re expecting. And once we introduce server rendering, this pattern will prove to be really fault-tolerant, too: because it uses a <form> element and URLs under the hood, it actually works really well without even loading your WASM on the client.
实际上,我们可以更进一步,做一些巧妙的处理:
We can actually take it a step further and do something kind of clever:
view! {
<Form method="GET" action="">
<input type="search" name="q" value=search
oninput="this.form.requestSubmit()"
/>
</Form>
}
你会注意到这个版本去掉了“提交(Submit)”按钮。相反,我们在输入框中添加了一个 oninput 属性。请注意,这 不是 on:input,后者会监听 input 事件并运行 Rust 代码。没有冒号的 oninput 是纯 HTML 属性。所以该字符串实际上是一个 JavaScript 字符串。this.form 为我们提供了输入框所属的表单。requestSubmit() 会在 <form> 上触发 submit 事件,这会被 <Form/> 捕获,就像我们点击了“提交”按钮一样。现在,表单会在每次按键或输入时“导航”,从而使 URL(以及搜索内容)与用户输入的内容保持完美同步。
You’ll notice that this version drops the Submit button. Instead, we add an oninput attribute to the input. Note that this is not on:input, which would listen for the input event and run some Rust code. Without the colon, oninput is the plain HTML attribute. So the string is actually a JavaScript string. this.form gives us the form the input is attached to. requestSubmit() fires the submit event on the <form>, which is caught by <Form/> just as if we had clicked a Submit button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the user’s input as they type.
:::admonish sandbox title="在线示例" collapsible=true
:::
CodeSandbox Source
use leptos::prelude::*;
use leptos_router::components::{Form, Route, Router, Routes};
use leptos_router::hooks::use_query_map;
use leptos_router::path;
#[component]
pub fn App() -> impl IntoView {
view! {
<Router>
<h1><code>"<Form/>"</code></h1>
<main>
<Routes fallback=|| "Not found.">
<Route path=path!("") view=FormExample/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn FormExample() -> impl IntoView {
// 响应式访问 URL 查询参数
// reactive access to URL query
let query = use_query_map();
let name = move || query.read().get("name").unwrap_or_default();
let number = move || query.read().get("number").unwrap_or_default();
let select = move || query.read().get("select").unwrap_or_default();
view! {
// 读取出 URL 查询字符串
// read out the URL query strings
<table>
<tr>
<td><code>"name"</code></td>
<td>{name}</td>
</tr>
<tr>
<td><code>"number"</code></td>
<td>{number}</td>
</tr>
<tr>
<td><code>"select"</code></td>
<td>{select}</td>
</tr>
</table>
// 每当提交时,<Form/> 都会进行导航
// <Form/> will navigate whenever submitted
<h2>"手动提交"</h2>
// <h2>"Manual Submission"</h2>
<Form method="GET" action="">
// input 的 name 决定了查询字符串的键(key)
// input names determine query string key
<input type="text" name="name" value=name/>
<input type="number" name="number" value=number/>
<select name="select">
// `selected` 将设置起始选中状态
// `selected` will set which starts as selected
<option selected=move || select() == "A">
"A"
</option>
<option selected=move || select() == "B">
"B"
</option>
<option selected=move || select() == "C">
"C"
</option>
</select>
// 提交操作应该引起客户端导航,而不是完整的页面重新加载
// submitting should cause a client-side
// navigation, not a full reload
<input type="submit"/>
</Form>
// 这个 <Form/> 使用了一些 JavaScript 来在每次输入时提交
// This <Form/> uses some JavaScript to submit
// on every input
<h2>"自动提交"</h2>
// <h2>"Automatic Submission"</h2>
<Form method="GET" action="">
<input
type="text"
name="name"
value=name
// 这个 oninput 属性会导致表单在每次字段输入时提交
// this oninput attribute will cause the
// form to submit on every input to the field
oninput="this.form.requestSubmit()"
/>
<input
type="number"
name="number"
value=number
oninput="this.form.requestSubmit()"
/>
<select name="select"
onchange="this.form.requestSubmit()"
>
<option selected=move || select() == "A">
"A"
</option>
<option selected=move || select() == "B">
"B"
</option>
<option selected=move || select() == "C">
"C"
</option>
</select>
// 提交操作应该引起客户端导航,而不是完整的页面重新加载
// submitting should cause a client-side
// navigation, not a full reload
<input type="submit"/>
</Form>
}
}
fn main() {
leptos::mount::mount_to_body(App)
}