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

@ -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
});