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