# Save and refresh the list

> A form in a dialog that saves a member, shows field errors, closes on success and refreshes exactly the lists the save touched.

Page: https://svelte-lean.vercel.app/query/recipes/save-and-refresh · Markdown: https://svelte-lean.vercel.app/query/recipes/save-and-refresh.md · Index: https://svelte-lean.vercel.app/llms.txt

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.

## Example

Edit a member and save. Then try an email outside `harbor.studio`: the server answers with a field error.

```svelte
<!-- 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>
```

The component shown, against an in-memory server that validates the email domain and answers after a fixed delay.

`src/lib/api/members.ts`

```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 with `issues()`.
- `error.fields` is typed by the mutation's body, so `issues(save.error, 'name')` names a real field.
- `save.reset()` clears the previous attempt when the dialog opens again.

## Variations

### Wait for the refreshed list

```ts
// Resolve run() only after the invalidated lists on screen have their new answer.
const create = createMutation(createMember, { settle: 'refetched' });
```

### Invalidate by hand

```ts
// After a write the definitions do not describe (a bulk import, a server job):
invalidate(['Members', tag('Member', 7)]);
```

## Related

- [Writing](https://svelte-lean.vercel.app/query/writing.md): mutations, invalidation and optimistic updates.
- [Forms](https://svelte-lean.vercel.app/query/forms.md): `fieldErrors`, `issues`, `errorKey`.
- [Optimistic toggle](https://svelte-lean.vercel.app/query/recipes/optimistic-toggle.md)
