Skip to content

Instantly share code, notes, and snippets.

@bennettscience
Last active June 21, 2022 12:06
Show Gist options
  • Save bennettscience/935909ddcc3f04638ee052f058e41380 to your computer and use it in GitHub Desktop.
Save bennettscience/935909ddcc3f04638ee052f058e41380 to your computer and use it in GitHub Desktop.
A Svelte form wrapper to dynamically load forms based on button click
<script>
/*
This is a generic wrapper component for any forms. Pass in an array of {field} objets to be
rendered into the form.
Submissions are all converted into JSON before being passed back to the parent for handling. This allows the
parent compoenent to determine the API route the form is submitting to rather than adding those options here.
*/
import Input from './formFields/Input.svelte';
import Select from './formFields/Select.svelte';
import DateTime from './formFields/DateTime.svelte';
import Number from './formFields/Number.svelte';
import TextArea from './formFields/TextArea.svelte';
import Link from './formFields/Link.svelte';
import Hidden from './formFields/Hidden.svelte';
import Radio from './formFields/Radio.svelte';
// pass in a function handler to onSubmit by the parent.
export let onSubmit;
export let fields;
export let buttonLabel = 'Submit';
export let disabled = false;
// Convert fields from [ { name: 'name', value: 'Value' } ] to { name : Value } which is more useful when submitting a form
const fieldsToObject = (fields) =>
fields.reduce(
(item, data) => ({ ...item, [data.name]: data.value }),
{},
);
// When submitting, turn our fields representation into a JSON body and pass back to the parent for handling.
const handleSubmit = () => onSubmit(fieldsToObject(fields));
</script>
<!-- When submitting, prevent the default action which would result in a refreshed page -->
<form on:submit|preventDefault={() => handleSubmit(fields)}>
<!-- Loop the fields and render the correct representation based on field.type -->
{#each fields as field}
{#if field.type === 'Input'}
<Input
name={field.name}
bind:value={field.value}
id={field.id}
label={field.label}
placeholder={field.placeholder}
/>
{:else if field.type === 'TextArea'}
<TextArea
name={field.name}
bind:value={field.value}
id={field.id}
label={field.label}
placeholder={field.placeholder}
/>
{:else if field.type === 'Select'}
<Select
name={field.name}
bind:value={field.value}
id={field.id}
label={field.label}
options={field.options}
/>
{:else if field.type === 'DateTime'}
<DateTime
name={field.name}
bind:value={field.value}
id={field.id}
datetime={field.datetime}
label={field.label}
shift={field.shift}
/>
{:else if field.type === 'Number'}
<Number
name={field.name}
bind:value={field.value}
id={field.id}
label={field.label}
placeholder={field.placeholder}
/>
{:else if field.type === 'Link'}
<Link
name={field.name}
uri={field.uri}
bind:value={field.value}
placeholder={field.placeholder}
label={field.label}
/>
{:else if field.type === 'Radio'}
<Radio
bind:value={field.value}
label={field.label}
name={field.name}
options={field.options}
/>
{/if}
{/each}
<button tabindex="0" type="submit" {disabled}>{buttonLabel}</button>
</form>
<style>
/* styles */
</style>
<script>
import { createEventDispatcher } from 'svelte';
import Form from './Form.svelte';
const d = createEventDispatcher();
// We need two props when the component is loaded:
// - dataTarget -> type of update (event, links, registrations, presenters, etc)
// - resourceId -> Unique ID of the event being modified.
export let dataTarget;
export let resourceId;
// Determine the endpoint and method for each update operation.
const targets = {
course: {
uri: `/courses/${resourceId}`,
method: 'PUT',
},
presenters: {
uri: `/courses/${resourceId}/presenters`,
method: 'POST',
},
}
// Store the fields needed in the form after fetching the data when
// the wrapper loads.
let fields = [];
// Course changes
// Event types and locations are dynamic, so they need to be loaded when the component is mounted.
// Their resources are at different endpoints so fetch those
const editCourse = async () => {
// Editing a course requires three endpoints:
// - the course
// - event types
// - locations
// Fetch them all now and get the fields prepped for the form.
// Returns an object as {course: {}, types: [{}...], locations: [{}...]}
const urls = [`/courses/${resourceId}`, `/courses/types`, `/locations`];
await Promise.all(
urls.map(async (url) => {
const resp = await fetch(url);
return await resp.json();
}),
).then(
([course, types, locations]) =>
(data = {
course,
types,
locations,
}),
);
// The object that comes back from the API is bigger than we need for presenters. Map
// the object down to the approved keys and build out a form fields array.
// Because `data` is an array of objects, Object.keys() is needed to check the keys within
// each object instead of filtering directly.
const allowed = ['id', 'name']; // the only keys we want from types and locations
Object.keys(data.course)
.filter((item) => {
let courseAllowed = [
'title',
'description',
'location',
'type',
'location',
'course_size',
'starts',
'ends',
];
if (courseAllowed.includes(item)) {
return true;
} else {
return false;
}
})
.map(async (key) => {
// Map each key into a field object for the form
let field;
if (key === 'title') {
field = {
name: 'title',
type: 'Input',
value: data.course[key],
placeholder: 'Enter title...',
label: 'Event Title',
};
} else if (key === 'description') {
field = {
name: 'description',
type: 'TextArea',
value: data.course[key],
placeholder: '',
label: 'Event description',
};
} else if (key === 'starts' || key === 'ends') {
field = {
name: key,
label: key,
id: key,
type: 'DateTime',
value: new Date(data.course[key]).toISOString(),
datetime: new Date(data.course[key]).toISOString(),
};
} else if (key === 'course_size') {
field = {
name: 'course_size',
id: 'course-size',
type: 'Number',
value: data.course[key],
placeholder: data[key],
label: 'Attendance limit',
};
} else if (key === 'type') {
let types = data.types.map((item) =>
allowed.reduce((idx, current) => {
idx[current] = item[current];
return { value: item.id, label: item.name };
}, {}),
);
field = {
name: 'coursetype_id',
id: 'course-type',
type: 'Select',
value: data.course[key].id,
label: 'Event type',
options: types,
};
} else if (key === 'location') {
let locations = data.locations.map((item) =>
allowed.reduce((idx, current) => {
idx[current] = item[current];
return { value: item.id, label: item.name };
}, {}),
);
field = {
name: 'location_id',
id: 'location',
type: 'Select',
value: data.course[key].id,
label: 'Location',
options: locations,
};
}
fields.push(field);
fields = fields.sort((a, b) => {
return allowed.indexOf(a.name) - allowed.indexOf(b.name);
});
});
fields = fields;
};
// Prefill form fields with existing values
if (dataTarget === 'course') {
editCourse();
} else if (dataTarget === 'presenters') {
editPresenters();
} // ...other targets...
// The benefit of a generic wrapper is that all data can be processed the same
// way, regardless of the `dataTarget`. Wrap all requests into a JSON object to
// send to the backend.
const handleSubmit = async (data) => {
let endpoint = targets[dataTarget].uri;
let method = targets[dataTarget].method;
try {
let req = await fetch(endpoint, {
method: method,
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
});
let response = await req.json();
// Send an event to the main <App> component to display and update
// to the user.
d('handleToast', {
isError: false,
toastBody: response.message,
});
setTimeout(() => d('success'), 2000);
} catch (e) {
d('handleToast', {
isError: true,
toastBody: e,
});
}
};
</script>
{#await fields then fields}
{#if dataTarget === 'duplicate'}
<b>Duplicate Event</b>
<p>
Enter a new title, start time, and end time for the event. Other
event details will be copied automatically.
</p>
{/if}
<Form {fields} onSubmit={(body) => handleSubmit(body)} />
{/await}
<style>
/* styles down here */
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment