Tag Archives: yup

SvelteKit Form Actions bound to TypeScript class + Validation (yup) w/dynamic array.

Last year, i wrote some posts using Svelte w/a forms lib + validation. Now that SvelteKit 1.0 is out, it includes new Form features, specifically Form Actions, which is a great way to do form handling in a very natural way for web browsers and web apps.

Here, i’m intending to build a proof-of-concept which does the following:

  • Use the Form Actions feature in /Kit
  • Create a TS class which maps to what the formData will contain
  • Use the yup validation library for validating the form submission
  • on failure of validation, return the error plus the form data object back to re-render the field in the form already entered, so the user doesn’t have to re-enter
  • use:enhance support for non-JS web clients

Although this is based on my prior Svelte Form posts, i took influence from WebJeda’s video on SvelteKit forms and validation. In my case, i go an extra step to implement a dynamic form with arrays.

Architecture

create +page.svelte for page w/form (create/update)

create +page.server.js for handling the Form Actions

create a class which maps to the form values: ProfileFormData.ts

create validation classes for use in the form actions ‘create’ | ‘update’ before the mutation in the hypothetical data store.

Validation errors get returned back to the +page.svelte form for redisplay

dynamic array in the FormData object will add form fields and support validation

I’m not going to use a form library, only native SvelteKit features, such as form actions, PageData load() from *.server.ts, bind:value on <input>, and the reactive $: variables.

+page.svelte

The +page.svelte is the heart of the functionality. Under SvelteKit, it will run both client-side JS and server-side JS.

Since we define a matching +page.server.ts, that will run first – server-side only. If one of the form actions return, the +page.svelte receives as form ActionData property.

/** @type {import('./$types').ActionData} */
export let form;

If this is a first page load, nothing is in the form property, so we create a new ProfileFormData() to render the HTML form on-screen.

But if this is either a re-render after a invalid ‘create’ form submit, or if it is an ‘update’ form after loading the record from the data source, the form.formValue contains the structure of the form to render, as well as the data values to populate it.

    // formDefault must match the shape of the form to be rendered 
    // and also fill in the defaults or the prior form submission w/ validation errors
    $: formDefault = setFormDefault(form?.formValue);

    // $: console.log('formDefault ', formDefault);
    // $: console.log('form.formValue changed ', form?.formValue);
    
    function setFormDefault(formValue) {
        if(formValue) {
            console.log('setFormDefault() from form.formValue');
            console.log('formValue: ', formValue);
            return formValue;
        }
        else {
            console.log('setFormDefault() from new ProfileFormData()');
            return new ProfileFormData();
        }
    }

+page.server.ts

The +page.server.ts is dedicated primarily for handling the SvelteKit Form Actions. In my case, i define two – one for ‘create’ and one for ‘update’. I will handle both cases in the +page.svelte <form>, since most fields are shared.

flat form parameter list to unflatten() JS nested object

I do use the unflatten() function from the ‘flat’ package. This allows for mapping a “flat” parameter list in formData() like:

{
  fullname: 'the name',
  email: 'asdf@asdf.d',
  'profile.address': '',
  'contacts.0.contacttype': 'atype',
  'contacts.0.name': 'dsfg',
  'contacts.1.contacttype': 'btype',
  'contacts.1.name': '',
  submit: 'submit button'
}

… to a nested data structure, for validation and persistence.

{
  fullname: 'the name',
  email: 'asdf@asdf.d',
  profile: { address: '' },
  contacts: [
    { contacttype: 'atype', name: 'dsfg' },
    { contacttype: 'btype', name: '' }
  ],
  submit: 'submit button'
}

When the form input elements are written out, i have to construct the names to match the array index format, such are contacts.0.propertyname, contacts.1.propertyname.

{#each formDefault.contacts as c, idx}
                <div>
                    <label for="contacts.{idx}.contacttype">contact type </label>
                    <input
                        type="text"
                        name="contacts.{idx}.contacttype"
                        class=""
                        placeholder="contact type"
                        bind:value={formDefault.contacts[idx].contacttype}
                    />
                    {#if form?.errors?.[`contacts[${idx}].contacttype`]}
                    <span class="error-text">{form?.errors?.[`contacts[${idx}].contacttype`]}</span>
                    {/if}
                </div>

At the form POST handler in +page.server.ts, they get mapped back to a real JS array. After which, the object can be validated. If it passes, save to the data store. If it fails validation, return error status, plus the list of validation errors, as well as the form object data, so it may be redisplayed in the form fields without requiring re-entry.

// formValue will model the form rendering
// it could be a default value, or else a validation error response to modify and resubmit
let formValue: {[key: string]: string};

export const actions: Actions = {
  create: async ({ cookies, request }) => {
    console.log("action: create");
    const fd = await request.formData();
    console.log("fd.forEach() ");
    fd.forEach((val, key) => {
      console.log(`${key}: `, val);
    });
    //formValue = formDataToProfileData(fd);
    formValue = formDataToFormValue(fd);
    console.log("formValue ", formValue);
    console.log("flatten(formValue) ", flatten(formValue));
    console.log("unflatten(formValue) ", unflatten(flatten(formValue)));

    // do create
    try {
      const result = await profileFormDataSchema.validate(formValue, validateOptions);
    }
    catch(error) {
      console.log('error: ', error);
      console.log('error.value: ', error.value);
      const errors = error.inner.reduce((acc, err) => {
        return { ...acc, [err.path]: err.message };
      }, {});
      console.log('errors: ', errors);

      return {
        status: 'error',
        errors: errors,
        formValue: {...error.value},
      };
    }
     
    return { 
      status: 'inserted',
      formValue,
    };
  },

Challenges in mutating the JS object arrays vs. copying them (bind:value problems)

One of the problems in binding the dynamic array form elements to their matching JS object array elements is that i couldn’t recreate the formDefault.contacts = [] array without losing the reference in <input bind:value={formDefault.contacts[idx].contacttype} />

Once i create the form object with the contacts array, i needed to keep the reference and not bash/overwrite it. So when implementing the [add contact] and delete contact [X] links, i use the array.push() and array.splice() methods, to modify the original reference only.

<a href="#" class="btn" on:click|preventDefault={() => {
    console.log('add ');
    console.log('formDefault.contacts = ', formDefault.contacts);
    formDefault.contacts.push({ contacttype: '', name: '', });
    formDefault.contacts = formDefault.contacts;
}}>add a contact</a>
<a href="#" class="btn" on:click|preventDefault={() => {
    console.log('del ', idx);
    formDefault.contacts.splice(idx, 1);
    formDefault.contacts = formDefault.contacts;
    console.log('formDefault.contacts: ', formDefault.contacts);
}}>[X]</a>

use:enhance

With a little testing, this code works with the progressive enhancement / use:enhance features on the forms. Just disable javascript and test.

Running example

Outcomes

Working on this, i hit a number of stumbling blocks. However, it led me to a better understanding of the SvelteKit 1.0 conventions and details.

I’m also confident in using the framework form actions.

References

Source code for this article:
https://github.com/nohea/enehana-sveltekit-form-actions-yup

Example app deployed to Vercel:
https://enehana-sveltekit-form-actions-yup.vercel.app/

WebJeda video on Form validation using Yup in Sveltekit
https://youtu.be/sTLwZc09FWw

Svelte Complex Forms with radio buttons, dynamic arrays, and Validation (svelte-forms-lib and yup)

Overview

Building new web apps in 2021 using a Svelte front-end is fun, with more reactivity and less code. Almost any web app will have some kind of form, and it helps to have a basic form builder and validation framework.

In this post, i’ll be exploring the svelte-forms-lib library to create a form, bound to a hierarchical object, and also wired up to a validation object. The form will support dynamically adding/removing items from an array property. It will also support radio buttons, which must be handled differently, since they are multiple <input> elements tied to the same variable.

Building a Complex Form, with svelte-forms-lib

The form i want to build will be a mix of property types:

  • Simple properties, such as ‘fullname’ (text input) and ‘prefix’ (radio button input)
  • A named object property (‘profile’), which will have a subsection for key/value pairs like ‘address’ and ‘gender’
  • A named array property (‘contacts’), which can contain zero or more contacts (with properties ‘name’, ’email’, and ‘contacttype’)

These various properties will have their own validation rules, which we will deal with later. They will also be sent to the backend on form submit, as a single JSON object. Something like this:

{
    fullname: 'Keoki Gonsalves',
    prefix: 'Mr.',
    profile: {
        address: '123 Main St.',
        gender: 'M'
    },
    contacts: [
        {
            contacttype: 'friend',
            name: 'Gina Kekahuna',
            email: 'ginak@example.com',
        },
        {
            contacttype: 'aquaintance',
            name: 'Marlon Waits',
            email: 'mwaits@example.com',
        },
    ]
}

Now that we’ve visualized our data model on the client-side, we can build a form to allow the user to populate that. Our challenge is to manage the slight impedance-mismatch between a form builder library and the object structure. Plus, allowing for an easy to use validation system.

Creating the svelte project, and creating the form with arrays and text inputs

I’ll create a vanilla sveltekit project, but it just needs to be svelte 3:

npm init svelte@next enehana-complex-svelte-form
cd enehana-complex-svelte-form
npm install
npm run dev -- --open

Making a page under /src/routes/form.svelte for this example. I will put as much as possible in this one page for simplicity’s sake, but normally we would split a few things off, as desired.

npm install svelte-forms-lib

We’ll start with a simple <script> section and the html form elements. Our example will be based on the svelte-forms-lib Forms Array example. Let’s just use regular text inputs to start, but do our array which will support multiple contacts on the form.

script section will call createForm() with the initial properties, and return a $form and $errors observable/store for linking the form elements with the JS object.

<script>
	import { createForm } from 'svelte-forms-lib';

	const formProps = {
		initialValues: {
			fullname: '',
			prefix: '',
			profile: {
				address: '',
				gender: ''
			},
			contacts: []
		},
		onSubmit: (values) => {
			console.log('onSubmit (via handleSubmit): ', JSON.stringify(values));
		}
	};

	const { form, errors, state, handleChange, handleSubmit, handleReset } = createForm(formProps);

	const addcontact = () => {
		console.log('addcontact()');
		$form.contacts = $form.contacts.concat({ name: '', email: '', contacttype: '' });
		$errors.contacts = $errors.contacts.concat({ name: '', email: '', contacttype: '' });
	};

	const removecontact = (i) => () => {
		$form.contacts = $form.contacts.filter((u, j) => j !== i);
		$errors.contacts = $errors.contacts.filter((u, j) => j !== i);
	};
</script>

Note there are add() and remove() functions for the contacts array and matching HTML form input sections.

The HTML form we will build out to match, w/css.

<main>
<div>
	<h1>Complex Svelte Form Example</h1>

	<h4>Test Form</h4>
	<form on:submit={handleSubmit}>
		<div>
			<label for="fullname"> Full Name </label>
			<input
				type="text"
				name="fullname"
				bind:value={$form.fullname}
				class=""
				placeholder="Full Name"
				on:change={handleChange}
				on:blur={handleChange}
			/>
		</div>

        <div>
			<label for="profile.address">Profile Address </label>
			<input
				type="text"
				name="profile.address"
				bind:value={$form.profile.address}
				class=""
				placeholder="Profile Address"
				on:change={handleChange}
				on:blur={handleChange}
			/>
		</div>

		<input type="submit" name="submit" value="submit button" />
	</form>
</div>

<div>
	<b>$form: </b>
	<pre>{JSON.stringify($form)}</pre>
</div>
<div>
	<b>$errors: </b>
	<pre>{JSON.stringify($errors)}</pre>
</div>

</main>

<style>
    label {
        display: inline-block;
        width: 200px;
    }

	.error-text {
		color: red;
	}
</style>

This is a simple 2 input form, but you can see the 2-way binding in action:

Now let’s add the dynamic contacts: [] array to the form. We loop using #each on the $form.contacts array, which we start empty. Each time we click “add”, an object is pushed to the array, which is bound to a new form group. Those inputs will be bound to the item of the array, based on their 0-based index value (0, 1, 2, …).

        <h4>Contacts</h4>
        {#each $form.contacts as contact, j}
          <div class="form-group">
            <div>
              <label for={`contacts[${j}].name`}>Name</label>
              <input
                name={`contacts[${j}].name`}
                placeholder="name"
                on:change={handleChange}
                on:blur={handleChange}
                bind:value={$form.contacts[j].name}
              />
            </div>
    
            <div>
                <label for={`contacts[${j}].email`}>Email</label>
                <input
                placeholder="email"
                name={`contacts[${j}].email`}
                on:change={handleChange}
                on:blur={handleChange}
                bind:value={$form.contacts[j].email}
              />
            </div>
    
            {#if $form.contacts.length === j + 1}
                <button type="button" on:click={removecontact(j)}>[- remove last contact]</button>
            {/if}
          </div>
        {/each}
    
        {#if $form.contacts}
            <div>
                <button on:click|preventDefault={addcontact}>[+ add contact]</button>
            </div>
        {/if}

The importance of the name=”” attribute matching the bound js object

We must keep in mind that the HTML form and the $form store is a more “flat” key/value pair data structure, whereas the object is it bound to is a dynamic javascript object, which can easily model hierarchical objects and arrays. This means the way we assign an <input name=””> needs to match the object. Otherwise, our form elements will modify the wrong sections of the object. I had a lot of trouble with this until i figured it out.

The <input> maps by the name=”” attribute, or the id=”” attribute if there is no name. The name/id attribute will be the key in the $form svelte store observable, as well as the matching $errors store.

Examples of the 2-way binding between form and objects:

I try to keep the naming as clear as possible.

formobject
<input name=”fullname” bind:value={$form.fullname} />$form.fullname
<input name=”profile.address” bind:value={$form.profile.address} />$form.profile.address
<input name=”contacts[0].name” bind:value={$form.contacts[0].name} />$form.contacts[0].name
{#each $form.contacts as c, x}
<input name={`contacts[${x}].name`} bind:value={$form.contacts[x].name} />
{/each}
$form.contacts[x].name
{#each $form.contacts as c, x}
{#each contactTypes as ct, y}
<label>
<input type=”radio” name={`contacts[${x}].contacttype`} value={ct} /> {ct}
</label>
{/each}
{/each}
$form.contacts[x].contacttype

It can get a little complicated on the html form side, but i like it clear on the javascript side. Theoretically, the HTML could be wrapped into a svelte component to make the syntax cleaner. Let’s leave that to another day.

Adding in validation using ‘yup’

yup is a form validation library, inspired by Eran Hammer‘s joi.

It seems real simple.

  • npm i yup
  • import * as yup from ‘yup’;
  • define your schema declaratively
  • set it as the validationSchema in the svelte-forms-lib createForm
  • throw in the html $errors next to the form fields, for a visual feedback on invalid data

The validation will run at <form on:submit={handleSubmit}> by svelte-forms-lib, and optionally at the input form element level if you add the on:change={handleChange} and/or on:blur={handleChange} svelte attributes.

Add in a validator schema:

    const validator = yup.object().shape({
        fullname: yup.string().required(),
        prefix: yup.string(),
        profile: yup.object().shape({
            address: yup
                .string()
                .required(),
            gender: yup
                .string()
        }),
        contacts: yup.array().of(
            yup.object().shape({
                contacttype: yup.string(),
                name: yup.string().required(),
                email: yup.string(),
            })
        )
    });

adding a validationSchema property to formProps:

const formProps = {
    ...
    validationSchema: validator,
    ...
}

then adding the error/validation messages near the fields:

{#if $errors.fullname}
	<div class="error-text">{$errors.fullname}</div>
{/if}

and for the deeply-nested fields, i found they often have missing property errors, so i’m using the new javascript optional chaining operator ?.

{#if $errors?.contacts[j]?.name}
  <div class="error-text">{$errors.contacts[j].name}</div>
{/if}

Now we’ve got this working:

Note that onSubmit() doesn’t fire until all the forms pass yup validation.

Handling radio buttons and checkboxes

Radio buttons and checkboxes require special handling. At first i thought i had to wire up my own idiom between svelte-forms-lib and svelte bind:group handler, but it turns out not to be the case.

Sometimes a radio, checkbox, or select drop down will have a list of simple values. In other cases, there could be complex values, in the cases where a list of items is pulled from a databases. There could be a product_id to store, but a product_name to display. I’m going to try examples of each.

The simple examples will be prefixes and genderOptions. We defined them as simple arrays:

const prefixOptions = ['Ms.', 'Mr.', 'Dr.'];
const genderOptions = ['F', 'M', 'X'];
const contactTypes = ['friend', 'family', 'aquaintence'];

The complex example:

    const products = [
        { product_id: 101, product_name: "Boots", },
        { product_id: 202, product_name: "Shoes", },
        { product_id: 333, product_name: "Jeans", },
    ];

For ‘prefix’, we have a similar label, but instead of one <input>, we get one for each option. So we loop thru the options using an {#each} loop, being careful to:

  • set all name=”” attributes to the same input name
  • set the value=”” to the actual value to store in the variable
  • use the on:change={handleChange} handler
<div>
	<label for="prefix"> Prefix </label>
	{#each prefixOptions as pre, i}
		<label class="compact">
			<input id={`prefix-${pre}`} 
			name="prefix" 
			value={pre}
			type="radio" 
			on:change={handleChange}
			on:blur={handleChange}
			/>
		<span> {pre} </span>
		</label>
	{/each}
	{#if $errors.prefix}
		<div class="error-text">{$errors.prefix}</div>
	{/if}
</div>

For $form.profile.gender, it is almost identical, but the naming must follow one level deeper:

<div>
	<label for="profile.gender"> Profile Gender</label>
	{#each genderOptions as g, i}
		<label class="compact">
			<input id={`prefix-${g}`} 
			name="profile.gender" 
			value={g}
			type="radio" 
			on:change={handleChange}
			on:blur={handleChange}
			/>
		<span> {g} </span>
		</label>
	{/each}
	{#if $errors.profile?.gender}
		<div class="error-text">{$errors.profile.gender}</div>
	{/if}
</div>

And with Contact Type, we need to include the array indexer in the name=”” attribute, so we don’t stomp on values from other array items. It is already inside another {#each} loop, iterating over $form.contacts

<div>
    <label for={`contacts[${j}].contacttype`}>Contact Type</label>
    {#each contactTypes as ct, i}
        <label class="compact">
            <input 
            type="radio" 
            id={`contacts[${j}].contacttype-${ct}`} 
            name={`contacts[${j}].contacttype`}
            value={ct}
            on:change={handleChange}
            on:blur={handleChange}
            />
        <span> {ct} </span>
        </label>
    {/each}
    {#if $errors.contacts[j]?.contacttype}
        <div class="error-text">{$errors.contacts[j].contacttype}</div>
    {/if}
</div>

Finally, let’s combine the array radio with a complex list of items: the ID will be the value, but the display will be a name or description

<div>
	<label for={`contacts[${j}].product_id`}>Product</label>
	{#each products as p, i}
		<label class="compact">
			<input 
			type="radio" 
			id={`contacts[${j}].product_id-${p.product_id}`} 
			name={`contacts[${j}].product_id`}
			value={p.product_id}
			on:change={handleChange}
			on:blur={handleChange}
			/>
		<span> {p.product_name} [{p.product_id}]</span>
		</label>
	{/each}
	{#if $errors.contacts[j]?.product_id}
		<div class="error-text">{$errors.contacts[j].product_id}</div>
	{/if}
</div>

Conclusion

My takeaway is that based on this test of more complex form building and validation using svelte, i’m now confident i could build larger web apps the way i expect — with validation and dynamic forms, including arrays.

I’d like to improve and refactor the examples into components— either the ones provided, or making my own.

References

https://svelte-forms-lib-sapper-docs.vercel.app/array

Nefe James – Top form validation libraries in Svelte

Source code at github

https://github.com/nohea/enehana-complex-svelte-form part 1