Skip to content

Commit d2515e4

Browse files
authored
Merge pull request #1 from nickt26/feature/useArrayField
0.2 Release
2 parents d587e96 + db6e06c commit d2515e4

37 files changed

+2108
-1046
lines changed

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"useTabs": true,
33
"singleQuote": true,
4-
"trailingComma": "none",
4+
"trailingComma": "all",
55
"printWidth": 120,
66
"plugins": ["prettier-plugin-svelte"],
77
"pluginSearchDirs": ["."],

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2023 Nicholas Trummer
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ npm install svelte-headless-form
1313
```html
1414
<script>
1515
import { createForm } from 'svelte-headless-form';
16-
const { submitForm, input, errors, values } = createForm({
16+
const { submitForm, input, errors, values, register } = createForm({
1717
validateMode: 'onBlur', // Defaults - Schemaless:onChange Schema:onBlur
1818
initialValues: {
1919
username: '',
@@ -29,11 +29,8 @@ npm install svelte-headless-form
2929
<form on:submit|preventDefault={submitForm((values) => console.log(values))}>
3030
<input
3131
type="text"
32-
name="username"
33-
value={$values.username}
34-
on:input={input.handleChange}
35-
on:blur={input.handleBlur}
36-
on:focus={input.handleFocus}
32+
bind:value={$values.username}
33+
use:register={{ name: 'username' }}
3734
/>
3835
{#if $errors.username}
3936
<div>
@@ -43,11 +40,8 @@ npm install svelte-headless-form
4340

4441
<input
4542
type="password"
46-
name="password"
47-
value={$values.password}
48-
on:input={input.handleChange}
49-
on:blur={input.handleBlur}
50-
on:focus={input.handleFocus}
43+
bind:value={$values.password}
44+
use:register={{ name: 'password' }}
5145
/>
5246
{#if $errors.password}
5347
<div>
@@ -65,8 +59,6 @@ Svelte Headless Form allows for 2 different validation implementations, called s
6559
In the [How To Use](#how-to-use) section we are demonstrating schemaless validation by giving each form value it's own validator in the initialValidators prop.
6660
If you are intereseted in using schema based validaton please give your createForm() a prop called 'validationResolver' which is a single function that returns an object with error strings located at the same path of the corresponding values. In the future we plan to have pre-built validation resolvers for all the major schema based validators like zod, yup and joi to name a few.
6761

68-
### **Please note that schema based validation is not implemented yet**
69-
7062
## Roadmap
7163

7264
These roadmap features are not ordered by priority.
@@ -75,11 +67,11 @@ These roadmap features are not ordered by priority.
7567
- [x] Support validation dependencies.
7668
- [ ] Update README with more advanced examples.
7769
- [ ] Create a website with a tutorial, an API overview and documentation.
78-
- [ ] Send through entire form state to schemaless validators.
70+
- [x] Send through entire form state to schemaless validators.
7971
- [x] Support async schemaless validators.
8072
- [x] Support schema-based validation.
81-
- [ ] Unify useField and useArrayField api by passing down control.
73+
- [x] Unify useField and useFieldArray api by passing down control.
8274
- [ ] Support a revalidateMode in createForm options.
83-
- [ ] Explore simpler options for attaching handleChange, handleBlur and handleFocus events to inputs.
75+
- [x] Explore simpler options for attaching handleChange, handleBlur and handleFocus events to inputs.
8476

8577
Please consider svelte-headless-form in beta until a 1.0 release.

app/.prettierrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"useTabs": true,
33
"singleQuote": true,
4-
"trailingComma": "none",
5-
"printWidth": 100,
4+
"trailingComma": "all",
5+
"printWidth": 120,
66
"plugins": ["prettier-plugin-svelte"],
77
"pluginSearchDirs": ["."],
88
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]

app/src/components/FieldArray.svelte

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script lang="ts">
2+
import type { FormValues, Roles } from '../types/FormValues';
3+
4+
import { useFieldArray } from '../../../src/core/useFieldArray';
5+
import type { FormControl } from '../../../src/types/Form';
6+
import Input from './Input.svelte';
7+
8+
export let control: FormControl<FormValues>;
9+
export let name: string;
10+
const {
11+
fields,
12+
remove,
13+
form: { touched, dirty, pristine },
14+
} = useFieldArray<Roles, FormValues>({ name, control });
15+
</script>
16+
17+
<div>
18+
{#each $fields as _, index}
19+
<Input {control} name={`${name}.${index}`} />
20+
<button type="button" on:click={() => remove(index)}>Remove</button>
21+
<div>touched: {$touched.roles[index]}</div>
22+
<div>dirty: {$dirty.roles[index]}</div>
23+
<div>pristine: {$pristine.roles[index]}</div>
24+
{/each}
25+
</div>

app/src/components/Input.svelte

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script lang="ts">
2+
import { useField } from '../../../src/core/useField';
3+
import type { FormControl } from '../../../src/types/Form';
4+
5+
type IncomingFormValues = $$Generic;
6+
7+
export let name: string;
8+
export let control: FormControl<IncomingFormValues & object>;
9+
export let type = 'text';
10+
export let parseToInt = false;
11+
12+
const {
13+
field: { value },
14+
fieldState: { error, isTouched },
15+
form: { register },
16+
} = useField<string | number | null, IncomingFormValues & object>({ name, control });
17+
</script>
18+
19+
<input
20+
{type}
21+
value={$value}
22+
on:input={(e) =>
23+
($value = parseToInt
24+
? isNaN(parseInt(e.currentTarget.value))
25+
? null
26+
: parseInt(e.currentTarget.value)
27+
: e.currentTarget.value)}
28+
use:register={{ name }}
29+
/>
30+
{#if $error && $isTouched}
31+
<span style="color:red;">{$error}</span>
32+
{/if}

app/src/routes/+layout.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<nav>
2+
<ul>
3+
<li><a href="/">Home</a></li>
4+
<li><a href="/test">Test</a></li>
5+
</ul>
6+
</nav>
7+
8+
<slot />

app/src/routes/+page.svelte

Lines changed: 105 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,124 @@
11
<script lang="ts">
22
import { createForm } from '../../../src/core/createForm';
3+
import FieldArray from '../components/FieldArray.svelte';
4+
import Input from '../components/Input.svelte';
5+
import { roles, type FormValues } from '../types/FormValues';
36
47
const delay = <T>(fn: () => T): Promise<T> =>
58
new Promise((resolve) =>
69
setTimeout(() => {
710
resolve(fn());
8-
}, 3000)
11+
}, 3000),
912
);
1013
11-
const initialValues = {
14+
const initialValues: FormValues = {
1215
username: '',
13-
password: ''
14-
};
15-
const {
16-
submitForm,
17-
touched,
18-
input,
19-
errors,
20-
values,
21-
state,
22-
dirty,
23-
pristine,
24-
resetForm,
25-
resetField
26-
} = createForm({
27-
initialDeps: {
28-
username: ['password']
16+
password: 'password',
17+
nested: {
18+
age: 0,
19+
gender: false,
2920
},
30-
initialValues,
31-
initialValidators: {
32-
username: async (value) =>
33-
await delay(() => (value.length > 0 ? false : 'Username is required')),
34-
password: (value) => (value.length > 0 ? false : 'Password is required')
35-
}
36-
// validationResolver: (values) => {
37-
// const errors: any = {};
21+
roles: ['admin', 'user', 'guest'],
22+
rolesAreUnique: null,
23+
};
24+
25+
const { submitForm, errors, values, state, resetForm, resetField, control, register, touched } =
26+
createForm<FormValues>({
27+
initialValues,
28+
initialValidators: {
29+
username: (value) => delay(() => (value.length > 0 ? false : 'Username is required')),
30+
password: (value) => (value.length > 0 ? false : 'Password is required'),
31+
nested: {
32+
age: (val) => (val === null ? 'Age is required' : val <= 0 ? 'Age must be greater than 0' : false),
33+
gender: (val) => (val === false ? 'Gender must be true' : false),
34+
},
35+
roles: initialValues.roles.map((_) => (val) => !roles.includes(val) ? 'Role is invalid' : false),
36+
rolesAreUnique: (_, { values, errors }) => {
37+
return values.roles.some((role, i) => values.roles.some((_role, j) => _role === role && i !== j)) &&
38+
errors.roles.every((error) => error === false)
39+
? 'Roles must be unique'
40+
: false;
41+
},
42+
},
43+
initialDeps: {
44+
// username: ['nested'],
45+
// password: ['username'],
46+
rolesAreUnique: ['roles'],
47+
},
48+
// validationResolver: (values) => {
49+
// const errors: PartialErrorFields<typeof initialValues> = {};
50+
51+
// if (values.username.length === 0) errors.username = 'Username is required';
52+
53+
// if (values.password.length === 0) errors.password = 'Password is required';
54+
55+
// if (values.nested.age <= 0 || values.nested.age === null)
56+
// errors.nested === undefined
57+
// ? (errors.nested = { age: 'Age must be greater than 0' })
58+
// : (errors.nested.age = 'Age must be greater than 0');
3859
39-
// if (values.username.length === 0) {
40-
// errors.username = 'Username is required';
41-
// }
60+
// if (values.nested.gender === false)
61+
// errors.nested === undefined
62+
// ? (errors.nested = { gender: 'Gender must be true' })
63+
// : (errors.nested.gender = 'Gender must be true');
64+
// return errors;
65+
// }
66+
});
4267
43-
// if (values.password.length === 0) {
44-
// errors.password = 'Password is required';
45-
// }
46-
// return errors;
47-
// }
48-
});
68+
let showUsername = true;
4969
</script>
5070

51-
<form on:submit|preventDefault={submitForm((values) => delay(() => console.log(values)))}>
52-
<input
53-
type="text"
54-
name="username"
55-
value={$values.username}
56-
on:input={input.handleChange}
57-
on:blur={input.handleBlur}
58-
on:focus={input.handleFocus}
59-
/>
60-
<span>touched: {$touched.username}</span>
61-
<span>dirty: {$dirty.username}</span>
62-
<span>pristine: {$pristine.username}</span>
63-
{#if $errors.username}
64-
<span>
65-
{$errors.username}
66-
</span>
71+
<form
72+
on:submit|preventDefault={submitForm(
73+
(values) => delay(() => console.log(values)),
74+
(errors) => console.log(errors),
75+
)}
76+
>
77+
<button type="button" on:click={() => (showUsername = !showUsername)}>Toggle Username</button>
78+
{#if showUsername}
79+
<Input {control} name="username" />
80+
<button type="button" on:click={() => resetField('username')}>Reset Username</button>
6781
{/if}
68-
<button type="button" on:click={() => resetField('username')}>Reset Username</button>
6982

70-
<input
71-
type="password"
72-
name="password"
73-
value={$values.password}
74-
on:input={input.handleChange}
75-
on:blur={input.handleBlur}
76-
on:focus={input.handleFocus}
77-
/>
78-
{#if $errors.password}
79-
<span>
80-
{$errors.password}
81-
</span>
83+
<Input {control} name="password" type="password" />
84+
<Input {control} name="nested.age" type="number" parseToInt />
85+
86+
<button
87+
type="button"
88+
on:click={() => ($values.nested.gender = !$values.nested.gender)}
89+
use:register={{ name: 'nested.gender', changeEvent: 'click' }}>Change Gender</button
90+
>
91+
<div>Gender: {$values.nested.gender}</div>
92+
{#if $errors.nested.gender}
93+
<div>
94+
{$errors.nested.gender}
95+
</div>
96+
{/if}
97+
98+
<FieldArray name="roles" {control} />
99+
<button type="button" on:click={() => resetField('roles')}>Reset Roles</button>
100+
{#if $errors.rolesAreUnique}
101+
<div>{$errors.rolesAreUnique}</div>
82102
{/if}
83103

84-
<button type="submit" disabled={$state.isSubmitting || $state.isValidating}>Submit</button>
85-
<button type="button" on:click={() => resetForm({ username: '123' })}>Reset</button>
104+
<button type="submit">Submit</button>
105+
<button
106+
type="button"
107+
on:click={() =>
108+
resetForm(
109+
{
110+
username: 'banana',
111+
nested: { age: 10 },
112+
roles: ['baker', 'admin', 'user', 'omega'],
113+
},
114+
{
115+
replaceArrays: false,
116+
validators: (values) => ({
117+
roles: values.roles.map((_) => (val) => !roles.includes(val) ? 'Role is invalid' : false),
118+
}),
119+
},
120+
)}>Reset</button
121+
>
86122

87123
<div>
88124
isValidating: {$state.isValidating ? 'Yes' : 'No'}
@@ -99,4 +135,7 @@
99135
<div>
100136
isPristine: {$state.isPristine ? 'Yes' : 'No'}
101137
</div>
138+
<div>
139+
hasErrors: {$state.hasErrors ? 'Yes' : 'No'}
140+
</div>
102141
</form>

app/src/routes/test/+page.svelte

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script lang="ts">
2+
import { createForm } from '../../../../src/core/createForm';
3+
4+
const { register, values } = createForm({
5+
initialValues: {
6+
selection: ''
7+
},
8+
initialValidators: {
9+
selection: (value) => (value.length > 0 ? false : 'Selection is required')
10+
}
11+
});
12+
</script>
13+
14+
<div>
15+
<select
16+
bind:value={$values.selection}
17+
use:register={{ name: 'selection', changeEvent: 'banana' }}
18+
>
19+
<option value="1">1</option> <option value="2">2</option><option value="3">3</option><option
20+
value="4">4</option
21+
></select
22+
>
23+
</div>

0 commit comments

Comments
 (0)