Query Recipes
Save and refresh the list
A form in a dialog that saves a member, marks the fields the server rejects, closes on success and refreshes exactly the lists the save touched.
Markdown version of this page, for assistants (Markdown for AI)
On this page
Example
Edit a member and save. Then try an email outside harbor.studio: the server answers
with a field error.
The example runs in the browser.
Requests the server received
- None yet.
Code
<!-- src/routes/members/+page.svelte -->
<script lang="ts">
import { createMutation, createQuery } from '@svelte-lean/query';
import { fieldErrors, issues } from '@svelte-lean/query/forms';
import { listMembers, updateMember } from '$api/members';
import type { Member } from '$api/types';
const uid = $props.id();
const members = createQuery(listMembers, () => ({}));
let dialog = $state<HTMLDialogElement>();
let editing = $state<Member | null>(null);
// `invalidates` on updateMember refreshes tag('Member', id) and 'Members': this list, once.
const save = createMutation(updateMember, { onSuccess: () => dialog?.close() });
function edit(member: Member) {
editing = member;
save.reset();
dialog?.showModal();
}
function submit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
event.preventDefault();
if (!editing) return;
const form = new FormData(event.currentTarget);
save
.run({ id: editing.id, name: String(form.get('name')), email: String(form.get('email')) })
.catch(() => {}); // the error is in save.error: field marks below
}
</script>
{#if members.refreshing}<progress aria-label="Refreshing"></progress>{/if}
<ul>
{#each members.data?.rows ?? [] as member (member.id)}
<li>
{member.name} · {member.email}
<button type="button" onclick={() => edit(member)}>Edit {member.name}</button>
</li>
{/each}
</ul>
<dialog bind:this={dialog} aria-labelledby="{uid}-title" onclose={() => (editing = null)}>
{#if editing}
<form {@attach fieldErrors(() => save.error)} onsubmit={submit}>
<h2 id="{uid}-title">Edit {editing.name}</h2>
<div data-slean="field">
<label>Name <input name="name" type="text" value={editing.name} /></label>
<p data-slean-part="error" id="{uid}-name-error">
{issues(save.error, 'name').join(' ')}
</p>
</div>
<div data-slean="field">
<label>Email <input name="email" type="email" value={editing.email} /></label>
<p data-slean-part="error" id="{uid}-email-error">
{issues(save.error, 'email').join(' ')}
</p>
</div>
{#if save.error && !save.error.fields}<p role="alert">{save.error.message}</p>{/if}
<button disabled={save.busy}>{save.busy ? 'Saving…' : 'Save'}</button>
<button type="button" onclick={() => dialog?.close()}>Cancel</button>
</form>
{/if}
</dialog>src/lib/api/members.ts
export const updateMember = defineMutation({
patch: 'api/members/{id}',
body: type<Partial<MemberInput>>(),
result: type<Member>(),
invalidates: (member) => [tag('Member', member.id), 'Members'],
access: 'members.edit'
});How it works
| Moment | Request | Screen |
|---|---|---|
| Save | PATCH api/members/1 | save.busy: the button is disabled; a second click starts nothing |
| The server answers 422 | not retried | aria-invalid, the message, custom validity and focus on the first field;
editing the field clears its marks |
| The server accepts | invalidates: Member:1 and Members; the list on
screen refetches once, cause invalidate | onSuccess closes the dialog; the list shows refreshing |
| Lists not on screen | none: they are marked stale and refetch when shown | — |
fieldErrors()writes only attributes and custom validity, and removes exactly what it wrote; the messages are rendered by the template withissues().error.fieldsis typed by the mutation's body, soissues(save.error, 'name')names a real field.save.reset()clears the previous attempt when the dialog opens again.
Variations
Wait for the refreshed list
TypeScript
// Resolve run() only after the invalidated lists on screen have their new answer.
const create = createMutation(createMember, { settle: 'refetched' });Invalidate by hand
TypeScript
// After a write the definitions do not describe (a bulk import, a server job):
invalidate(['Members', tag('Member', 7)]);Related
- Writing: mutations, invalidation and optimistic updates.
- Forms:
fieldErrors,issues,errorKey. - Optimistic toggle