Monthly Archives: January 2022

Svelte Complex Forms, part 3 – components using get/setContext() for less passing props

In my prior two posts, i created a more complex dynamic and hierarchical form, including radio buttons, and extracted the “sveltey” html into a couple components.

In this post, i’m refining my components a bit, to reduce the boilerplate attributes needed for each instance. Currently, they look like this:

<ZInput
	nameAttr="fullname"
	nameLabel="Full Name"
	bindValue={$form.fullname}
	errorText={$errors?.fullname}
	{handleChange}
/>

<ZRadio
	nameAttr="prefix"
	nameLabel="Prefix"
	itemList={prefixOptions}
	itemValueChecked="n/a"
	errorText={$errors?.prefix}
	{handleChange}></ZRadio>

I would like to remove the errorText and handleChange props, even the bindValue if possible.

To bypass explicit props just to reference the $errors object and the handleChange functions, i’ll use Svelte’s getContext() and setContext(). I borrowed it from the svelte-forms-lib optional components, but mine are different in that they also include a <label> and $errors indicator. For a great explanation of Svelte Context, see Tan Li Hau’s Store vs Context tweet/video.

In the form.svelte page, it contains the bare <form> element, and the createForm() call which returns $form, $errors, and the handle* functions. We add new setContext() call, setting those objects. This will allow child components to getContext() and access the same objects, without requiring them to be passed in via props.

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

// allows for referencing the internal $form and $errors from this page, 
// so we can add the array handlers
setContext(key, {
	form,
	errors,
	handleChange,
	handleSubmit,
});

Now in the child components, add getContext() and modify the use.

ZInput <script> section:

// allows the Form* components to share state with the parent form
const { form, errors, handleChange } = getContext(key);

Then we won’t have to pass in those objects via prop. However, we still have a problem…

Impedance-mismatch: flat $errors keys vs. hierarchical $forms object

As discussed in my prior post, the underlying $form store is “flat”, but the underlying svelte store is hierarchical, meaning we have to refer to $form by that flat key. For example:

$form$errors
$form[‘fullname’]$errors.fullname
$form[‘profile.address’]$errors.profile.address

$form[‘contacts[0].name’]

$errors.contacts[0].name
$form[‘contacts[2].name’]$errors.contacts[2].name

I think this is the inevitable mismatch where the html form has a list of flat “name”s in a traditional POST, but the data it is handling is hierarchical.

If we try to use the key passed in for “name”, it won’t work to find the matching error message. So we have to convert the string key “contacts[2].name” to the equivalent object reference $errors.contacts[2].name in order to display the linked error message (if any).

I ended up using a function from stack overflow, which allows me to pass in an object, and a string key, which returns the value from the object the string key would point to:

// window.a = {b: {c: {d: {etc: 'success'}}}}
// getScopedObj(window, `a.b.c.d.etc`)             // success
// getScopedObj(window, `a['b']["c"].d.etc`)       // success
// getScopedObj(window, `a['INVALID']["c"].d.etc`) // undefined
export function getScopedObj(scope, str) {
    // console.log(`getScopedObj(scope, ${str})`);
    let obj = scope, arr;

    try {
        arr = str.split(/[\[\]\.]/) // split by [,],.
            .filter(el => el)             // filter out empty one
            .map(el => el.replace(/^['"]+|['"]+$/g, '')); // remove string quotation
        arr.forEach(el => obj = obj[el])
    } catch (e) {
        obj = undefined;
    }

    return obj;
}

Then my final component looks like this:

<script>
    import { getScopedObj } from "$lib/util";
    import { getContext } from "svelte";
    import { key } from "svelte-forms-lib";
    export let nameAttr;
    export let nameLabel;
    export let bindValue;
    // allows the Form* components to share state with the parent form
    const { form, errors, handleChange } = getContext(key);
</script>
<div>
    <label for={nameAttr}>{nameLabel}</label>
    <input
        placeholder={nameLabel}
        name={nameAttr}
        on:change={handleChange}
        on:blur={handleChange}
        bind:value={bindValue}
    />
    {#if getScopedObj($errors, nameAttr)}
        <div class="form-error">{getScopedObj($errors, nameAttr)}</div>
    {/if}
</div>

<ZRadio> can be modified in a similar way. Now the component tags are more concise:

<ZInput
	nameAttr={`contacts[${j}].email`}
	nameLabel="Email"
	bindValue={$form.contacts[j].email}
	/>

<ZRadio
	nameAttr={`contacts[${j}].contacttype`}
	nameLabel="Contact Type"
	itemList={contactTypes}
	itemValueChecked="n/a"
	/>

… and the user behavior is the same.

code for part 3 is at:

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

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