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",
"@sveltejs/vite-plugin-svelte": "^2.4.2",
"@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':
specifier: ^1.3.8
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:
specifier: ^1.3.0
version: 1.4.1(typescript@5.1.6)(zod@3.21.4)
xstate:
specifier: ^4.38.2
version: 4.38.2
devDependencies:
'@iconify/svelte':
@ -3260,6 +3266,22 @@ packages:
- react
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:
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
hasBin: true
@ -7148,6 +7170,10 @@ packages:
utf-8-validate: 5.0.10
dev: false
/xstate@4.38.2:
resolution: {integrity: sha512-Fba/DwEPDLneHT3tbJ9F3zafbQXszOlyCJyQqqdzmtlY/cwE2th462KK48yaANf98jHlP6lJvxfNtN0LFKXPQg==}
dev: false
/xstream@11.14.0:
resolution: {integrity: sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==}
dependencies:

View File

@ -1,105 +1,200 @@
<script lang="ts">
import { Stepper, Step } from '@skeletonlabs/skeleton';
import { recipeStore } from '$lib/components/recipies/recipeStore';
import { createMessage } from '$lib/services/messages/messages';
<script>
import { createMachine, assign } from 'xstate';
import { useMachine } from '@xstate/svelte';
import { superForm } from 'sveltekit-superforms/client';
import { TreeSchema } from '$lib/types/TreeSchema';
import { writable, get } from 'svelte/store';
import { createUser } from './userService';
let name = '';
let email = '';
let lockedState = true;
const initialFormData = { name: '', age: '' };
function logOperation(text) {
const message = {
text: text,
sender: 'user',
type: 'chat'
};
createMessage(message);
}
const { form, errors, validate, constraints, capture, restore } = superForm(initialFormData, {
validators: TreeSchema,
warnings: {
noValidationAndConstraints: false
},
validationMethod: 'oninput',
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 {
alert('Error: Please enter a name');
export const snapshot = { capture, restore };
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() {
if (email) {
recipeStore.update((state) => {
state.context.email = email;
state.step = 2;
return state;
});
logOperation('Email added: ' + email);
} else {
alert('Error: Please enter an email');
const stateMachine = createMachine(
{
id: 'steps',
initial: 'start',
context: {
name: '',
email: ''
},
states: {
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() {
logOperation('Recipe Completed. Preparing for restart...');
recipeStore.update((state) => {
state.step = 3;
return state;
});
}
const { state, send } = useMachine(stateMachine);
function restartRecipe() {
name = '';
email = '';
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();
}
$: {
// Reactively update the form validation status based on the errors
isFormValid.set(Object.keys(get(errors)).length === 0);
}
</script>
<div class="flex items-center justify-center w-full h-full">
<Stepper on:step={onStepHandler} start={$recipeStore.step} class="w-full max-w-2xl">
<Step locked={lockedState}>
<svelte:fragment slot="header">Enter Name</svelte:fragment>
<div>
<label for="name">Name:</label>
<input type="text" class="text-black" bind:value={name} />
</div>
</Step>
<Step locked={lockedState}>
<svelte:fragment slot="header">Enter Email</svelte:fragment>
<div>
<label for="email">Email:</label>
<input type="text" class="text-black" bind:value={email} />
</div>
</Step>
<Step>
<svelte:fragment slot="header">Completed</svelte:fragment>
<h2>Recipe Completed</h2>
<p>Your Name: {$recipeStore.context.name}</p>
<p>Your Email: {$recipeStore.context.email}</p>
</Step>
<Step>
<svelte:fragment slot="header">Restart</svelte:fragment>
<h2>Ready to start over?</h2>
<button on:click={restartRecipe}>Restart</button>
</Step>
</Stepper>
</div>
<main>
{#if $state.value === 'start'}
<!-- Step 1 -->
<div>
<h1 class="text-2xl">Step 1 - Start</h1>
<button class="px-4 py-2 mt-4 text-white bg-blue-500 rounded" on:click={() => send('NEXT')}>
Next
</button>
</div>
{:else if $state.value === 'nameInput'}
<!-- Step 2 -->
<div>
<h1 class="text-2xl">Step 2 - Name Input</h1>
<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>
<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.name}
on:click={() => send('NEXT')}
>
Next
</button>
</form>
</div>
{:else if $state.value === 'emailInput'}
<div>
<h1 class="text-2xl">Step 3 - Email Input</h1>
<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';
// Define custom validation messages
const validationMessages = {
name: {
minLength: 'Name must be at least 3 characters.',
maxLength: 'Name must contain at most 50 characters.',
},
age: {
min: 'Age must be at least 0 years.',
max: 'Age must be at most 5000 years.'
email: {
invalid: 'Please provide a valid email address.',
}
};
export const TreeSchema = z.object({
name: z.string().min(3, validationMessages.name.minLength).max(50, validationMessages.name.maxLength),
age: z.number().min(0, validationMessages.age.min).max(5000, validationMessages.age.max)
name: z.string()
.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: {
adapter: adapter()
},
ssr: {
// Other SSR options...
dynamicImportShim: false
},
preprocess: vitePreprocess(),
};
export default config;