Svelte Complex Forms, part 2 – refactoring into custom components

As a followup to my last post Svelte Complex Forms with radio buttons, dynamic arrays, and Validation (svelte-forms-lib and yup), i created a more complex dynamic and hierarchical form, including radio buttons. I was glad it worked, but not happy about the more complex array prefixing.

In this post, i will attempt to extract the “sveltey” html into a couple components. I’ll just name them with a Z-prefix for kicks.

  • a component with the label + input type=text + error (ZInput)
  • a component with the label + input type=radio + error (ZRadio)

Creating them under src/lib/c/*.svelte

The idea here is to supply all the variables in the component as props (a la Component
Format
), so the components are relatively generalized in the app, and we can set them in our existing each loops for the form.

ZInput : input type=text

Let’s start simple – the text input. Our first example looks like this:

<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}
	/>
	{#if $errors.fullname}
		<div class="error-text">{$errors.fullname}</div>
	{/if}
</div>

The props to extract could look to be:

  • nameAttr (fullname)
  • nameLabel (Full Name)
  • bindValue ($form.fullname)
  • errorText ($errors.fullname)
  • handleChange (needs to be referenced to the parent page/component)

Looking at the more nested example, the same props apply:

<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}
	/>
	{#if $errors?.contacts[j]?.email}
		<div class="error-text">{$errors.contacts[j].email}</div>
	{/if}
</div>

Here’s the component i will create:

<script>
    export let nameAttr;
    export let nameLabel;
    export let bindValue;
    export let errorText;
    export let handleChange;
</script>
<div>
    <label for={nameAttr}>{nameLabel}</label>
    <input
        placeholder={nameLabel}
        name={nameAttr}
        on:change={handleChange}
        on:blur={handleChange}
        bind:value={bindValue}
    />
    {#if errorText}
        <div class="error-text">{errorText}</div>
    {/if}
</div>

… and the ways to call it from the form.svelte page:

<ZInput nameAttr="fullname"
	nameLabel="Full Name"
	bindValue={$form.fullname} 
	errorText={$errors?.fullname} 
	handleChange={handleChange}></ZInput>
<ZInput nameAttr={`contacts[${j}].email`}
	nameLabel="Email"
	bindValue={$form.contacts[j].email} 
	errorText={$errors?.contacts[j]?.email} 
	handleChange={handleChange}></ZInput>

ZRadio – input type=radio in an each loop

The radio buttons are a little more, since we’ll have to supply an array of objects to the component, in a generic way.

The current code looks like this:

<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>

We’ll extract the following:

  • nameAttr
  • nameLabel
  • itemList [ { id, name, label, value} ]
  • itemValueChecked (if there is a pre-checked item – single choice)
  • errorText
  • handleChange

And we get…

<script>
    export let nameAttr;
    export let nameLabel;
    export let itemList;
    export let itemValueChecked;
    export let errorText;
    export let handleChange;

    function isChecked(checkedValue, itemValue) {
        if(checkedValue === itemValue) {
            return true;
        }
        else {
            return false;
        }
    }
</script>
<div>
    <label for={nameAttr}>{nameLabel}</label>
    {#each itemList as p, i}
        <label class="compact">
            <input
                type="radio"
                id={`${nameAttr}-${p.value}`}
                name={nameAttr}
                value={p.value}
                on:change={handleChange}
                on:blur={handleChange}
                checked={isChecked(itemValueChecked, p.value)}
            />
            <span> {p.label}{#if p.label != p.id}[{p.id}]{/if}</span>
        </label>
    {/each}
    {#if errorText}
        <div class="error-text">{errorText}</div>
    {/if}
</div>

Now the instantiation is a little more complex, as we’ll have to alter or remap the itemList objects to a consistent keys. For simple, non-object array lists, the id, name, label, and value are all the same. But the complex object lists, they are distinct.

The ZRadio component code:

<script>
    export let nameAttr;
    export let nameLabel;
    export let itemList;
    export let itemValueChecked;
    export let errorText;
    export let handleChange;

    function isChecked(checkedValue, itemValue) {
        if(checkedValue === itemValue) {
            return true;
        }
        else {
            return false;
        }
    }
</script>
<div>
    <label for={nameAttr}>{nameLabel}</label>
    {#each itemList as p, i}
        <label class="compact">
            <input
                type="radio"
                id={`${nameAttr}-${p.value}`}
                name={nameAttr}
                value={p.value}
                on:change={handleChange}
                on:blur={handleChange}
                checked={isChecked(itemValueChecked, p.value)}
            />
            <span> {p.label} [{p.id}]</span>
        </label>
    {/each}
    {#if errorText}
        <div class="error-text">{errorText}</div>
    {/if}
</div>

Remapping the item lists:

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

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

onMount(() => {
	// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#using_map_to_reformat_objects_in_an_array

	prefixOptions = simpleRemap(prefixOptions);
	genderOptions = simpleRemap(genderOptions);
	contactTypes = simpleRemap(contactTypes);

	products = products.map((element) => {
		return {
			id: element.product_id,
			name: element.product_name,
			label: element.product_name,
			value: element.product_id,
		};
	});
});

function simpleRemap(itemList) {
	return itemList.map(element => {
		return {
			id: element,
			name: element,
			label: element,
			value: element,
		};
	});
}

Then calling it:

<ZRadio
	nameAttr="prefix"
	nameLabel="Prefix"
	itemList={prefixOptions}
	itemValueChecked="n/a"
	errorText={$errors?.prefix}
	{handleChange}></ZRadio>
<ZRadio
	nameAttr={`contacts[${j}].contacttype`}
	nameLabel="Contact Type"
	itemList={contactTypes}
	itemValueChecked="n/a"
	errorText={$errors.contacts[j]?.contacttype}
	{handleChange}></ZRadio>

<ZRadio
	nameAttr={`contacts[${j}].product_id`}
	nameLabel="Product"
	itemList={products}
	itemValueChecked="n/a"
	errorText={$errors.contacts[j]?.product_id}
	{handleChange}></ZRadio>

Now the functionality should be exactly the same, but the form code is less messy and more readable.

The source code changes are in the same git repo as Part 1, but in a branch “part2”.

https://github.com/nohea/enehana-complex-svelte-form/tree/part2

Leave a Reply

Your email address will not be published. Required fields are marked *