Added Basic xstate store management POC

This commit is contained in:
Samuel Andert 2023-08-01 11:58:34 +02:00
parent 3d2960390a
commit 125d7d997e
10 changed files with 224 additions and 113 deletions

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { fetchBalance } from '@wagmi/core';
import { onMount } from 'svelte';

View File

@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { connectWallet } from '$lib/services/wallet/wallet';
import WalletConnect from '$lib/WalletConnect.svelte';
import Send from '$lib/Send.svelte';
// export let id;
export let store;
@ -21,6 +22,7 @@
<div class="mb-4 text-lg font-medium">
PKP Wallet: <span class="text-blue-600">{$store.pkpWallet.address}</span>
</div>
<Send pkpWallet={$store.pkpWallet} />
{/if}
<WalletConnect />
</div>

View File

@ -0,0 +1,14 @@
<!-- src/lib/components/CheckValidation.svelte -->
<script lang="ts">
export let store;
$: console.log('checkvalidation: ' + $store.isValidated);
</script>
<div class="flex items-center justify-center w-12 h-12 m-10 rounded-full">
{#if $store.isValidated}
<div class="w-full h-full bg-green-500 rounded-full" />
{:else}
<div class="w-full h-full bg-red-500 rounded-full" />
{/if}
</div>

View File

@ -1,10 +1,16 @@
<script lang="ts">
import { superForm, message } from 'sveltekit-superforms/client';
import { superForm } from 'sveltekit-superforms/client';
import { UserSchema } from '$lib/types/UserSchema';
import { afterUpdate } from 'svelte';
import { writable } from 'svelte/store';
import { RangeSlider, SlideToggle } from '@skeletonlabs/skeleton';
export let store;
export let services;
let isStoreLoaded = false;
$: if ($store) isStoreLoaded = true;
const initialFormData = {
name: '',
email: '',
@ -17,19 +23,42 @@
const fields = ['name', 'email', 'about', 'age', 'favoriteFood', 'slider', 'toggle'];
const { form, errors, validate, constraints, capture, restore } = superForm(initialFormData, {
const { form, errors, validate, constraints } = superForm(initialFormData, {
validators: UserSchema,
warnings: {
noValidationAndConstraints: false
},
validationMethod: 'oninput', // Trigger validation on input events
// Set the clearOnSubmit option to clear both errors and message on submit
clearOnSubmit: 'errors-and-message'
});
export const snapshot = { capture, restore };
const successMessage = writable<string | null>(null);
// Update the isValidated property of the store whenever the errors object changes
$: {
$store.isValidated = !(
$errors.name ||
$errors.email ||
$errors.about ||
$errors.age ||
$errors.favoriteFood
);
}
$: {
if (
!$errors.name &&
!$errors.email &&
!$errors.about &&
!$errors.age &&
!$errors.favoriteFood
) {
services.validationRecipe.validateMe().send('VALIDATE');
} else {
services.validationRecipe.validateMe().send('INVALIDATE');
}
}
async function handleSubmit() {
// Manually validate the form
const validationResult = await validate();
@ -61,103 +90,110 @@
});
</script>
<div class="flex items-center justify-center min-h-screen overflow-scroll">
{#if $successMessage}
<!-- Display the success message instead of the form -->
<aside class="w-full max-w-md p-4 alert variant-ghost" id="message-container">
<!-- Icon -->
<!-- <div>(icon)</div> -->
<!-- Message -->
<div class="alert-message">
<h3 class="h3">Success</h3>
<p>{$successMessage}</p>
</div>
<!-- Actions (in this case, only the Reset button) -->
<div class="alert-actions">
<button
class="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"
on:click={handleReset}
>
Reset
</button>
</div>
</aside>
{:else}
<form on:submit|preventDefault={handleSubmit} class="w-full max-w-md">
{#each fields as field}
<div class="mb-4">
{#if $errors[field]}
<span class="block mb-2 font-semibold text-red-500">{$errors[field]}</span>
{:else}
<label for={field} class="block mb-2 font-semibold text-white"
>{field.charAt(0).toUpperCase() + field.slice(1)}</label
{#if isStoreLoaded}
<div class="flex items-center justify-center min-h-screen overflow-scroll">
<div class="w-full">
STORE: {JSON.stringify($store)}
{#if $successMessage}
<!-- Display the success message instead of the form -->
<aside class="w-full max-w-md p-4 alert variant-ghost" id="message-container">
<!-- Icon -->
<!-- <div>(icon)</div> -->
<!-- Message -->
<div class="alert-message">
<h3 class="h3">Success</h3>
<p>{$successMessage}</p>
</div>
<!-- Actions (in this case, only the Reset button) -->
<div class="alert-actions">
<button
class="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"
on:click={handleReset}
>
{/if}
Reset
</button>
</div>
</aside>
{:else}
<form on:submit|preventDefault={handleSubmit} class="w-full max-w-md">
{#each fields as field}
<div class="mb-4">
{#if $errors[field]}
<span class="block mb-2 font-semibold text-red-500">{$errors[field]}</span>
{:else}
<label for={field} class="block mb-2 font-semibold text-white"
>{field.charAt(0).toUpperCase() + field.slice(1)}
</label>
{/if}
{#if field === 'about'}
<textarea
name={field}
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[field]}
aria-invalid={$errors[field] ? 'true' : undefined}
{...constraints[field]}
/>
{:else if field === 'favoriteFood'}
<select
name={field}
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[field]}
aria-invalid={$errors[field] ? 'true' : undefined}
{...constraints[field]}
>
<option value="">Select...</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="coconut">Coconut</option>
<option value="strawberry">Strawberry</option>
<option value="mango">Mango</option>
</select>
{:else if field === 'age'}
<input
name={field}
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[field]}
aria-invalid={$errors[field] ? 'true' : undefined}
{...constraints[field]}
/>
{:else if field === 'slider'}
<RangeSlider name={field} bind:value={$form[field]} min={0} max={100} step={1} ticked>
<div class="flex items-center justify-between">
<div class="text-xs">{$form[field]} / 100</div>
</div>
</RangeSlider>
{:else if field === 'toggle'}
<SlideToggle name={field} bind:checked={$form[field]} />
{:else}
<input
name={field}
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[field]}
aria-invalid={$errors[field] ? 'true' : undefined}
{...constraints[field]}
/>
{/if}
</div>
{/each}
<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 ||
$errors.email ||
$errors.about ||
$errors.age ||
$errors.favoriteFood}
>
Submit
</button>
</form>
{/if}
</div>
{#if field === 'about'}
<textarea
name={field}
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[field]}
aria-invalid={$errors[field] ? 'true' : undefined}
{...constraints[field]}
/>
{:else if field === 'favoriteFood'}
<select
name={field}
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[field]}
aria-invalid={$errors[field] ? 'true' : undefined}
{...constraints[field]}
>
<option value="">Select...</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="coconut">Coconut</option>
<option value="strawberry">Strawberry</option>
<option value="mango">Mango</option>
</select>
{:else if field === 'age'}
<input
name={field}
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[field]}
aria-invalid={$errors[field] ? 'true' : undefined}
{...constraints[field]}
/>
{:else if field === 'slider'}
<RangeSlider
name={field}
bind:value={$form[field]}
min={0}
max={100}
step={1}
ticked
>
<div class="flex items-center justify-between">
<div class="text-xs">{$form[field]} / 100</div>
</div>
</RangeSlider>
{:else if field === 'toggle'}
<SlideToggle name={field} bind:checked={$form[field]} />
{:else}
<input
name={field}
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[field]}
aria-invalid={$errors[field] ? 'true' : undefined}
{...constraints[field]}
/>
{/if}
</div>
{/each}
<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={!$store.isValidated}
>
Submit
</button>
</form>
{/if}
</div>
</div>
{/if}

View File

@ -1,8 +0,0 @@
/src/lib/components/examples/Form.svelte:98:7 'type' attribute cannot be dynamic if input uses two-way binding
/src/lib/components/examples/Form.svelte:98:7
96 | <input
97 | name={field}
98 | type={field === 'age' ? 'number' : 'text'}
^
99 | 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"
100 | bind:value={$form[field]}

View File

@ -0,0 +1 @@
{"core":{},"composite.Interpreter2":{"id":"validation"},"helloEarthAlert":{}}

View File

@ -0,0 +1,27 @@
import { createMachine, interpret } from 'xstate';
const validationMachine = createMachine({
id: 'validation',
initial: 'notValidated',
states: {
notValidated: {
on: {
VALIDATE: 'validated'
}
},
validated: {
on: {
INVALIDATE: 'notValidated'
}
}
}
});
export function validateMe() {
const service = interpret(validationMachine).onTransition((state) => {
console.log('Current validation state:', state.value);
}).start();
return service;
}

View File

@ -29,7 +29,7 @@ const validationMessages = {
};
export const UserSchema = z.object({
name: z.string().min(3, validationMessages.name.minLength).max(10, validationMessages.name.maxLength),
name: z.string().nonempty('Name is required.').min(3, validationMessages.name.minLength).max(10, validationMessages.name.maxLength),
email: z.string().email(validationMessages.email.isEmail),
about: z.string().max(500, validationMessages.about.maxLength),
age: z.number().min(18, validationMessages.age.min).max(120, validationMessages.age.max),

View File

@ -0,0 +1,39 @@
<!-- src/routes/form/+page.svelte -->
<script lang="ts">
import Composite from '$lib/core/Composite.svelte';
let composite = {
id: 'testform',
store: {
isValidated: false
},
layout: {
areas: `
"checkvalidation form"
`,
columns: '1fr 3fr',
rows: 'auto'
},
children: [
{
id: 'checkvalidation',
component: 'CheckValidation',
slot: 'checkvalidation',
map: {
isValidated: '@testform:isValidated'
}
},
{
id: 'form',
component: 'Form',
slot: 'form',
map: {
isValidated: '@testform:isValidated'
},
services: ['validationRecipe', 'helloEarthAlert']
}
]
};
</script>
<Composite {composite} />

View File

@ -6,7 +6,7 @@
store: {
title: 'Hello Earth',
description:
'Here you can find all the references, how to use the store and mapping logic of store to store and data to store maps',
'how to use the store and mapping logic of store to store and data to store maps',
helloMapMe: 'this is going to be mapped'
},
layout: {