@zoijs/forms 0.1.1 → 0.1.2

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 CHANGED
@@ -1,31 +1,39 @@
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
1
+ # Changelog
2
+
3
+ All notable changes to `@zoijs/forms` are documented here.
4
+
5
+ ## 0.1.2 — 2026-10-05
6
+
7
+ ### Documentation
8
+ - **CDN guidance uses exact, integrity-pinned URLs (SEC-7).** The README's no-install example
9
+ imported a floating `esm.sh/@zoijs/forms@0.1` URL; it now maps `@zoijs/core` and `@zoijs/forms` to
10
+ exact-version jsDelivr files with integrity hashes in an import map and imports by name. README
11
+ only — no code change.
12
+
13
+ ## 0.1.1 — 2026-06-25
14
+
15
+ Consistency hardening — **non-breaking, additive only.**
16
+
17
+ - Added reader-style accessors that match the rest of the ecosystem
18
+ (`data()` / `loading()` / `value(name)` …): **`all()`** (all values),
19
+ **`allErrors()`** (all errors), **`allTouched()`** (all touched flags), and
20
+ **`isTouched(name)`** (one field's touched flag). All reactive inside a binding.
21
+ - The raw reactive state `values` / `errors` / `touched` is **kept** for backward
22
+ compatibility (now documented as advanced/direct access). No existing code breaks.
23
+ - Docs and the login example updated to prefer the reader methods.
24
+ - No behavior changes, no new form features, no core changes.
25
+
26
+ ## 0.1.0 — 2026-06-25
27
+
28
+ Initial release of the tiny, native-forms-first helper for Zoijs.
29
+
30
+ - `form(initialValues, options?)` returning reactive `values` / `errors` / `touched`
31
+ state plus per-field helpers: `value`, `set`, `error`, `setError`, `clearError`,
32
+ `touch`, and `reset`
33
+ - `validate(rules?)` — a simple field → function rule map (no schemas, no deps),
34
+ with optional default rules via `options.validate`
35
+ - `handleSubmit(fn)` — a thin wrapper that prevents the default reload and calls
36
+ `fn(values)`; the network call stays yours (use `@zoijs/action`)
37
+ - Native-forms first: works with ordinary `<input>` / `<textarea>` and submission
38
+ via `@zoijs/action`
39
+ - 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,192 +1,194 @@
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
- [![npm](https://img.shields.io/npm/v/@zoijs/forms.svg)](https://www.npmjs.com/package/@zoijs/forms)
8
- [![license](https://img.shields.io/npm/l/@zoijs/forms.svg)](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
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
+ [![npm](https://img.shields.io/npm/v/@zoijs/forms.svg)](https://www.npmjs.com/package/@zoijs/forms)
8
+ [![license](https://img.shields.io/npm/l/@zoijs/forms.svg)](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: in an import map, point "@zoijs/core" and "@zoijs/forms" at
31
+ **exact-version** jsDelivr file URLs with integrity hashes, then import by name (so every
32
+ package shares one core). See the [CDN guide](https://zoijs.dev/installation#from-a-cdn).
33
+
34
+ ```js
35
+ import { form } from "@zoijs/forms";
36
+ ```
37
+
38
+ ## What `form()` does
39
+
40
+ `form(initialValues, options?)` keeps your form's **values**, **errors**, and
41
+ **touched** state in Zoijs reactive state, with small per-field helpers:
42
+
43
+ ```js
44
+ import { form } from "@zoijs/forms";
45
+
46
+ const login = form({ email: "", password: "" });
47
+
48
+ login.all(); // { email: "", password: "" } — all values (reactive)
49
+ login.value("email"); // one field (reactive)
50
+ login.set("email", "a@b.com"); // update one field
51
+ login.error("email"); // one field's error (reactive)
52
+ login.setError("email", "Required");
53
+ login.clearError("email");
54
+ login.isTouched("email"); // has this field been touched? (reactive)
55
+ login.touch("email"); // mark touched (e.g. on blur)
56
+ login.reset(); // restore initial values, clear errors + touched
57
+ ```
58
+
59
+ These reader methods (`all()`, `value()`, `error()`, `isTouched()`, …) match the
60
+ rest of the ecosystem — the same shape as `data()`, `loading()`, and `pending()` —
61
+ so once you've learned one Zoijs package, the others read the same way.
62
+
63
+ ## The API
64
+
65
+ | Member | What it does |
66
+ |---|---|
67
+ | `form(initialValues, options?)` | Create a form helper |
68
+ | `all()` | Read all values (reactive) |
69
+ | `value(name)` | Read one field's value (reactive) |
70
+ | `set(name, value)` | Update one field |
71
+ | `allErrors()` | Read all errors (reactive) |
72
+ | `error(name)` | Read one field's error (reactive) |
73
+ | `setError(name, message)` | Set one field's error |
74
+ | `clearError(name)` | Clear one field's error |
75
+ | `allTouched()` | Read all touched flags (reactive) |
76
+ | `isTouched(name)` | Has one field been touched? (reactive) |
77
+ | `touch(name)` | Mark a field touched |
78
+ | `reset()` | Restore initial values; clear errors + touched |
79
+ | `validate(rules?)` | Run rules, set errors, return whether valid |
80
+ | `handleSubmit(fn)` | Wrap a submit handler: prevents reload, calls `fn(values)` |
81
+
82
+ ### Advanced: raw state access
83
+
84
+ The underlying reactive state is also exposed as `values`, `errors`, and `touched`
85
+ (each a `createState`-shaped `{ get, set, peek }`). Prefer the reader methods above;
86
+ reach for the raw state only when you need direct access — e.g. `login.values.peek()`
87
+ to read without subscribing. These remain for backward compatibility and won't be
88
+ removed in 0.x.
89
+
90
+ ## Login form example
91
+
92
+ Use native inputs — `value` reads from the form, `oninput` writes back, `onblur`
93
+ marks touched:
94
+
95
+ ```js
96
+ import { html, mount } from "@zoijs/core";
97
+ import { form } from "@zoijs/forms";
98
+
99
+ const login = form({ email: "", password: "" });
100
+
101
+ function App() {
102
+ return html`
103
+ <input
104
+ name="email"
105
+ value=${() => login.value("email")}
106
+ oninput=${(e) => login.set("email", e.target.value)}
107
+ onblur=${() => login.touch("email")}
108
+ />
109
+ ${() => (login.error("email") ? html`<span class="err">${login.error("email")}</span>` : null)}
110
+ `;
111
+ }
112
+
113
+ mount(App, "#app");
114
+ ```
115
+
116
+ See [`examples/login/`](examples/login/).
117
+
118
+ ## Contact form example
119
+
120
+ A contact form with a textarea, default rules via `options.validate`, and
121
+ `handleSubmit`. See [`examples/contact/`](examples/contact/).
122
+
123
+ ## Validation
124
+
125
+ Validation is just a map of field → function. A rule returns a message when
126
+ invalid, or a falsy value when valid. No schemas, no dependencies.
127
+
128
+ ```js
129
+ const valid = login.validate({
130
+ email: (value) => (value.includes("@") ? null : "Enter a valid email"),
131
+ password: (value) => (value.length >= 8 ? null : "Minimum 8 characters"),
132
+ });
133
+ // valid === false, and login.error("email") is now set
134
+ ```
135
+
136
+ You can also pass the rules once via options and call `validate()` with no args:
137
+
138
+ ```js
139
+ const login = form({ email: "", password: "" }, {
140
+ validate: { email: (v) => (v.includes("@") ? null : "Enter a valid email") },
141
+ });
142
+ login.validate();
143
+ ```
144
+
145
+ ## Using it with `@zoijs/action`
146
+
147
+ Forms holds state; **`@zoijs/action` does the request.** Validate, then submit:
148
+
149
+ ```js
150
+ import { action } from "@zoijs/action";
151
+
152
+ const submitLogin = action(async (values) => {
153
+ await api.login(values);
154
+ });
155
+
156
+ html`
157
+ <form onsubmit=${async (e) => {
158
+ e.preventDefault();
159
+ if (!login.validate(rules)) return;
160
+ await submitLogin.run(login.all());
161
+ }}>
162
+ ...
163
+ <button disabled=${() => submitLogin.pending()}>Sign in</button>
164
+ </form>
165
+ `;
166
+ ```
167
+
168
+ `handleSubmit` is a thin convenience that prevents the default reload for you:
169
+
170
+ ```js
171
+ const onSubmit = login.handleSubmit((values) => submitLogin.run(values));
172
+ html`<form onsubmit=${onSubmit}>...</form>`;
173
+ ```
174
+
175
+ ## Common mistakes
176
+
177
+ - **Reading outside a binding.** Wrap reads in an arrow to make them live:
178
+ `value=${() => login.value("email")}`, not `value=${login.value("email")}`.
179
+ - **Expecting forms to submit for you.** It doesn't — pair it with `@zoijs/action`.
180
+ - **Expecting auto-validation.** `set()` only updates the value. Call `validate()`
181
+ (on submit or blur) when you want errors; `clearError()` to remove one.
182
+ - **Reaching for field arrays / nested objects.** Keep values flat. For complex,
183
+ dynamic shapes, manage your own reactive state with `createState`.
184
+
185
+ ## What this package intentionally does *not* do
186
+
187
+ By design, to stay tiny: no form provider/context, no field registration, no
188
+ field arrays, no schema validation, no resolver system, no async-validation
189
+ engine, no controlled-component framework, and no third-party validation
190
+ dependency. It's the 90%-case helper — native forms, plus a little structure.
191
+
192
+ ## License
193
+
194
+ [MIT](LICENSE) © Zoijs contributors
package/package.json CHANGED
@@ -1,61 +1,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
- }
1
+ {
2
+ "name": "@zoijs/forms",
3
+ "version": "0.1.2",
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": "node ../scripts/test-server.mjs .. 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,89 +1,88 @@
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>;
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
+ // Source State from @zoijs/core (re-exported for convenience) so the raw
7
+ // values/errors/touched shape can never drift from `createState`. Other packages
8
+ // import core types the same way (e.g. @zoijs/router imports TemplateResult).
9
+ import type { State } from "@zoijs/core";
10
+ export type { State };
11
+
12
+ /** A single field rule: return a message when invalid, or a falsy value when valid. */
13
+ export type Rule<V = any, Values = Record<string, any>> = (
14
+ value: V,
15
+ values: Values
16
+ ) => string | null | undefined | false;
17
+
18
+ /** A map of field name → rule. */
19
+ export type Rules<V> = { [K in keyof V]?: Rule<V[K], V> };
20
+
21
+ /** Options for {@link form}. */
22
+ export interface FormOptions<V> {
23
+ /** Default rules used by `validate()` and when no rules are passed. */
24
+ validate?: Rules<V>;
25
+ }
26
+
27
+ /** A tiny reactive form helper created by {@link form}. */
28
+ export interface Form<V extends Record<string, any>> {
29
+ // --- values ---
30
+ /** All values (reactive). Reader-style, like `data()` / `loading()`. */
31
+ all(): V;
32
+ /** Read one field's value (reactive). */
33
+ value<K extends keyof V>(name: K): V[K];
34
+ /** Update one field's value. */
35
+ set<K extends keyof V>(name: K, value: V[K]): void;
36
+
37
+ // --- errors ---
38
+ /** All errors keyed by field name (reactive). */
39
+ allErrors(): Partial<Record<keyof V, string>>;
40
+ /** Read one field's error (reactive), or `undefined`. */
41
+ error(name: keyof V): string | undefined;
42
+ /** Set one field's error message. */
43
+ setError(name: keyof V, message: string): void;
44
+ /** Clear one field's error. */
45
+ clearError(name: keyof V): void;
46
+
47
+ // --- touched ---
48
+ /** All touched flags keyed by field name (reactive). */
49
+ allTouched(): Partial<Record<keyof V, boolean>>;
50
+ /** Whether one field has been touched (reactive). */
51
+ isTouched(name: keyof V): boolean;
52
+ /** Mark a field touched (e.g. on blur). */
53
+ touch(name: keyof V): void;
54
+
55
+ // --- lifecycle ---
56
+ /** Restore initial values and clear errors + touched. */
57
+ reset(): void;
58
+ /** Run rules (or the option rules), set errors, and return whether valid. */
59
+ validate(rules?: Rules<V>): boolean;
60
+ /** Wrap a submit handler: prevents the default reload and calls `fn(values)`. */
61
+ handleSubmit(fn: (values: V, event?: Event) => unknown): (event?: Event) => unknown;
62
+
63
+ // --- raw reactive state (advanced / backward-compatible) ---
64
+ // Prefer all() / allErrors() / allTouched() above. These remain for direct
65
+ // state access (e.g. `values.peek()`), and won't be removed in 0.x.
66
+ /** Raw values state. Advanced — prefer {@link Form.all}. */
67
+ values: State<V>;
68
+ /** Raw errors state. Advanced — prefer {@link Form.allErrors}. */
69
+ errors: State<Partial<Record<keyof V, string>>>;
70
+ /** Raw touched state. Advanced — prefer {@link Form.allTouched}. */
71
+ touched: State<Partial<Record<keyof V, boolean>>>;
72
+ }
73
+
74
+ /**
75
+ * Create a tiny reactive form helper. Native-forms first: you write ordinary
76
+ * `<input>`s and submit with `@zoijs/action` — forms only holds values, errors,
77
+ * and touched state.
78
+ *
79
+ * ```ts
80
+ * const login = form({ email: "", password: "" });
81
+ * login.value("email"); // string
82
+ * login.set("email", "a@b.com");
83
+ * login.error("email"); // string | undefined
84
+ * login.touch("email");
85
+ * login.reset();
86
+ * ```
87
+ */
88
+ export function form<V extends Record<string, any>>(initialValues: V, options?: FormOptions<V>): Form<V>;
package/src/index.js CHANGED
@@ -1,122 +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
- // 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
- }
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
+ }