In this guide, we'll walk through building a dynamic user feedback form using the @perseid/form library, a powerful alternative to Formly and Superforms. You'll see how @perseid/form makes it easy to manage form state, validation, and conditional rendering. The form we'll build will ask users to rate a service and provide feedback. Depending on the rating, it will either show a "thank you" message or prompt the user to provide additional feedback.
? Let's get started!
The first step is to define the form configuration. This configuration outlines how the form behaves, including the fields, steps, and the flow between them. Here, we're going to create fields for a rating and a review, with conditional logic based on the user's rating. We'll also define messages for both positive and negative feedback.
Here's the configuration code:
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"], }, }, };
In this configuration:
The key point to grasp here is the function of the fields property. It defines the structure of the data that will be submitted, essentially acting as a data model. In contrast, the steps property outlines the form's flow, determining how these fields will be presented to the user.
Now that we have the configuration, it's time to build the actual UI that will render the form. Using @perseid/form/svelte, we can create custom field components to manage user interactions for each part of the form.
Here's the core Svelte component:
<!-- 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}
Here, the Field component uses the path prop to decide what to render:
"Thank you" messages that appear based on the rating. The form will dynamically adjust its fields and steps based on user input.
Pretty cool, right?
Now that our form configuration and component are ready, let's integrate them into a basic Svelte app. Here's the code to initialize and render the form:
// 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, });
This code mounts the form to the DOM. The Form component, which connects our configuration and Field component, handles everything else.
Alright, we have our app logic, but if you run the code now, you'll see that it's a bit... raw ?
So, let's pimp the form by adding some styles and animations! Below is a simple stylesheet that makes it way more appealing:
// 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; } }
And voilà ?
Congratulations! ? You've just built a dynamic user feedback form with Perseid and Svelte.
In this tutorial, we went over how to:
Feel free to experiment with additional fields and steps to suit your use case. Have fun building awesome forms! ?
The above is the detailed content of Building a User Feedback Form with Svelte and Perseid. For more information, please follow other related articles on the PHP Chinese website!