@zoijs/forms 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -18
- package/LICENSE +21 -21
- package/README.md +192 -178
- package/package.json +61 -61
- package/src/index.d.ts +89 -70
- package/src/index.js +122 -105
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `@zoijs/forms` are documented here.
|
|
4
|
-
|
|
5
|
-
## 0.1.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@zoijs/forms` are documented here.
|
|
4
|
+
|
|
5
|
+
## 0.1.1 — 2026-06-25
|
|
6
|
+
|
|
7
|
+
Consistency hardening — **non-breaking, additive only.**
|
|
8
|
+
|
|
9
|
+
- Added reader-style accessors that match the rest of the ecosystem
|
|
10
|
+
(`data()` / `loading()` / `value(name)` …): **`all()`** (all values),
|
|
11
|
+
**`allErrors()`** (all errors), **`allTouched()`** (all touched flags), and
|
|
12
|
+
**`isTouched(name)`** (one field's touched flag). All reactive inside a binding.
|
|
13
|
+
- The raw reactive state `values` / `errors` / `touched` is **kept** for backward
|
|
14
|
+
compatibility (now documented as advanced/direct access). No existing code breaks.
|
|
15
|
+
- Docs and the login example updated to prefer the reader methods.
|
|
16
|
+
- No behavior changes, no new form features, no core changes.
|
|
17
|
+
|
|
18
|
+
## 0.1.0 — 2026-06-25
|
|
19
|
+
|
|
20
|
+
Initial release of the tiny, native-forms-first helper for Zoijs.
|
|
21
|
+
|
|
22
|
+
- `form(initialValues, options?)` returning reactive `values` / `errors` / `touched`
|
|
23
|
+
state plus per-field helpers: `value`, `set`, `error`, `setError`, `clearError`,
|
|
24
|
+
`touch`, and `reset`
|
|
25
|
+
- `validate(rules?)` — a simple field → function rule map (no schemas, no deps),
|
|
26
|
+
with optional default rules via `options.validate`
|
|
27
|
+
- `handleSubmit(fn)` — a thin wrapper that prevents the default reload and calls
|
|
28
|
+
`fn(values)`; the network call stays yours (use `@zoijs/action`)
|
|
29
|
+
- Native-forms first: works with ordinary `<input>` / `<textarea>` and submission
|
|
30
|
+
via `@zoijs/action`
|
|
31
|
+
- Built entirely on `@zoijs/core`'s public API — the core is unchanged
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Zoijs contributors
|
|
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 all
|
|
13
|
-
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 THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zoijs contributors
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,178 +1,192 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# @zoijs/forms
|
|
4
|
-
|
|
5
|
-
**A tiny, native-forms-first helper for [Zoijs](https://zoijs.
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/@zoijs/forms)
|
|
8
|
-
[](LICENSE)
|
|
9
|
-
|
|
10
|
-
[
|
|
11
|
-
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
`@zoijs/forms` is an **optional** package. Add it when a form needs a little
|
|
17
|
-
structure — tracking values, errors, and which fields have been touched. You
|
|
18
|
-
still write ordinary `<input>`s, and you still submit with
|
|
19
|
-
[`@zoijs/action`](https://www.npmjs.com/package/@zoijs/action). Forms never
|
|
20
|
-
touches the network.
|
|
21
|
-
|
|
22
|
-
You can learn the whole thing in about 5 minutes.
|
|
23
|
-
|
|
24
|
-
## Install
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm install @zoijs/core @zoijs/forms
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Or with no install, from a CDN:
|
|
31
|
-
|
|
32
|
-
```js
|
|
33
|
-
import { form } from "https://esm.sh/@zoijs/forms";
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## What `form()` does
|
|
37
|
-
|
|
38
|
-
`form(initialValues, options?)` keeps your form's **values**, **errors**, and
|
|
39
|
-
**touched** state in Zoijs reactive state, with small per-field helpers:
|
|
40
|
-
|
|
41
|
-
```js
|
|
42
|
-
import { form } from "@zoijs/forms";
|
|
43
|
-
|
|
44
|
-
const login = form({ email: "", password: "" });
|
|
45
|
-
|
|
46
|
-
login.
|
|
47
|
-
login.value("email"); // one field (reactive)
|
|
48
|
-
login.set("email", "a@b.com"); // update one field
|
|
49
|
-
login.error("email"); // one field's error (reactive)
|
|
50
|
-
login.setError("email", "Required");
|
|
51
|
-
login.clearError("email");
|
|
52
|
-
login.
|
|
53
|
-
login.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @zoijs/forms
|
|
4
|
+
|
|
5
|
+
**A tiny, native-forms-first helper for [Zoijs](https://zoijs.dev).** Reactive values, errors, and touched state — without a form framework.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@zoijs/forms)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
[Documentation](https://zoijs.dev) · [Core package](https://www.npmjs.com/package/@zoijs/core)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
`@zoijs/forms` is an **optional** package. Add it when a form needs a little
|
|
17
|
+
structure — tracking values, errors, and which fields have been touched. You
|
|
18
|
+
still write ordinary `<input>`s, and you still submit with
|
|
19
|
+
[`@zoijs/action`](https://www.npmjs.com/package/@zoijs/action). Forms never
|
|
20
|
+
touches the network.
|
|
21
|
+
|
|
22
|
+
You can learn the whole thing in about 5 minutes.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @zoijs/core @zoijs/forms
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or with no install, from a CDN:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { form } from "https://esm.sh/@zoijs/forms@0.1";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What `form()` does
|
|
37
|
+
|
|
38
|
+
`form(initialValues, options?)` keeps your form's **values**, **errors**, and
|
|
39
|
+
**touched** state in Zoijs reactive state, with small per-field helpers:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import { form } from "@zoijs/forms";
|
|
43
|
+
|
|
44
|
+
const login = form({ email: "", password: "" });
|
|
45
|
+
|
|
46
|
+
login.all(); // { email: "", password: "" } — all values (reactive)
|
|
47
|
+
login.value("email"); // one field (reactive)
|
|
48
|
+
login.set("email", "a@b.com"); // update one field
|
|
49
|
+
login.error("email"); // one field's error (reactive)
|
|
50
|
+
login.setError("email", "Required");
|
|
51
|
+
login.clearError("email");
|
|
52
|
+
login.isTouched("email"); // has this field been touched? (reactive)
|
|
53
|
+
login.touch("email"); // mark touched (e.g. on blur)
|
|
54
|
+
login.reset(); // restore initial values, clear errors + touched
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
These reader methods (`all()`, `value()`, `error()`, `isTouched()`, …) match the
|
|
58
|
+
rest of the ecosystem — the same shape as `data()`, `loading()`, and `pending()` —
|
|
59
|
+
so once you've learned one Zoijs package, the others read the same way.
|
|
60
|
+
|
|
61
|
+
## The API
|
|
62
|
+
|
|
63
|
+
| Member | What it does |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `form(initialValues, options?)` | Create a form helper |
|
|
66
|
+
| `all()` | Read all values (reactive) |
|
|
67
|
+
| `value(name)` | Read one field's value (reactive) |
|
|
68
|
+
| `set(name, value)` | Update one field |
|
|
69
|
+
| `allErrors()` | Read all errors (reactive) |
|
|
70
|
+
| `error(name)` | Read one field's error (reactive) |
|
|
71
|
+
| `setError(name, message)` | Set one field's error |
|
|
72
|
+
| `clearError(name)` | Clear one field's error |
|
|
73
|
+
| `allTouched()` | Read all touched flags (reactive) |
|
|
74
|
+
| `isTouched(name)` | Has one field been touched? (reactive) |
|
|
75
|
+
| `touch(name)` | Mark a field touched |
|
|
76
|
+
| `reset()` | Restore initial values; clear errors + touched |
|
|
77
|
+
| `validate(rules?)` | Run rules, set errors, return whether valid |
|
|
78
|
+
| `handleSubmit(fn)` | Wrap a submit handler: prevents reload, calls `fn(values)` |
|
|
79
|
+
|
|
80
|
+
### Advanced: raw state access
|
|
81
|
+
|
|
82
|
+
The underlying reactive state is also exposed as `values`, `errors`, and `touched`
|
|
83
|
+
(each a `createState`-shaped `{ get, set, peek }`). Prefer the reader methods above;
|
|
84
|
+
reach for the raw state only when you need direct access — e.g. `login.values.peek()`
|
|
85
|
+
to read without subscribing. These remain for backward compatibility and won't be
|
|
86
|
+
removed in 0.x.
|
|
87
|
+
|
|
88
|
+
## Login form example
|
|
89
|
+
|
|
90
|
+
Use native inputs — `value` reads from the form, `oninput` writes back, `onblur`
|
|
91
|
+
marks touched:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import { html, mount } from "@zoijs/core";
|
|
95
|
+
import { form } from "@zoijs/forms";
|
|
96
|
+
|
|
97
|
+
const login = form({ email: "", password: "" });
|
|
98
|
+
|
|
99
|
+
function App() {
|
|
100
|
+
return html`
|
|
101
|
+
<input
|
|
102
|
+
name="email"
|
|
103
|
+
value=${() => login.value("email")}
|
|
104
|
+
oninput=${(e) => login.set("email", e.target.value)}
|
|
105
|
+
onblur=${() => login.touch("email")}
|
|
106
|
+
/>
|
|
107
|
+
${() => (login.error("email") ? html`<span class="err">${login.error("email")}</span>` : null)}
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
mount(App, "#app");
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See [`examples/login/`](examples/login/).
|
|
115
|
+
|
|
116
|
+
## Contact form example
|
|
117
|
+
|
|
118
|
+
A contact form with a textarea, default rules via `options.validate`, and
|
|
119
|
+
`handleSubmit`. See [`examples/contact/`](examples/contact/).
|
|
120
|
+
|
|
121
|
+
## Validation
|
|
122
|
+
|
|
123
|
+
Validation is just a map of field → function. A rule returns a message when
|
|
124
|
+
invalid, or a falsy value when valid. No schemas, no dependencies.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const valid = login.validate({
|
|
128
|
+
email: (value) => (value.includes("@") ? null : "Enter a valid email"),
|
|
129
|
+
password: (value) => (value.length >= 8 ? null : "Minimum 8 characters"),
|
|
130
|
+
});
|
|
131
|
+
// valid === false, and login.error("email") is now set
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
You can also pass the rules once via options and call `validate()` with no args:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const login = form({ email: "", password: "" }, {
|
|
138
|
+
validate: { email: (v) => (v.includes("@") ? null : "Enter a valid email") },
|
|
139
|
+
});
|
|
140
|
+
login.validate();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Using it with `@zoijs/action`
|
|
144
|
+
|
|
145
|
+
Forms holds state; **`@zoijs/action` does the request.** Validate, then submit:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { action } from "@zoijs/action";
|
|
149
|
+
|
|
150
|
+
const submitLogin = action(async (values) => {
|
|
151
|
+
await api.login(values);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
html`
|
|
155
|
+
<form onsubmit=${async (e) => {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
if (!login.validate(rules)) return;
|
|
158
|
+
await submitLogin.run(login.all());
|
|
159
|
+
}}>
|
|
160
|
+
...
|
|
161
|
+
<button disabled=${() => submitLogin.pending()}>Sign in</button>
|
|
162
|
+
</form>
|
|
163
|
+
`;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`handleSubmit` is a thin convenience that prevents the default reload for you:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
const onSubmit = login.handleSubmit((values) => submitLogin.run(values));
|
|
170
|
+
html`<form onsubmit=${onSubmit}>...</form>`;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Common mistakes
|
|
174
|
+
|
|
175
|
+
- **Reading outside a binding.** Wrap reads in an arrow to make them live:
|
|
176
|
+
`value=${() => login.value("email")}`, not `value=${login.value("email")}`.
|
|
177
|
+
- **Expecting forms to submit for you.** It doesn't — pair it with `@zoijs/action`.
|
|
178
|
+
- **Expecting auto-validation.** `set()` only updates the value. Call `validate()`
|
|
179
|
+
(on submit or blur) when you want errors; `clearError()` to remove one.
|
|
180
|
+
- **Reaching for field arrays / nested objects.** Keep values flat. For complex,
|
|
181
|
+
dynamic shapes, manage your own reactive state with `createState`.
|
|
182
|
+
|
|
183
|
+
## What this package intentionally does *not* do
|
|
184
|
+
|
|
185
|
+
By design, to stay tiny: no form provider/context, no field registration, no
|
|
186
|
+
field arrays, no schema validation, no resolver system, no async-validation
|
|
187
|
+
engine, no controlled-component framework, and no third-party validation
|
|
188
|
+
dependency. It's the 90%-case helper — native forms, plus a little structure.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
[MIT](LICENSE) © Zoijs contributors
|
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@zoijs/forms",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A tiny, native-forms-first helper for Zoijs: reactive values, errors, and touched state. No JSX, no build step.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/index.js",
|
|
7
|
-
"types": "src/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./src/index.d.ts",
|
|
11
|
-
"default": "./src/index.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"src",
|
|
16
|
-
"README.md",
|
|
17
|
-
"LICENSE",
|
|
18
|
-
"CHANGELOG.md"
|
|
19
|
-
],
|
|
20
|
-
"sideEffects": false,
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=18"
|
|
23
|
-
},
|
|
24
|
-
"author": "Zoijs contributors (https://zoijs.
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"homepage": "https://zoijs.
|
|
27
|
-
"repository": {
|
|
28
|
-
"type": "git",
|
|
29
|
-
"url": "git+https://github.com/Zoijs/zoijs.git",
|
|
30
|
-
"directory": "forms"
|
|
31
|
-
},
|
|
32
|
-
"bugs": {
|
|
33
|
-
"url": "https://github.com/Zoijs/zoijs/issues"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"zoijs",
|
|
37
|
-
"forms",
|
|
38
|
-
"form",
|
|
39
|
-
"validation",
|
|
40
|
-
"fields",
|
|
41
|
-
"reactive",
|
|
42
|
-
"no-build",
|
|
43
|
-
"no-jsx"
|
|
44
|
-
],
|
|
45
|
-
"peerDependencies": {
|
|
46
|
-
"@zoijs/core": "^1.0.0"
|
|
47
|
-
},
|
|
48
|
-
"scripts": {
|
|
49
|
-
"test": "node --test --import ./tests/setup-dom.js",
|
|
50
|
-
"test:types": "tsc --noEmit -p tsconfig.json",
|
|
51
|
-
"test:browser": "playwright test",
|
|
52
|
-
"dev": "npx serve -l 3700 .."
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@playwright/test": "^1.61.1",
|
|
56
|
-
"@zoijs/action": "file:../action",
|
|
57
|
-
"@zoijs/core": "file:../framework",
|
|
58
|
-
"jsdom": "^29.1.1",
|
|
59
|
-
"typescript": "^5.9.3"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@zoijs/forms",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A tiny, native-forms-first helper for Zoijs: reactive values, errors, and touched state. No JSX, no build step.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"author": "Zoijs contributors (https://zoijs.dev)",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://zoijs.dev",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/Zoijs/zoijs.git",
|
|
30
|
+
"directory": "forms"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/Zoijs/zoijs/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"zoijs",
|
|
37
|
+
"forms",
|
|
38
|
+
"form",
|
|
39
|
+
"validation",
|
|
40
|
+
"fields",
|
|
41
|
+
"reactive",
|
|
42
|
+
"no-build",
|
|
43
|
+
"no-jsx"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@zoijs/core": "^1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "node --test --import ./tests/setup-dom.js",
|
|
50
|
+
"test:types": "tsc --noEmit -p tsconfig.json",
|
|
51
|
+
"test:browser": "playwright test",
|
|
52
|
+
"dev": "npx serve -l 3700 .."
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@playwright/test": "^1.61.1",
|
|
56
|
+
"@zoijs/action": "file:../action",
|
|
57
|
+
"@zoijs/core": "file:../framework",
|
|
58
|
+
"jsdom": "^29.1.1",
|
|
59
|
+
"typescript": "^5.9.3"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,70 +1,89 @@
|
|
|
1
|
-
// Type definitions for @zoijs/forms.
|
|
2
|
-
//
|
|
3
|
-
// Authored in plain JavaScript; these declarations add editor autocomplete and
|
|
4
|
-
// optional type-checking without requiring TypeScript.
|
|
5
|
-
|
|
6
|
-
/** A reactive value (same shape as core's createState). */
|
|
7
|
-
export interface State<T> {
|
|
8
|
-
get(): T;
|
|
9
|
-
set(value: T): void;
|
|
10
|
-
peek(): T;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** A single field rule: return a message when invalid, or a falsy value when valid. */
|
|
14
|
-
export type Rule<V = any, Values = Record<string, any>> = (
|
|
15
|
-
value: V,
|
|
16
|
-
values: Values
|
|
17
|
-
) => string | null | undefined | false;
|
|
18
|
-
|
|
19
|
-
/** A map of field name → rule. */
|
|
20
|
-
export type Rules<V> = { [K in keyof V]?: Rule<V[K], V> };
|
|
21
|
-
|
|
22
|
-
/** Options for {@link form}. */
|
|
23
|
-
export interface FormOptions<V> {
|
|
24
|
-
/** Default rules used by `validate()` and when no rules are passed. */
|
|
25
|
-
validate?: Rules<V>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** A tiny reactive form helper created by {@link form}. */
|
|
29
|
-
export interface Form<V extends Record<string, any>> {
|
|
30
|
-
|
|
31
|
-
values
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
|
|
1
|
+
// Type definitions for @zoijs/forms.
|
|
2
|
+
//
|
|
3
|
+
// Authored in plain JavaScript; these declarations add editor autocomplete and
|
|
4
|
+
// optional type-checking without requiring TypeScript.
|
|
5
|
+
|
|
6
|
+
/** A reactive value (same shape as core's createState). */
|
|
7
|
+
export interface State<T> {
|
|
8
|
+
get(): T;
|
|
9
|
+
set(value: T): void;
|
|
10
|
+
peek(): T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A single field rule: return a message when invalid, or a falsy value when valid. */
|
|
14
|
+
export type Rule<V = any, Values = Record<string, any>> = (
|
|
15
|
+
value: V,
|
|
16
|
+
values: Values
|
|
17
|
+
) => string | null | undefined | false;
|
|
18
|
+
|
|
19
|
+
/** A map of field name → rule. */
|
|
20
|
+
export type Rules<V> = { [K in keyof V]?: Rule<V[K], V> };
|
|
21
|
+
|
|
22
|
+
/** Options for {@link form}. */
|
|
23
|
+
export interface FormOptions<V> {
|
|
24
|
+
/** Default rules used by `validate()` and when no rules are passed. */
|
|
25
|
+
validate?: Rules<V>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A tiny reactive form helper created by {@link form}. */
|
|
29
|
+
export interface Form<V extends Record<string, any>> {
|
|
30
|
+
// --- values ---
|
|
31
|
+
/** All values (reactive). Reader-style, like `data()` / `loading()`. */
|
|
32
|
+
all(): V;
|
|
33
|
+
/** Read one field's value (reactive). */
|
|
34
|
+
value<K extends keyof V>(name: K): V[K];
|
|
35
|
+
/** Update one field's value. */
|
|
36
|
+
set<K extends keyof V>(name: K, value: V[K]): void;
|
|
37
|
+
|
|
38
|
+
// --- errors ---
|
|
39
|
+
/** All errors keyed by field name (reactive). */
|
|
40
|
+
allErrors(): Partial<Record<keyof V, string>>;
|
|
41
|
+
/** Read one field's error (reactive), or `undefined`. */
|
|
42
|
+
error(name: keyof V): string | undefined;
|
|
43
|
+
/** Set one field's error message. */
|
|
44
|
+
setError(name: keyof V, message: string): void;
|
|
45
|
+
/** Clear one field's error. */
|
|
46
|
+
clearError(name: keyof V): void;
|
|
47
|
+
|
|
48
|
+
// --- touched ---
|
|
49
|
+
/** All touched flags keyed by field name (reactive). */
|
|
50
|
+
allTouched(): Partial<Record<keyof V, boolean>>;
|
|
51
|
+
/** Whether one field has been touched (reactive). */
|
|
52
|
+
isTouched(name: keyof V): boolean;
|
|
53
|
+
/** Mark a field touched (e.g. on blur). */
|
|
54
|
+
touch(name: keyof V): void;
|
|
55
|
+
|
|
56
|
+
// --- lifecycle ---
|
|
57
|
+
/** Restore initial values and clear errors + touched. */
|
|
58
|
+
reset(): void;
|
|
59
|
+
/** Run rules (or the option rules), set errors, and return whether valid. */
|
|
60
|
+
validate(rules?: Rules<V>): boolean;
|
|
61
|
+
/** Wrap a submit handler: prevents the default reload and calls `fn(values)`. */
|
|
62
|
+
handleSubmit(fn: (values: V, event?: Event) => unknown): (event?: Event) => unknown;
|
|
63
|
+
|
|
64
|
+
// --- raw reactive state (advanced / backward-compatible) ---
|
|
65
|
+
// Prefer all() / allErrors() / allTouched() above. These remain for direct
|
|
66
|
+
// state access (e.g. `values.peek()`), and won't be removed in 0.x.
|
|
67
|
+
/** Raw values state. Advanced — prefer {@link Form.all}. */
|
|
68
|
+
values: State<V>;
|
|
69
|
+
/** Raw errors state. Advanced — prefer {@link Form.allErrors}. */
|
|
70
|
+
errors: State<Partial<Record<keyof V, string>>>;
|
|
71
|
+
/** Raw touched state. Advanced — prefer {@link Form.allTouched}. */
|
|
72
|
+
touched: State<Partial<Record<keyof V, boolean>>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a tiny reactive form helper. Native-forms first: you write ordinary
|
|
77
|
+
* `<input>`s and submit with `@zoijs/action` — forms only holds values, errors,
|
|
78
|
+
* and touched state.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* const login = form({ email: "", password: "" });
|
|
82
|
+
* login.value("email"); // string
|
|
83
|
+
* login.set("email", "a@b.com");
|
|
84
|
+
* login.error("email"); // string | undefined
|
|
85
|
+
* login.touch("email");
|
|
86
|
+
* login.reset();
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function form<V extends Record<string, any>>(initialValues: V, options?: FormOptions<V>): Form<V>;
|
package/src/index.js
CHANGED
|
@@ -1,105 +1,122 @@
|
|
|
1
|
-
// @zoijs/forms — a tiny, optional form helper for Zoijs.
|
|
2
|
-
//
|
|
3
|
-
// form(initialValues, options?) keeps a form's values, errors, and touched state
|
|
4
|
-
// in plain Zoijs reactive state, with small per-field helpers. It is native-forms
|
|
5
|
-
// first: you still write ordinary <input>s and read e.target.value yourself, and
|
|
6
|
-
// you still submit with @zoijs/action — forms never touches the network.
|
|
7
|
-
//
|
|
8
|
-
// import { form } from "@zoijs/forms";
|
|
9
|
-
//
|
|
10
|
-
// const login = form({ email: "", password: "" });
|
|
11
|
-
// login.value("email"); // read one field (reactive)
|
|
12
|
-
// login.set("email", "a@b.com"); // update one field
|
|
13
|
-
// login.error("email"); // read one field's error (reactive)
|
|
14
|
-
// login.setError("email", "Required"); login.clearError("email");
|
|
15
|
-
// login.touch("email"); // mark touched (e.g. on blur)
|
|
16
|
-
// login.reset(); // restore initial values, clear errors+touched
|
|
17
|
-
//
|
|
18
|
-
// No provider, no field registration, no context, no schema, no resolver, no
|
|
19
|
-
// async-validation engine. Built entirely on the core's public API (createState)
|
|
20
|
-
// — the core is unchanged.
|
|
21
|
-
|
|
22
|
-
import { createState } from "@zoijs/core";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Create a tiny reactive form helper.
|
|
26
|
-
* @param {Record<string, any>} initialValues
|
|
27
|
-
* @param {{ validate?: Record<string, (value:any, values:any)=>(string|null|undefined)> }} [options]
|
|
28
|
-
*/
|
|
29
|
-
export function form(initialValues = {}, options = {}) {
|
|
30
|
-
const initial = { ...initialValues };
|
|
31
|
-
const values = createState({ ...initial });
|
|
32
|
-
const errors = createState({});
|
|
33
|
-
const touched = createState({});
|
|
34
|
-
|
|
35
|
-
const value = (name) => values.get()[name];
|
|
36
|
-
|
|
37
|
-
const set = (name, val) => {
|
|
38
|
-
values.set({ ...values.get(), [name]: val });
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const error = (name) => errors.get()[name];
|
|
42
|
-
|
|
43
|
-
const setError = (name, message) => {
|
|
44
|
-
errors.set({ ...errors.get(), [name]: message });
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const clearError = (name) => {
|
|
48
|
-
const next = { ...errors.get() };
|
|
49
|
-
delete next[name];
|
|
50
|
-
errors.set(next);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const touch = (name) => {
|
|
54
|
-
if (touched.get()[name]) return; // already touched — no needless update
|
|
55
|
-
touched.set({ ...touched.get(), [name]: true });
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const reset = () => {
|
|
59
|
-
values.set({ ...initial });
|
|
60
|
-
errors.set({});
|
|
61
|
-
touched.set({});
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Run each rule against its field's current value. A rule returns a message
|
|
65
|
-
// string when invalid, or a falsy value when valid. Sets errors and returns
|
|
66
|
-
// whether the whole form is valid. Rules default to options.validate.
|
|
67
|
-
const validate = (rules) => {
|
|
68
|
-
const useRules = rules || options.validate || {};
|
|
69
|
-
const vals = values.get();
|
|
70
|
-
const next = {};
|
|
71
|
-
let valid = true;
|
|
72
|
-
for (const name in useRules) {
|
|
73
|
-
const message = useRules[name](vals[name], vals);
|
|
74
|
-
if (message) {
|
|
75
|
-
next[name] = message;
|
|
76
|
-
valid = false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
errors.set(next);
|
|
80
|
-
return valid;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// A thin submit wrapper: prevent the default page reload and call fn with the
|
|
84
|
-
// current values. Validation and the network call stay yours (use validate()
|
|
85
|
-
// and @zoijs/action) — forms never submits anything itself.
|
|
86
|
-
const handleSubmit = (fn) => (event) => {
|
|
87
|
-
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
88
|
-
return fn(values.get(), event);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
// @zoijs/forms — a tiny, optional form helper for Zoijs.
|
|
2
|
+
//
|
|
3
|
+
// form(initialValues, options?) keeps a form's values, errors, and touched state
|
|
4
|
+
// in plain Zoijs reactive state, with small per-field helpers. It is native-forms
|
|
5
|
+
// first: you still write ordinary <input>s and read e.target.value yourself, and
|
|
6
|
+
// you still submit with @zoijs/action — forms never touches the network.
|
|
7
|
+
//
|
|
8
|
+
// import { form } from "@zoijs/forms";
|
|
9
|
+
//
|
|
10
|
+
// const login = form({ email: "", password: "" });
|
|
11
|
+
// login.value("email"); // read one field (reactive)
|
|
12
|
+
// login.set("email", "a@b.com"); // update one field
|
|
13
|
+
// login.error("email"); // read one field's error (reactive)
|
|
14
|
+
// login.setError("email", "Required"); login.clearError("email");
|
|
15
|
+
// login.touch("email"); // mark touched (e.g. on blur)
|
|
16
|
+
// login.reset(); // restore initial values, clear errors+touched
|
|
17
|
+
//
|
|
18
|
+
// No provider, no field registration, no context, no schema, no resolver, no
|
|
19
|
+
// async-validation engine. Built entirely on the core's public API (createState)
|
|
20
|
+
// — the core is unchanged.
|
|
21
|
+
|
|
22
|
+
import { createState } from "@zoijs/core";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a tiny reactive form helper.
|
|
26
|
+
* @param {Record<string, any>} initialValues
|
|
27
|
+
* @param {{ validate?: Record<string, (value:any, values:any)=>(string|null|undefined)> }} [options]
|
|
28
|
+
*/
|
|
29
|
+
export function form(initialValues = {}, options = {}) {
|
|
30
|
+
const initial = { ...initialValues };
|
|
31
|
+
const values = createState({ ...initial });
|
|
32
|
+
const errors = createState({});
|
|
33
|
+
const touched = createState({});
|
|
34
|
+
|
|
35
|
+
const value = (name) => values.get()[name];
|
|
36
|
+
|
|
37
|
+
const set = (name, val) => {
|
|
38
|
+
values.set({ ...values.get(), [name]: val });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const error = (name) => errors.get()[name];
|
|
42
|
+
|
|
43
|
+
const setError = (name, message) => {
|
|
44
|
+
errors.set({ ...errors.get(), [name]: message });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const clearError = (name) => {
|
|
48
|
+
const next = { ...errors.get() };
|
|
49
|
+
delete next[name];
|
|
50
|
+
errors.set(next);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const touch = (name) => {
|
|
54
|
+
if (touched.get()[name]) return; // already touched — no needless update
|
|
55
|
+
touched.set({ ...touched.get(), [name]: true });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const reset = () => {
|
|
59
|
+
values.set({ ...initial });
|
|
60
|
+
errors.set({});
|
|
61
|
+
touched.set({});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Run each rule against its field's current value. A rule returns a message
|
|
65
|
+
// string when invalid, or a falsy value when valid. Sets errors and returns
|
|
66
|
+
// whether the whole form is valid. Rules default to options.validate.
|
|
67
|
+
const validate = (rules) => {
|
|
68
|
+
const useRules = rules || options.validate || {};
|
|
69
|
+
const vals = values.get();
|
|
70
|
+
const next = {};
|
|
71
|
+
let valid = true;
|
|
72
|
+
for (const name in useRules) {
|
|
73
|
+
const message = useRules[name](vals[name], vals);
|
|
74
|
+
if (message) {
|
|
75
|
+
next[name] = message;
|
|
76
|
+
valid = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
errors.set(next);
|
|
80
|
+
return valid;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// A thin submit wrapper: prevent the default page reload and call fn with the
|
|
84
|
+
// current values. Validation and the network call stay yours (use validate()
|
|
85
|
+
// and @zoijs/action) — forms never submits anything itself.
|
|
86
|
+
const handleSubmit = (fn) => (event) => {
|
|
87
|
+
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
88
|
+
return fn(values.get(), event);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Reader-style accessors for whole-form state, matching the rest of the
|
|
92
|
+
// ecosystem (data(), loading(), value(name), …). Reactive inside a binding.
|
|
93
|
+
const all = () => values.get();
|
|
94
|
+
const allErrors = () => errors.get();
|
|
95
|
+
const allTouched = () => touched.get();
|
|
96
|
+
const isTouched = (name) => !!touched.get()[name];
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
// values
|
|
100
|
+
all,
|
|
101
|
+
value,
|
|
102
|
+
set,
|
|
103
|
+
// errors
|
|
104
|
+
allErrors,
|
|
105
|
+
error,
|
|
106
|
+
setError,
|
|
107
|
+
clearError,
|
|
108
|
+
// touched
|
|
109
|
+
allTouched,
|
|
110
|
+
isTouched,
|
|
111
|
+
touch,
|
|
112
|
+
// lifecycle
|
|
113
|
+
reset,
|
|
114
|
+
validate,
|
|
115
|
+
handleSubmit,
|
|
116
|
+
// raw reactive state — advanced / backward-compatible. Prefer all() /
|
|
117
|
+
// allErrors() / allTouched() above; these stay for direct state access.
|
|
118
|
+
values,
|
|
119
|
+
errors,
|
|
120
|
+
touched,
|
|
121
|
+
};
|
|
122
|
+
}
|