Added xState as flow engine

This commit is contained in:
Samuel Andert 2023-07-28 22:25:31 +02:00
parent e1fd73a7e8
commit bdc3445a67
8 changed files with 228 additions and 176 deletions

View File

@ -52,6 +52,8 @@
"@lit-protocol/types": "^2.2.41", "@lit-protocol/types": "^2.2.41",
"@sveltejs/vite-plugin-svelte": "^2.4.2", "@sveltejs/vite-plugin-svelte": "^2.4.2",
"@wagmi/core": "^1.3.8", "@wagmi/core": "^1.3.8",
"viem": "^1.3.0" "@xstate/svelte": "^2.1.0",
"viem": "^1.3.0",
"xstate": "^4.38.2"
} }
} }

View File

@ -31,9 +31,15 @@ dependencies:
'@wagmi/core': '@wagmi/core':
specifier: ^1.3.8 specifier: ^1.3.8
version: 1.3.8(react@18.2.0)(typescript@5.1.6)(viem@1.4.1)(zod@3.21.4) version: 1.3.8(react@18.2.0)(typescript@5.1.6)(viem@1.4.1)(zod@3.21.4)
'@xstate/svelte':
specifier: ^2.1.0
version: 2.1.0(svelte@4.1.1)(xstate@4.38.2)
viem: viem:
specifier: ^1.3.0 specifier: ^1.3.0
version: 1.4.1(typescript@5.1.6)(zod@3.21.4) version: 1.4.1(typescript@5.1.6)(zod@3.21.4)
xstate:
specifier: ^4.38.2
version: 4.38.2
devDependencies: devDependencies:
'@iconify/svelte': '@iconify/svelte':
@ -3260,6 +3266,22 @@ packages:
- react - react
dev: false dev: false
/@xstate/svelte@2.1.0(svelte@4.1.1)(xstate@4.38.2):
resolution: {integrity: sha512-cot553w2v4MdmDLkRBLhEjGO5LlnlPcpZ9RT7jFqpn+h0rpmjtkva6zjIZddPrxEOM6DVHDwzYbpDe+BErElQg==}
peerDependencies:
'@xstate/fsm': ^2.1.0
svelte: ^3.24.1 || ^4
xstate: ^4.38.1
peerDependenciesMeta:
'@xstate/fsm':
optional: true
xstate:
optional: true
dependencies:
svelte: 4.1.1
xstate: 4.38.2
dev: false
/JSONStream@1.3.5: /JSONStream@1.3.5:
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
hasBin: true hasBin: true
@ -7148,6 +7170,10 @@ packages:
utf-8-validate: 5.0.10 utf-8-validate: 5.0.10
dev: false dev: false
/xstate@4.38.2:
resolution: {integrity: sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==}
dev: false
/xstream@11.14.0: /xstream@11.14.0:
resolution: {integrity: sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==} resolution: {integrity: sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==}
dependencies: dependencies:

View File

@ -1,105 +1,200 @@
<script lang="ts"> <script>
import { Stepper, Step } from '@skeletonlabs/skeleton'; import { createMachine, assign } from 'xstate';
import { recipeStore } from '$lib/components/recipies/recipeStore'; import { useMachine } from '@xstate/svelte';
import { createMessage } from '$lib/services/messages/messages'; import { superForm } from 'sveltekit-superforms/client';
import { TreeSchema } from '$lib/types/TreeSchema';
import { writable, get } from 'svelte/store';
import { createUser } from './userService';
let name = ''; const initialFormData = { name: '', age: '' };
let email = '';
let lockedState = true;
function logOperation(text) { const { form, errors, validate, constraints, capture, restore } = superForm(initialFormData, {
const message = { validators: TreeSchema,
text: text, warnings: {
sender: 'user', noValidationAndConstraints: false
type: 'chat' },
}; validationMethod: 'oninput',
createMessage(message); clearOnSubmit: 'errors-and-message'
}
function operation1() {
if (name) {
recipeStore.update((state) => {
state.context.name = name;
state.step = 1;
return state;
}); });
logOperation('Name added: ' + name);
} else { export const snapshot = { capture, restore };
alert('Error: Please enter a name');
const isFormValid = writable(false); // Store to keep track of form validation status
async function handleSubmit() {
const validationResult = await validate();
if (!validationResult.valid) {
return;
} }
console.log(form);
isFormValid.set(true); // Set the form validation status to true after successful validation
} }
function operation2() { const stateMachine = createMachine(
if (email) { {
recipeStore.update((state) => { id: 'steps',
state.context.email = email; initial: 'start',
state.step = 2; context: {
return state; name: '',
}); email: ''
logOperation('Email added: ' + email); },
} else { states: {
alert('Error: Please enter an email'); start: {
on: { NEXT: 'nameInput' }
},
nameInput: {
on: {
NEXT: {
target: 'emailInput',
actions: ['setName']
},
BACK: 'start'
}
},
emailInput: {
on: {
NEXT: {
target: 'summary',
actions: ['setEmail']
},
BACK: 'nameInput'
}
},
summary: {
on: {
SUBMIT: 'submitting'
}
},
submitting: {
invoke: {
src: 'createUserService',
onDone: 'success',
onError: 'failure'
}
},
success: {},
failure: {}
}
},
{
actions: {
setName: assign({
name: (context, event) => $form.name
}),
setEmail: assign({
email: (context, event) => $form.email
})
},
services: {
createUserService: (context) => createUser(context.name, context.email)
} }
} }
);
function completeAndPrepareForRestart() { const { state, send } = useMachine(stateMachine);
logOperation('Recipe Completed. Preparing for restart...');
recipeStore.update((state) => {
state.step = 3;
return state;
});
}
function restartRecipe() { $: {
name = ''; // Reactively update the form validation status based on the errors
email = ''; isFormValid.set(Object.keys(get(errors)).length === 0);
recipeStore.set({
id: 'createUser',
step: 0,
context: {},
error: null
});
}
$: lockedState = ($recipeStore.step === 0 && !name) || ($recipeStore.step === 1 && !email);
function onStepHandler(e) {
if (e.detail.state.current === 1) {
operation1();
} else if (e.detail.state.current === 2) {
operation2();
} else if (e.detail.state.current === 3) {
completeAndPrepareForRestart();
}
} }
</script> </script>
<div class="flex items-center justify-center w-full h-full"> <main>
<Stepper on:step={onStepHandler} start={$recipeStore.step} class="w-full max-w-2xl"> {#if $state.value === 'start'}
<Step locked={lockedState}> <!-- Step 1 -->
<svelte:fragment slot="header">Enter Name</svelte:fragment>
<div> <div>
<label for="name">Name:</label> <h1 class="text-2xl">Step 1 - Start</h1>
<input type="text" class="text-black" bind:value={name} /> <button class="px-4 py-2 mt-4 text-white bg-blue-500 rounded" on:click={() => send('NEXT')}>
Next
</button>
</div> </div>
</Step> {:else if $state.value === 'nameInput'}
<Step locked={lockedState}> <!-- Step 2 -->
<svelte:fragment slot="header">Enter Email</svelte:fragment>
<div> <div>
<label for="email">Email:</label> <h1 class="text-2xl">Step 2 - Name Input</h1>
<input type="text" class="text-black" bind:value={email} /> <form on:submit|preventDefault={handleSubmit} class="w-full max-w-md">
<div class="mb-4">
{#if $errors.name}
<span class="block mb-2 font-semibold text-red-500">{$errors.name}</span>
{:else}
<label for="name" class="block mb-2 font-semibold text-white">Name</label>
{/if}
<input
name="name"
type="text"
class="w-full px-3 py-2 bg-transparent border-gray-100 rounded-md border-1 ring-0 ring-white focus:outline-none focus:ring-2 focus:ring-blue-500"
bind:value={$form.name}
aria-invalid={$errors.name ? 'true' : undefined}
{...constraints.name}
/>
</div> </div>
</Step> <button
<Step> type="submit"
<svelte:fragment slot="header">Completed</svelte:fragment> class="w-full px-4 py-2 mt-4 text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
<h2>Recipe Completed</h2> disabled={$errors.name}
<p>Your Name: {$recipeStore.context.name}</p> on:click={() => send('NEXT')}
<p>Your Email: {$recipeStore.context.email}</p> >
</Step> Next
<Step> </button>
<svelte:fragment slot="header">Restart</svelte:fragment> </form>
<h2>Ready to start over?</h2> </div>
<button on:click={restartRecipe}>Restart</button> {:else if $state.value === 'emailInput'}
</Step> <div>
</Stepper> <h1 class="text-2xl">Step 3 - Email Input</h1>
</div> <form on:submit|preventDefault={handleSubmit} class="w-full max-w-md">
<div class="mb-4">
{#if $errors.email}
<span class="block mb-2 font-semibold text-red-500">{$errors.email}</span>
{:else}
<label for="email" class="block mb-2 font-semibold text-white">Email</label>
{/if}
<input
name="email"
type="email"
class="w-full px-3 py-2 bg-transparent border-gray-100 rounded-md border-1 ring-0 ring-white focus:outline-none focus:ring-2 focus:ring-blue-500"
bind:value={$form.email}
aria-invalid={$errors.email ? 'true' : undefined}
{...constraints.email}
/>
</div>
<button
type="submit"
class="w-full px-4 py-2 mt-4 text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={$errors.email}
on:click={() => send('NEXT')}
>
Next
</button>
</form>
</div>
<!-- Add a Summary Section -->
{:else if $state.value === 'summary'}
<div>
<h1 class="text-2xl">Summary</h1>
<p>Name: {$form.name}</p>
<p>Email: {$form.email}</p>
<button class="px-4 py-2 mt-4 text-white bg-blue-500 rounded" on:click={() => send('SUBMIT')}>
Submit
</button>
</div>
{:else if $state.value === 'submitting'}
<div>
<h1 class="text-2xl">Submitting...</h1>
</div>
{:else if $state.value === 'success'}
<div>
<h1 class="text-2xl">User created successfully!</h1>
<!-- You can add a button to reset the form or navigate to another page -->
</div>
{:else if $state.value === 'failure'}
<div>
<h1 class="text-2xl">Failed to create user. Please try again.</h1>
<button class="px-4 py-2 mt-4 text-white bg-red-500 rounded" on:click={() => send('SUBMIT')}>
Retry
</button>
</div>
{/if}
</main>

View File

@ -1,68 +0,0 @@
<script lang="ts">
import { superForm } from 'sveltekit-superforms/client';
import { TreeSchema } from '$lib/types/treeSchema'; // Import the TreeType schema
import { afterUpdate } from 'svelte';
import { writable } from 'svelte/store';
const initialFormData = { name: '', age: '' }; // Replace email with age
const { form, errors, validate, constraints, capture, restore } = superForm(initialFormData, {
validators: TreeSchema, // Use TreeSchema
warnings: {
noValidationAndConstraints: false
},
validationMethod: 'oninput',
clearOnSubmit: 'errors-and-message'
});
export const snapshot = { capture, restore };
const successMessage = writable<string | null>(null);
async function handleSubmit() {
const validationResult = await validate();
if (!validationResult.valid) {
return;
}
console.log(form);
successMessage.set('Form submitted successfully!');
}
function handleReset() {
form.set({});
successMessage.set(null);
}
afterUpdate(() => {
const messageContainer = document.getElementById('message-container');
if (messageContainer) {
messageContainer.scrollIntoView({ behavior: 'smooth' });
}
});
</script>
{#if $successMessage}
<!-- Success message code remains the same -->
{:else}
<form on:submit|preventDefault={handleSubmit} class="w-full max-w-md">
<!-- Name field remains the same -->
<div class="mb-4">
{#if $errors.age}
<span class="block mb-2 font-semibold text-red-500">{$errors.age}</span>
{:else}
<label for="age" class="block mb-2 font-semibold text-white">Age</label>
{/if}
<input
name="age"
type="number"
class="w-full px-3 py-2 bg-transparent border-gray-100 rounded-md border-1 ring-0 ring-white focus:outline-none focus:ring-2 focus:ring-blue-500"
bind:value={$form.age}
aria-invalid={$errors.age ? 'true' : undefined}
{...constraints.age}
/>
</div>
<!-- Submit button code remains the same -->
</form>
{/if}

View File

@ -1,8 +0,0 @@
import { writable } from 'svelte/store';
export const recipeStore = writable({
id: 'createUser',
step: 0,
context: {},
error: null
});

View File

@ -1,18 +1,19 @@
import { z } from 'zod'; import { z } from 'zod';
// Define custom validation messages
const validationMessages = { const validationMessages = {
name: { name: {
minLength: 'Name must be at least 3 characters.', minLength: 'Name must be at least 3 characters.',
maxLength: 'Name must contain at most 50 characters.', maxLength: 'Name must contain at most 50 characters.',
}, },
age: { email: {
min: 'Age must be at least 0 years.', invalid: 'Please provide a valid email address.',
max: 'Age must be at most 5000 years.'
} }
}; };
export const TreeSchema = z.object({ export const TreeSchema = z.object({
name: z.string().min(3, validationMessages.name.minLength).max(50, validationMessages.name.maxLength), name: z.string()
age: z.number().min(0, validationMessages.age.min).max(5000, validationMessages.age.max) .min(3, validationMessages.name.minLength)
.max(50, validationMessages.name.maxLength),
email: z.string()
.email(validationMessages.email.invalid)
}); });

View File

@ -5,6 +5,10 @@ const config = {
kit: { kit: {
adapter: adapter() adapter: adapter()
}, },
ssr: {
// Other SSR options...
dynamicImportShim: false
},
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
}; };
export default config; export default config;