Skip to content

Commit

Permalink
Merge pull request #24 from x0k/async-action
Browse files Browse the repository at this point in the history
Add `useMutation` API
  • Loading branch information
x0k authored Nov 4, 2024
2 parents a786e8f + 52ca9bf commit ec499b7
Show file tree
Hide file tree
Showing 12 changed files with 433 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-terms-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"docs": minor
---

Add generic backend integration guide
5 changes: 5 additions & 0 deletions .changeset/yellow-horses-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sjsf/form": minor
---

Add `useAction` API
4 changes: 4 additions & 0 deletions apps/docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export default defineConfig({
label: "Themes",
autogenerate: { directory: "themes" },
},
{
label: "Integrations",
autogenerate: { directory: "integrations" },
},
{
label: "Advanced scenarios",
autogenerate: { directory: "advanced" },
Expand Down
8 changes: 4 additions & 4 deletions apps/docs/src/content/docs/guides/programmatic-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ sidebar:

import { Code, Card } from '@astrojs/starlight/components';

import BindForm from './_programmatic-control.svelte';
import bindFormCode from './_programmatic-control.svelte?raw';
import Form from './_programmatic-control.svelte';
import formCode from './_programmatic-control.svelte?raw';

You can bind to the from element to control it programmatically:

<Code code={bindFormCode} lang="svelte" />
<Code code={formCode} lang="svelte" />

<Card>
<BindForm client:only="svelte" />
<Form client:only="svelte" />
</Card>
62 changes: 62 additions & 0 deletions apps/docs/src/content/docs/integrations/_generic-backend.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script lang="ts">
import { FormContent, SubmitButton } from "@sjsf/form";
import { Status, useMutation } from "@sjsf/form/use-mutation.svelte";
import { useCustomForm } from "@/components/custom-form";
let isError = $state(false);
let duration = $state(0);
let data = $state<string>();
const mutation = useMutation({
mutate: (_signal, value: string | undefined = "") =>
new Promise<string>((resolve, reject) => {
data = undefined;
isError = Math.random() > 0.5;
duration = Math.random() * 5000;
setTimeout(() => {
if (isError) {
reject(value);
} else {
resolve(value);
}
}, duration);
}),
onSuccess(response) {
data = response;
form.reset();
},
onFailure: console.error,
delayedMs: 1000,
timeoutMs: 3000,
});
const form = useCustomForm({
schema: {
type: "string",
},
onSubmit: mutation.run,
get disabled() {
return mutation.isProcessed;
},
});
</script>

<p>
Reject: {isError}, delay: {(duration / 1000).toFixed(2)}sec
</p>

<form use:form.enhance style="display: flex; flex-direction: column; gap: 1rem">
<FormContent bind:value={form.formValue} />
{#if mutation.isDelayed}
<button style="padding: 0.5rem;" disabled>Processed...</button>
{:else}
<SubmitButton />
{/if}
{#if data !== undefined}
<p>Data: {data}</p>
{/if}
{#if mutation.state.status === Status.Failed}
<p class="text-red-500">Failed: {mutation.state.reason}</p>
{/if}
</form>
18 changes: 18 additions & 0 deletions apps/docs/src/content/docs/integrations/generic-backend.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Generic backend
sidebar:
order: 0
---

import { Card, Code } from '@astrojs/starlight/components'

import Form from './_generic-backend.svelte';
import formCode from './_generic-backend.svelte?raw';

You can improve the experience of sending data to the server by using the `useAction` hook.

<Code code={formCode} lang="svelte" />

<Card>
<Form client:only="svelte" />
</Card>
2 changes: 1 addition & 1 deletion mkfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ f/:
c:
pnpm run check
t:
pnpm run test
pnpm run test $@
popd

ds/:
Expand Down
5 changes: 5 additions & 0 deletions packages/form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@types/json-schema": "^7.0.15",
"@types/json-schema-merge-allof": "^0.6.5",
"deep-freeze-es6": "^3.0.2",
"svelte": "catalog:",
"svelte-check": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
Expand Down Expand Up @@ -88,6 +89,10 @@
"./get-default-form-state": {
"types": "./dist/get-default-form-state.d.ts",
"default": "./dist/get-default-form-state.js"
},
"./use-mutation.svelte": {
"types": "./dist/use-mutation.svelte.d.ts",
"svelte": "./dist/use-mutation.svelte.js"
}
}
}
134 changes: 134 additions & 0 deletions packages/form/src/use-mutation.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { tick } from 'svelte';

import {
abortPrevious,
forgetPrevious,
ignoreNewUntilPreviousIsFinished,
Status,
useMutation,
} from "./use-mutation.svelte.js";

describe("useMutation", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Status", () => {
it("Should correctly update status during success flow", async () => {
const mutation = useMutation({
mutate: () => Promise.resolve(),
delayedMs: 50,
timeoutMs: 100,
});
const promise = mutation.run(undefined);
expect(mutation.status).toBe(Status.Processed);
await promise;
expect(mutation.status).toBe(Status.Success);
vi.advanceTimersByTime(50);
expect(mutation.status).toBe(Status.Success);
vi.advanceTimersByTime(50);
expect(mutation.status).toBe(Status.Success);
});
it("Should correctly update status during error flow", async () => {
const mutation = useMutation({
mutate: () => Promise.reject(),
delayedMs: 50,
timeoutMs: 100,
});
const promise = mutation.run(undefined);
expect(mutation.status).toBe(Status.Processed);
await promise;
expect(mutation.status).toBe(Status.Failed);
vi.advanceTimersByTime(50);
expect(mutation.status).toBe(Status.Failed);
vi.advanceTimersByTime(50);
expect(mutation.status).toBe(Status.Failed);
});
it("Should correctly update statuses: processed -> processed(delayed) -> failed(timeout)", async () => {
const mutation = useMutation({
mutate: () =>
new Promise((resolve) => {
setTimeout(() => {
resolve(0);
}, 100);
}),
delayedMs: 10,
timeoutMs: 50,
});
mutation.run(undefined);
expect(mutation.status).toBe(Status.Processed);
expect(mutation.isDelayed).toBe(false);
vi.advanceTimersByTime(10);
expect(mutation.status).toBe(Status.Processed);
expect(mutation.isDelayed).toBe(true);
vi.advanceTimersByTime(40);
if (mutation.state.status === Status.Failed) {
expect(mutation.state.reason).toBe("timeout");
} else {
expect.fail()
}
vi.advanceTimersByTime(50);
await tick();
expect(mutation.status).toBe(Status.Failed);
});
});
describe("Combinator", () => {
it("Should ignore new mutation until the previous mutation is completed", async () => {
const impl = vi.fn(
() =>
new Promise((resolve) => {
setTimeout(() => {
resolve(0);
}, 100);
})
);
const mutation = useMutation({
mutate: impl,
combinator: ignoreNewUntilPreviousIsFinished,
});
mutation.run(undefined);
mutation.run(undefined);
vi.advanceTimersByTime(100);
await tick();
mutation.run(undefined);
expect(impl).toBeCalledTimes(2);
});
it("Should forget previous mutation with 'forgetPrevious' combinator", async () => {
let count = 0;
const onSuccess = vi.fn();
const mutation = useMutation({
mutate: () => Promise.resolve(count++),
combinator: forgetPrevious,
onSuccess,
});
mutation.run(undefined);
await mutation.run(undefined);
expect(onSuccess).toBeCalledTimes(1);
expect(onSuccess).toBeCalledWith(1);
});
it("Should abort previous mutation with 'abortPrevious' combinator", async () => {
const onAbort = vi.fn();
let count = 0;
const impl = vi.fn((signal: AbortSignal) => {
signal.addEventListener("abort", onAbort);
return Promise.resolve(count++);
});
const onSuccess = vi.fn();
const mutation = useMutation({
mutate: impl,
combinator: abortPrevious,
onSuccess,
});
mutation.run(undefined);
mutation.run(undefined);
await mutation.run(undefined);
expect(onAbort).toBeCalledTimes(2);
expect(impl).toBeCalledTimes(3);
expect(onSuccess).toBeCalledTimes(1);
expect(onSuccess).toBeCalledWith(2);
});
});
});
Loading

0 comments on commit ec499b7

Please sign in to comment.