Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[@svelteui/core] form management #428

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
4 changes: 3 additions & 1 deletion packages/svelteui-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@
},
"dependencies": {
"@floating-ui/dom": "1.2.8",
"@stitches/core": "1.2.8"
"@stitches/core": "1.2.8",
"fast-deep-equal": "^3.1.3",
"klona": "^2.0.6"
},
"devDependencies": {
"@babel/core": "7.21.8",
Expand Down
111 changes: 111 additions & 0 deletions packages/svelteui-core/src/components/Form/Blah.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script>
import { Trash } from 'radix-icons-svelte';
import { Button } from '../Button';
import Center from '../Center/Center.svelte';
import Checkbox from '../Checkbox/Checkbox.svelte';
import Flex from '../Flex/Flex.svelte';
import Stack from '../Stack/Stack.svelte';
import { TextInput } from '../TextInput';
import Field from './Field.svelte';
import Form from './Form.svelte';
import { hasLength, isNotEmpty } from './utils';
</script>

<Form
initialValues={{
user: { firstName: 'Super', lastName: '' },
pokemon: [
{ name: 'Mantine', type: 'water' },
{ name: 'Monferno', type: 'fire' },
{ name: 'Magicarp', type: 'water' }
],
agree: false
}}
validation={{
user: {
firstName: isNotEmpty('First name is required'),
lastName: hasLength({ min: 3 }, 'Value must have 3 or more characters')
},
pokemon: { name: isNotEmpty('Name is required') },
agree: (v) => (v ? undefined : 'You must agree to the terms we know you didnt read')
}}
validateInputOnChange
validateInputOnBlur
let:form
>
<form
on:submit={form.onSubmit((values) => {
console.log({ values });
})}
on:reset={form.onReset}
>
<Center>
<Stack>
<Field {form} name="user.firstName" let:field let:htmlInputProps>
<label>
<div>First name (html input)</div>
<input
{...htmlInputProps}
aria-describedby={field.error ? 'first-name-error' : undefined}
on:input={field.onChange}
/>
</label>
{#if field.error}
<div id="first-name-error">{field.error}</div>
{/if}
</Field>
<Field {form} name="user.lastName" let:field let:inputProps>
<TextInput label="Last name (svelteui input)" {...inputProps} on:input={field.onChange} />
</Field>
<h2>Pokemon</h2>
{#each form.values.pokemon as _pokemon, i}
<Flex gap="$md" align="end">
<Field {form} name={`pokemon.${i}.name`} let:field let:inputProps>
<TextInput label="Name" {...inputProps} on:input={field.onChange} />
</Field>
<Field {form} name={`pokemon.${i}.type`} let:field let:inputProps>
<TextInput label="Type" {...inputProps} on:input={field.onChange} />
</Field>
<Button
on:click={() => form.removeListItem('pokemon', i)}
type="button"
variant="default"><Trash /></Button
>
</Flex>
{/each}
<Flex gap="$md">
<Button
on:click={() => form.insertListItem('pokemon', { name: '', type: '' })}
type="button"
>
Add Pokemon
</Button>
<Button
disabled={!form.values.pokemon.length}
on:click={() =>
form.reorderListItem('pokemon', { from: form.values.pokemon.length - 1, to: 0 })}
type="button"
>
Move Last Item To The Top
</Button>
</Flex>
<Field {form} name="agree" isCheckbox let:field let:inputProps>
<Checkbox
aria-describedby={field.error ? 'agree-error' : undefined}
label="I agree to the conditions i didnt read"
{...inputProps}
on:input={field.onChange}
/>
<!-- Checkbox doesn't support errors yet -->
{#if field.error}
<div id="agree-error">{field.error}</div>
{/if}
</Field>
<Flex gap="$md">
<Button type="reset" variant="outline">Reset</Button>
<Button type="submit">Submit</Button>
</Flex>
</Stack>
</Center>
</form>
</Form>
30 changes: 30 additions & 0 deletions packages/svelteui-core/src/components/Form/Field.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import type { FieldProps as $$FieldProps, GetInputPropsReturnType } from './Form';

interface $$Props extends $$FieldProps {}

export let form: $$Props['form'] = undefined,
name: $$Props['name'] = undefined,
isCheckbox: $$Props['isCheckbox'] = false;

let fieldProps: GetInputPropsReturnType;

$: fieldProps = form.getInputProps(name, { type: isCheckbox ? 'checkbox' : 'input' });
$: field = {
...fieldProps,
onValueChange: fieldProps.onChange,
onChange: (e) => fieldProps.onChange(isCheckbox ? e.target.checked : e.target.value)
};
$: inputProps = {
error: field.error,
value: !isCheckbox ? field.value : undefined,
checked: isCheckbox ? field.checked : undefined
};
$: htmlInputProps = {
value: !isCheckbox ? field.value : undefined,
checked: isCheckbox ? field.checked : undefined,
'aria-invalid': !!field.error || undefined
};
</script>

<slot {field} {inputProps} {htmlInputProps} />
25 changes: 25 additions & 0 deletions packages/svelteui-core/src/components/Form/Form.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { UseFormReturnType } from './src';
import type { UseFormInput } from './utils/types';

export type Values = Record<string, any>;

export interface FormProps extends Omit<UseFormInput<Values>, 'validate'> {
onSubmit?(values: TransformedValues<Form>): void;
onReset?(values: TransformedValues<Form>): void;
validation?: Record<string, any>;
}

export interface FieldProps {
form: UseFormReturnType<Values>;
name: string;
isCheckbox?: boolean;
}

export interface GetInputPropsReturnType {
value: any;
onChange: any;
checked?: any;
error?: any;
onFocus?: any;
onBlur?: any;
}
12 changes: 12 additions & 0 deletions packages/svelteui-core/src/components/Form/Form.stories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import { Meta, Template, Story } from '@storybook/addon-svelte-csf';
import Blah from './Blah.svelte';
</script>

<Meta title="Components/Form" component={Blah} />

<Template let:args>
<Blah {...args} />
</Template>

<Story name="Blah" id="blahStory" />
Loading