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