在本指南中,我们将逐步使用 @perseid/form 库构建动态用户反馈表单,这是 Formly 和 Superforms 的强大替代品。您将看到 @perseid/form 如何轻松管理表单状态、验证和条件渲染。我们将构建的表单将要求用户对服务进行评分并提供反馈。根据评级,它会显示“谢谢”消息或提示用户提供其他反馈。
?让我们开始吧!
第一步是定义表单配置。此配置概述了表单的行为方式,包括字段、步骤以及它们之间的流程。在这里,我们将根据用户的评分使用 条件逻辑 创建评分和评论字段。我们还将定义积极和消极反馈的消息。
这是配置代码:
import { type Configuration } from "@perseid/form"; const formConfiguration: Configuration = { // Root step-the form will start from there. root: "feedback", // Callback triggered on form submission. onSubmit(data) { alert(`Submitting the following JSON: ${JSON.stringify(data)}`); return Promise.resolve(); }, // `fields` define the data model the form is going to deal with. // Expect the submitted data JSON to match this schema. fields: { rating: { type: "integer", required: true, }, review: { type: "string", required: true, // Display this field only if condition is met... condition: (inputs) => inputs.rating !== null && (inputs.rating as number) < 3, }, // Type `null` means that the value of this field will not be included in submitted data. submit: { type: "null", submit: true, }, message_good: { type: "null", }, message_bad: { type: "null", }, }, // Now that fields are defined, you can organize them in a single or multiple steps, // depending on the UI you want to build! steps: { feedback: { fields: ["rating", "review", "submit"], // Whether to submit the form at the end of this step. submit: true, // Next step is conditionned to previous user inputs... nextStep: (inputs) => (inputs.rating as number) < 3 ? "thanks_bad" : "thanks_good", }, thanks_good: { fields: ["message_good"], }, thanks_bad: { fields: ["message_bad"], }, }, };
在此配置中:
这里要掌握的重点是fields属性的作用。它定义了将提交的数据的结构,本质上充当数据模型。相比之下,steps 属性概述了表单的流程,确定如何将这些字段呈现给用户。
现在我们已经有了配置,是时候构建将呈现表单的实际 UI 了。使用@perseid/form/svelte,我们可以创建自定义字段组件来管理表单每个部分的用户交互。
这是核心 Svelte 组件:
<!-- The actual Svelte component, used to build the UI! --> <script lang="ts" context="module"> import type { FormFieldProps } from "@perseid/form/svelte"; </script> <script lang="ts"> export let path: FormFieldProps['path']; export let type: FormFieldProps['type']; export let value: FormFieldProps['value']; export let Field: FormFieldProps['Field']; export let error: FormFieldProps['error']; export let status: FormFieldProps['status']; export let engine: FormFieldProps['engine']; export let fields: FormFieldProps['fields']; export let isActive: FormFieldProps['isActive']; export let activeStep: FormFieldProps['activeStep']; export let isRequired: FormFieldProps['isRequired']; export let setActiveStep: FormFieldProps['setActiveStep']; export let useSubscription: FormFieldProps['useSubscription']; let currentRating = 0; $: currentValue = value as number; $: fields, isActive, activeStep, setActiveStep, useSubscription, type, error, Field, isRequired; const setCurrentRating = (newRating: number) => { currentRating = newRating; }; const handleReviewChange = (event: Event) => { engine.userAction({ type: "input", path, data: (event.target as HTMLTextAreaElement).value }) }; </script> <!-- Display a different element depending on the field... --> {#if path === 'thanks_good.1.message_good'} <div class="message"> <h1>Thanks for the feedback ?</h1> <p>We are glad you enjoyed!</p> </div> {:else if path === 'thanks_bad.1.message_bad'} <div class="message"> <h1>We're sorry to hear that ?</h1> <p>We'll do better next time, promise!</p> </div> {:else if path === 'feedback.0.review'} <div class={`review ${status === "error" ? "review--error" : ""}`}> <label for="#review">Could you tell us more?</label> <textarea id="review" on:change={handleReviewChange} /> </div> {:else if path === 'feedback.0.rating'} <!-- Depending on the field status, define some extra classes for styling... --> <div role="button" tabindex="0" class={`rating ${status === "error" ? "rating--error" : ""}`} on:mouseleave={() => { setCurrentRating(currentValue ?? 0); }} > <h1>How would you rate our service?</h1> {#each [1, 2, 3, 4, 5] as rating (rating)} <span role="button" tabindex="0" class={`rating__star ${currentRating >= rating ? "rating__star--active" : ""}`} on:mouseenter={() => { setCurrentRating(rating); }} on:keydown={() => {}} on:click={() => { // On click, notify the form engine about new user input. engine.userAction({ type: "input", path, data: rating }); }} ></span> {/each} </div> {:else} <!-- path === 'feedback.0.submit' --> <button class="submit" on:click={() => { engine.userAction({ type: "input", path, data: true }); }} > Submit </button> {/if}
这里,Field 组件使用 path 属性来决定渲染什么:
根据评分显示的“谢谢”消息。该表单将根据用户输入动态调整其字段和步骤。
很酷,对吧?
现在我们的表单配置和组件已准备就绪,让我们将它们集成到基本的 Svelte 应用程序中。这是初始化和渲染表单的代码:
// Let's run the app! // Creating Svelte root... const container = document.querySelector("#root") as unknown as HTMLElement; container.innerHTML = ''; new Form({ props: { Field: Field, configuration: formConfiguration, }, target: container, });
此代码将表单安装到 DOM。 Form 组件连接我们的配置和 Field 组件,处理其他所有事情。
好吧,我们有了应用程序逻辑,但是如果您现在运行代码,您会发现它有点......原始?
所以,让我们通过添加一些样式和动画来修饰表单!下面是一个简单的样式表,使它更具吸引力:
// A few animations for fun... @keyframes swipe-out { 0% { opacity: 1; transform: translateX(0); } 75% { opacity: 0; transform: translateX(-100%); } 100% { opacity: 0; transform: translateX(-100%); } } @keyframes swipe-in-one { 0% { opacity: 0; transform: translateX(100%); } 75% { transform: translateX(0); } 100% { opacity: 1; transform: translateX(0); } } @keyframes swipe-in-two { 0% { opacity: 0; transform: translateX(0); } 75% { transform: translateX(-100%); } 100% { opacity: 1; transform: translateX(-100%); } } @keyframes bubble-in { 0% { transform: scale(0.5); } 75% { transform: scale(1.5); } 100% { transform: scale(1); } } @keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } } // Some global basic styling... * { box-sizing: border-box; } body { margin: 0; display: grid; height: 100vh; color: #aaaaaa; align-items: center; font-family: "Helvetica", sans-serif; } // And form-specific styling. .perseid-form { width: 100%; margin: auto; &__steps { display: flex; overflow: hidden; } &__step { min-width: 100%; padding: 1rem 3rem; animation: 500ms ease-in-out forwards swipe-out; &__fields { display: grid; row-gap: 2rem; } } &__step[class*="active"]:first-child { animation: 500ms ease-in-out forwards swipe-in-one; } &__step[class*="active"]:last-child:not(:first-child) { animation: 500ms ease-in-out forwards swipe-in-two; } } .submit { border: none; cursor: pointer; padding: 1rem 2rem; border-radius: 8px; color: #fefefe; font-size: 1.25rem; background: #46c0b0; justify-self: flex-end; transition: all 250ms ease-in-out; &:hover { background: #4cccbb; } } .rating { position: relative; padding: 0.25rem 0; &__star { cursor: pointer; display: inline-block; font-size: 2rem; min-width: 2rem; min-height: 2rem; &::after { content: "⚪️"; } &--active { animation: 250ms ease-in-out forwards bubble-in; &::after { content: "?"; } } } &[class*="error"] { &::after { left: 0; bottom: -1.5rem; color: #f13232; position: absolute; font-size: 0.75rem; content: "? This field is required"; animation: 250ms ease-in-out forwards fade-in; } } } .review { display: grid; row-gap: 1rem; position: relative; animation: 250ms ease-in-out forwards fade-in; label { font-size: 1.25rem; } textarea { resize: none; min-height: 5rem; border-radius: 8px; border: 1px solid #46c0b0; transition: all 250ms ease-in-out; } &[class*="error"] { &::after { left: 0; bottom: -1.5rem; color: #f13232; position: absolute; font-size: 0.75rem; content: "? This field is required"; animation: 250ms ease-in-out forwards fade-in; } } } @media screen and (min-width: 30rem) { .perseid-form { max-width: 30rem; } }
瞧?
恭喜! ?您刚刚使用 Perseid 和 Svelte 构建了一个动态用户反馈表单。
在本教程中,我们介绍了如何:
请随意尝试其他字段和步骤以适合您的用例。享受构建精彩表单的乐趣! ?
以上是使用 Svelte 和 Perseid 构建用户反馈表的详细内容。更多信息请关注PHP中文网其他相关文章!