@streetui/forms 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StreetUI 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 ADDED
@@ -0,0 +1,158 @@
1
+ # @streetui/forms
2
+
3
+ A reactive form model and a small synchronous validation system, built
4
+ **entirely** on `@streetui/state` signals. There is no second state system: every
5
+ piece of form state — `values`, `errors`, `touched`, `dirty`, `valid`, and the
6
+ submission lifecycle — is a signal or a derived signal, so it composes with the
7
+ renderer's existing reactive bindings and needs no special integration.
8
+
9
+ ```bash
10
+ # part of the StreetUI monorepo — no separate install
11
+ ```
12
+
13
+ ---
14
+
15
+ ## The form model
16
+
17
+ `createForm<T>()` takes typed initial values, optional per-field validators, and
18
+ an `onSubmit` handler. `T` must extend `Record<string, string>` (HTML input
19
+ values are strings), and every reactive accessor is typed against it.
20
+
21
+ ```ts
22
+ import { createForm, required, email, minLength } from '@streetui/forms';
23
+
24
+ interface SignupValues {
25
+ name: string;
26
+ email: string;
27
+ password: string;
28
+ [key: string]: string;
29
+ }
30
+
31
+ const form = createForm<SignupValues>({
32
+ initialValues: { name: '', email: '', password: '' },
33
+ validators: {
34
+ name: required('Name is required'),
35
+ email: [required('Email is required'), email()], // ordered — first error wins
36
+ password: [required(), minLength(8, 'At least 8 characters')],
37
+ },
38
+ onSubmit: async (values) => {
39
+ await createAccount(values); // any async function; may throw
40
+ },
41
+ });
42
+ ```
43
+
44
+ ### Form-level reactive state
45
+
46
+ | Accessor | Type | Meaning |
47
+ |---|---|---|
48
+ | `form.values` | `ReadonlySignal<T>` | Current values of every field. |
49
+ | `form.errors` | `ReadonlySignal<Partial<Record<keyof T, string>>>` | Only the fields that currently have an error. |
50
+ | `form.touched` | `ReadonlySignal<Partial<Record<keyof T, boolean>>>` | Per-field touched flags. |
51
+ | `form.dirty` | `ReadonlySignal<boolean>` | True if any field differs from its initial value. |
52
+ | `form.valid` | `ReadonlySignal<boolean>` | True when every field passes validation. |
53
+ | `form.submitting` | `ReadonlySignal<boolean>` | True while `onSubmit` is in flight. |
54
+ | `form.submitted` | `ReadonlySignal<boolean>` | True after a successful submit. |
55
+ | `form.status` | `ReadonlySignal<'idle' \| 'submitting' \| 'success' \| 'error'>` | The submission lifecycle. |
56
+ | `form.submitError` | `ReadonlySignal<unknown>` | Whatever `onSubmit` threw, if anything. |
57
+
58
+ Because these are the same signals used everywhere else in StreetUI, they drop
59
+ straight into DSL bindings and `derived()`.
60
+
61
+ ---
62
+
63
+ ## Fields and input binding
64
+
65
+ `form.field(name)` returns the reactive state and setters for one field. The
66
+ field's `value` is a writable `Signal<string>` — pass it directly to the DSL's
67
+ `input({ bind })`. That is the **one** binding the renderer already wires:
68
+ typing updates form state, and programmatic updates update the input. No
69
+ duplicate event listeners are added.
70
+
71
+ ```ts
72
+ const emailField = form.field('email');
73
+
74
+ page.input({ bind: emailField.value, type: 'email' });
75
+ page.when(
76
+ derived(() => emailField.touched.get() && emailField.error.get() !== undefined),
77
+ (err) => err.text(derived(() => emailField.error.get() ?? ''), { role: 'alert' }),
78
+ );
79
+ ```
80
+
81
+ A `Field` exposes `value` (writable signal), `error`, `touched`, `dirty`, `valid`
82
+ (read-only signals), and the imperative helpers `setValue()`, `markTouched()`,
83
+ and `reset()`. Errors are computed reactively from the validators, so surfacing
84
+ them is a matter of reading `field.error` — usually gated on `field.touched` so
85
+ a pristine field is not flagged before the user has interacted with it.
86
+
87
+ ---
88
+
89
+ ## Submission lifecycle
90
+
91
+ `form.submit()` marks every field touched (so validation errors become visible),
92
+ then:
93
+
94
+ - if the form is **invalid**, it stays `idle` and returns without calling
95
+ `onSubmit` — the now-visible field errors tell the user what to fix;
96
+ - if **valid**, it transitions `idle → submitting`, awaits `onSubmit(values)`,
97
+ and settles on `success` or — if `onSubmit` throws — `error`, capturing the
98
+ thrown value in `form.submitError`.
99
+
100
+ Wire it to both the button and the native form submit event; the renderer's
101
+ event system already calls `preventDefault()` for submit events, so no manual
102
+ handling is needed:
103
+
104
+ ```ts
105
+ page.form('signup', (fb) => {
106
+ // ...fields...
107
+ fb.button(
108
+ derived(() => (form.submitting.get() ? 'Creating…' : 'Create account')),
109
+ { disabled: form.submitting, onClick: () => void form.submit() },
110
+ );
111
+ }, { onSubmit: () => void form.submit() });
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Validators
117
+
118
+ A `Validator` is just `(value: string) => string | undefined` — return a message
119
+ to fail, `undefined` to pass. Built-ins cover the common cases; anything else is
120
+ a plain function.
121
+
122
+ | Validator | Fails when |
123
+ |---|---|
124
+ | `required(message?)` | the trimmed value is empty |
125
+ | `minLength(n, message?)` | shorter than `n` characters |
126
+ | `maxLength(n, message?)` | longer than `n` characters |
127
+ | `email(message?)` | a non-empty value is not a plausible email |
128
+ | `pattern(regex, message?)` | a non-empty value does not match `regex` |
129
+
130
+ Per-field validators run **in order and the first error wins**, so list
131
+ `required` first for mandatory fields. A custom validator needs no wrapper:
132
+
133
+ ```ts
134
+ const noSpaces: Validator = (v) => (/\s/.test(v) ? 'No spaces allowed' : undefined);
135
+
136
+ createForm({ initialValues: { handle: '' }, validators: { handle: [required(), noSpaces] } });
137
+ ```
138
+
139
+ ### Async validation
140
+
141
+ Async validation is intentionally **not** part of this core, to keep the
142
+ validity model synchronous and predictable. Layer it on with `@streetui/state`'s
143
+ `resource()`: kick off a resource on value change and surface `resource.error`
144
+ alongside `field.error`. This keeps network-driven checks out of the synchronous
145
+ `valid` computation.
146
+
147
+ ---
148
+
149
+ ## Lifecycle
150
+
151
+ `createForm()` allocates field subscriptions and derived signals. Call
152
+ `form.dispose()` when the owning subtree is torn down — inside a route this is
153
+ `ctx.onCleanup(() => form.dispose())` — to release every subscription and derived
154
+ signal. `form.reset()` restores initial values and clears errors, touched, dirty,
155
+ and submission state without disposing.
156
+
157
+ A complete, runnable form (real HTTP submit, validation, error state, SSR +
158
+ hydration, i18n'd messages) lives in `examples/streetui-account`.
package/dist/index.cjs ADDED
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createForm: () => createForm,
24
+ email: () => email,
25
+ maxLength: () => maxLength,
26
+ minLength: () => minLength,
27
+ pattern: () => pattern,
28
+ required: () => required,
29
+ runValidators: () => runValidators
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/validators.ts
34
+ function required(message = "This field is required") {
35
+ return (value) => value.trim().length === 0 ? message : void 0;
36
+ }
37
+ function minLength(length, message) {
38
+ return (value) => value.length < length ? message ?? `Must be at least ${length} characters` : void 0;
39
+ }
40
+ function maxLength(length, message) {
41
+ return (value) => value.length > length ? message ?? `Must be at most ${length} characters` : void 0;
42
+ }
43
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
44
+ function email(message = "Enter a valid email address") {
45
+ return (value) => value.length === 0 || EMAIL_RE.test(value) ? void 0 : message;
46
+ }
47
+ function pattern(regex, message = "Invalid format") {
48
+ return (value) => value.length === 0 || regex.test(value) ? void 0 : message;
49
+ }
50
+ function runValidators(value, validators) {
51
+ if (validators === void 0) return void 0;
52
+ const list = Array.isArray(validators) ? validators : [validators];
53
+ for (const validate of list) {
54
+ const error = validate(value);
55
+ if (error !== void 0) return error;
56
+ }
57
+ return void 0;
58
+ }
59
+
60
+ // src/form.ts
61
+ var import_state = require("@streetui/state");
62
+ function createForm(config) {
63
+ const names = Object.keys(config.initialValues);
64
+ const validators = config.validators ?? {};
65
+ let programmatic = false;
66
+ const fields = /* @__PURE__ */ new Map();
67
+ for (const name of names) {
68
+ const initial = config.initialValues[name];
69
+ const value = (0, import_state.signal)(initial);
70
+ const touched = (0, import_state.signal)(false);
71
+ const error = (0, import_state.derived)(
72
+ () => runValidators(value.get(), validators[name])
73
+ );
74
+ const valid2 = (0, import_state.derived)(() => error.get() === void 0);
75
+ const dirty2 = (0, import_state.derived)(() => value.get() !== initial);
76
+ const unsub = value.subscribe(() => {
77
+ if (!programmatic) touched.set(true);
78
+ });
79
+ const api = {
80
+ name,
81
+ value,
82
+ error,
83
+ touched,
84
+ dirty: dirty2,
85
+ valid: valid2,
86
+ setValue(next) {
87
+ value.set(next);
88
+ },
89
+ markTouched(next = true) {
90
+ touched.set(next);
91
+ },
92
+ reset() {
93
+ programmatic = true;
94
+ try {
95
+ (0, import_state.batch)(() => {
96
+ value.set(initial);
97
+ touched.set(false);
98
+ });
99
+ } finally {
100
+ programmatic = false;
101
+ }
102
+ }
103
+ };
104
+ fields.set(name, { api, value, touched, error, valid: valid2, dirty: dirty2, initial, unsub });
105
+ }
106
+ const field = (name) => {
107
+ const f = fields.get(name);
108
+ if (f === void 0) throw new Error(`Unknown form field: ${name}`);
109
+ return f;
110
+ };
111
+ const values = (0, import_state.derived)(() => {
112
+ const out = {};
113
+ for (const name of names) out[name] = field(name).value.get();
114
+ return out;
115
+ });
116
+ const errors = (0, import_state.derived)(() => {
117
+ const out = {};
118
+ for (const name of names) {
119
+ const e = field(name).error.get();
120
+ if (e !== void 0) out[name] = e;
121
+ }
122
+ return out;
123
+ });
124
+ const touchedMap = (0, import_state.derived)(() => {
125
+ const out = {};
126
+ for (const name of names) out[name] = field(name).touched.get();
127
+ return out;
128
+ });
129
+ const dirty = (0, import_state.derived)(() => names.some((n) => field(n).dirty.get()));
130
+ const valid = (0, import_state.derived)(() => names.every((n) => field(n).valid.get()));
131
+ const status = (0, import_state.signal)("idle");
132
+ const submitting = (0, import_state.derived)(() => status.get() === "submitting");
133
+ const submitted = (0, import_state.derived)(() => status.get() === "success");
134
+ const submitError = (0, import_state.signal)(void 0);
135
+ function setValues(partial) {
136
+ programmatic = true;
137
+ try {
138
+ (0, import_state.batch)(() => {
139
+ for (const name of names) {
140
+ const next = partial[name];
141
+ if (next !== void 0) field(name).value.set(next);
142
+ }
143
+ });
144
+ } finally {
145
+ programmatic = false;
146
+ }
147
+ }
148
+ function reset() {
149
+ programmatic = true;
150
+ try {
151
+ (0, import_state.batch)(() => {
152
+ for (const name of names) {
153
+ const f = field(name);
154
+ f.value.set(f.initial);
155
+ f.touched.set(false);
156
+ }
157
+ status.set("idle");
158
+ submitError.set(void 0);
159
+ });
160
+ } finally {
161
+ programmatic = false;
162
+ }
163
+ }
164
+ async function submit() {
165
+ (0, import_state.batch)(() => {
166
+ for (const name of names) field(name).touched.set(true);
167
+ });
168
+ if (!valid.peek()) {
169
+ return;
170
+ }
171
+ submitError.set(void 0);
172
+ status.set("submitting");
173
+ try {
174
+ await config.onSubmit?.(values.peek());
175
+ status.set("success");
176
+ } catch (err) {
177
+ submitError.set(err);
178
+ status.set("error");
179
+ }
180
+ }
181
+ function dispose() {
182
+ for (const f of fields.values()) {
183
+ f.unsub();
184
+ f.error.dispose();
185
+ f.valid.dispose();
186
+ f.dirty.dispose();
187
+ }
188
+ values.dispose();
189
+ errors.dispose();
190
+ touchedMap.dispose();
191
+ dirty.dispose();
192
+ valid.dispose();
193
+ submitting.dispose();
194
+ submitted.dispose();
195
+ }
196
+ return {
197
+ values,
198
+ errors,
199
+ touched: touchedMap,
200
+ dirty,
201
+ valid,
202
+ submitting,
203
+ submitted,
204
+ status,
205
+ submitError,
206
+ field: (name) => field(name).api,
207
+ setValues,
208
+ submit,
209
+ reset,
210
+ dispose
211
+ };
212
+ }
213
+ // Annotate the CommonJS export names for ESM import in node:
214
+ 0 && (module.exports = {
215
+ createForm,
216
+ email,
217
+ maxLength,
218
+ minLength,
219
+ pattern,
220
+ required,
221
+ runValidators
222
+ });
223
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/validators.ts","../src/form.ts"],"sourcesContent":["export * from './validators.js';\nexport * from './form.js';\n","/**\n * A small, sync validation system.\n *\n * A `Validator` maps a string field value to an error message, or `undefined`\n * when the value is acceptable. This is deliberately tiny — the built-ins cover\n * the common cases (`required`, `minLength`, `maxLength`, `email`, `pattern`)\n * and anything else is just a plain function `(value: string) => string | undefined`.\n *\n * Validators for a field run in order and the FIRST error wins, so list\n * `required` first if a field is mandatory.\n *\n * Async validation is intentionally NOT part of this core. It can be layered on\n * top with `@streetui/state`'s `resource()` (kick off a resource on value\n * change and surface `resource.error` alongside the field error) without\n * destabilising the synchronous validity model here.\n */\n\nexport type Validator = (value: string) => string | undefined;\n\n/** Fails when the trimmed value is empty. */\nexport function required(message = 'This field is required'): Validator {\n return (value) => (value.trim().length === 0 ? message : undefined);\n}\n\n/** Fails when the value is shorter than `length` characters. */\nexport function minLength(length: number, message?: string): Validator {\n return (value) =>\n value.length < length\n ? (message ?? `Must be at least ${length} characters`)\n : undefined;\n}\n\n/** Fails when the value is longer than `length` characters. */\nexport function maxLength(length: number, message?: string): Validator {\n return (value) =>\n value.length > length\n ? (message ?? `Must be at most ${length} characters`)\n : undefined;\n}\n\n// Pragmatic, dependency-free email shape check (not a full RFC 5322 parser).\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** Fails when a non-empty value is not a plausible email address. */\nexport function email(message = 'Enter a valid email address'): Validator {\n return (value) => (value.length === 0 || EMAIL_RE.test(value) ? undefined : message);\n}\n\n/** Fails when a non-empty value does not match `regex`. */\nexport function pattern(regex: RegExp, message = 'Invalid format'): Validator {\n return (value) => (value.length === 0 || regex.test(value) ? undefined : message);\n}\n\n/** Run a validator (or ordered list) and return the first error, if any. */\nexport function runValidators(\n value: string,\n validators: Validator | ReadonlyArray<Validator> | undefined,\n): string | undefined {\n if (validators === undefined) return undefined;\n const list = Array.isArray(validators) ? validators : [validators];\n for (const validate of list) {\n const error = validate(value);\n if (error !== undefined) return error;\n }\n return undefined;\n}\n","/**\n * Reactive form model built entirely on `@streetui/state` signals.\n *\n * There is no second state system here: every piece of form state (`values`,\n * `errors`, `touched`, `dirty`, `valid`, submission status) is a signal or a\n * derived signal, so it composes with the renderer's existing reactive bindings.\n * A field's `value` is a writable `Signal<string>`, which plugs straight into\n * the DSL's `input({ bind })` — typing updates form state and programmatic\n * updates update the input, through the one binding the renderer already wires.\n */\n\nimport {\n signal,\n derived,\n batch,\n DerivedSignal,\n type Signal,\n type ReadonlySignal,\n type Unsubscribe,\n} from '@streetui/state';\nimport { type Validator, runValidators } from './validators.js';\n\n/** Form values are a flat, typed record of string fields (HTML input values). */\nexport type FormValues = Record<string, string>;\n\nexport interface Field {\n readonly name: string;\n /** Writable value signal — pass to `input({ bind: field.value })`. */\n readonly value: Signal<string>;\n /** Current validation error, or `undefined` when the field is valid. */\n readonly error: ReadonlySignal<string | undefined>;\n /** True once the field has received a genuine user interaction. */\n readonly touched: ReadonlySignal<boolean>;\n /** True when the value differs from its initial value. */\n readonly dirty: ReadonlySignal<boolean>;\n /** True when the field has no validation error. */\n readonly valid: ReadonlySignal<boolean>;\n /** Programmatically set the value (does not mark the field touched). */\n setValue(next: string): void;\n /** Force the touched flag (defaults to true). */\n markTouched(touched?: boolean): void;\n /** Restore this field's initial value and clear its touched flag. */\n reset(): void;\n}\n\nexport type FormValidators<T extends FormValues> = {\n readonly [K in keyof T]?: Validator | ReadonlyArray<Validator>;\n};\n\nexport interface FormConfig<T extends FormValues> {\n readonly initialValues: T;\n readonly validators?: FormValidators<T>;\n /** Called by `submit()` once all fields are valid. May be async. */\n readonly onSubmit?: (values: T) => void | Promise<void>;\n}\n\nexport type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';\n\nexport interface Form<T extends FormValues> {\n readonly values: ReadonlySignal<T>;\n readonly errors: ReadonlySignal<Partial<Record<keyof T, string>>>;\n readonly touched: ReadonlySignal<Partial<Record<keyof T, boolean>>>;\n readonly dirty: ReadonlySignal<boolean>;\n readonly valid: ReadonlySignal<boolean>;\n readonly submitting: ReadonlySignal<boolean>;\n readonly submitted: ReadonlySignal<boolean>;\n readonly status: ReadonlySignal<SubmitStatus>;\n readonly submitError: ReadonlySignal<unknown>;\n /** Access the reactive state + setters for one field. */\n field<K extends keyof T & string>(name: K): Field;\n /** Merge a partial set of values in (does not mark fields touched). */\n setValues(partial: Partial<T>): void;\n /** Validate, mark all fields touched, then run `onSubmit` if valid. */\n submit(): Promise<void>;\n /** Restore initial values and clear errors/touched/dirty/submission state. */\n reset(): void;\n /** Tear down all field subscriptions and derived signals. */\n dispose(): void;\n}\n\ninterface FieldInternal {\n readonly api: Field;\n readonly value: Signal<string>;\n readonly touched: Signal<boolean>;\n readonly error: DerivedSignal<string | undefined>;\n readonly valid: DerivedSignal<boolean>;\n readonly dirty: DerivedSignal<boolean>;\n readonly initial: string;\n readonly unsub: Unsubscribe;\n}\n\n// __FORM_IMPL__\n\nexport function createForm<T extends FormValues>(config: FormConfig<T>): Form<T> {\n const names = Object.keys(config.initialValues) as Array<keyof T & string>;\n const validators = config.validators ?? ({} as FormValidators<T>);\n\n // While true, value changes originate from setValues()/reset() and must NOT\n // mark a field touched. Batch flushes run synchronously inside batch(), i.e.\n // before this flag is reset, so guarding around batch() is sound.\n let programmatic = false;\n\n const fields = new Map<string, FieldInternal>();\n\n for (const name of names) {\n // Keys come from Object.keys(initialValues), so the value is always present;\n // the annotation defeats noUncheckedIndexedAccess widening to `| undefined`.\n const initial: string = config.initialValues[name] as string;\n const value = signal<string>(initial);\n const touched = signal<boolean>(false);\n const error = derived<string | undefined>(() =>\n runValidators(value.get(), validators[name]),\n );\n const valid = derived<boolean>(() => error.get() === undefined);\n const dirty = derived<boolean>(() => value.get() !== initial);\n\n // Mark touched on the first genuine (non-programmatic) value change.\n const unsub = value.subscribe(() => {\n if (!programmatic) touched.set(true);\n });\n\n const api: Field = {\n name,\n value,\n error,\n touched,\n dirty,\n valid,\n setValue(next: string): void {\n value.set(next);\n },\n markTouched(next = true): void {\n touched.set(next);\n },\n reset(): void {\n programmatic = true;\n try {\n batch(() => {\n value.set(initial);\n touched.set(false);\n });\n } finally {\n programmatic = false;\n }\n },\n };\n\n fields.set(name, { api, value, touched, error, valid, dirty, initial, unsub });\n }\n\n const field = (name: string): FieldInternal => {\n const f = fields.get(name);\n if (f === undefined) throw new Error(`Unknown form field: ${name}`);\n return f;\n };\n\n const values = derived<T>(() => {\n const out: Record<string, string> = {};\n for (const name of names) out[name] = field(name).value.get();\n return out as T;\n });\n\n const errors = derived<Partial<Record<keyof T, string>>>(() => {\n const out: Partial<Record<keyof T, string>> = {};\n for (const name of names) {\n const e = field(name).error.get();\n if (e !== undefined) out[name] = e;\n }\n return out;\n });\n\n const touchedMap = derived<Partial<Record<keyof T, boolean>>>(() => {\n const out: Partial<Record<keyof T, boolean>> = {};\n for (const name of names) out[name] = field(name).touched.get();\n return out;\n });\n\n const dirty = derived<boolean>(() => names.some((n) => field(n).dirty.get()));\n const valid = derived<boolean>(() => names.every((n) => field(n).valid.get()));\n\n const status = signal<SubmitStatus>('idle');\n const submitting = derived<boolean>(() => status.get() === 'submitting');\n const submitted = derived<boolean>(() => status.get() === 'success');\n const submitError = signal<unknown>(undefined);\n\n function setValues(partial: Partial<T>): void {\n programmatic = true;\n try {\n batch(() => {\n for (const name of names) {\n const next = partial[name];\n if (next !== undefined) field(name).value.set(next);\n }\n });\n } finally {\n programmatic = false;\n }\n }\n\n function reset(): void {\n programmatic = true;\n try {\n batch(() => {\n for (const name of names) {\n const f = field(name);\n f.value.set(f.initial);\n f.touched.set(false);\n }\n status.set('idle');\n submitError.set(undefined);\n });\n } finally {\n programmatic = false;\n }\n }\n\n async function submit(): Promise<void> {\n // Touch every field so validation errors become visible on submit attempts.\n batch(() => {\n for (const name of names) field(name).touched.set(true);\n });\n if (!valid.peek()) {\n // Invalid — do not enter the submitting lifecycle; field errors now show.\n return;\n }\n submitError.set(undefined);\n status.set('submitting');\n try {\n await config.onSubmit?.(values.peek());\n status.set('success');\n } catch (err) {\n submitError.set(err);\n status.set('error');\n }\n }\n\n function dispose(): void {\n for (const f of fields.values()) {\n f.unsub();\n f.error.dispose();\n f.valid.dispose();\n f.dirty.dispose();\n }\n values.dispose();\n errors.dispose();\n touchedMap.dispose();\n dirty.dispose();\n valid.dispose();\n submitting.dispose();\n submitted.dispose();\n }\n\n return {\n values,\n errors,\n touched: touchedMap,\n dirty,\n valid,\n submitting,\n submitted,\n status,\n submitError,\n field: (name) => field(name).api,\n setValues,\n submit,\n reset,\n dispose,\n };\n}\n\n"],"mappings":"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","names":["valid","dirty"]}
@@ -0,0 +1,98 @@
1
+ import { Signal, ReadonlySignal } from '@streetui/state';
2
+
3
+ /**
4
+ * A small, sync validation system.
5
+ *
6
+ * A `Validator` maps a string field value to an error message, or `undefined`
7
+ * when the value is acceptable. This is deliberately tiny — the built-ins cover
8
+ * the common cases (`required`, `minLength`, `maxLength`, `email`, `pattern`)
9
+ * and anything else is just a plain function `(value: string) => string | undefined`.
10
+ *
11
+ * Validators for a field run in order and the FIRST error wins, so list
12
+ * `required` first if a field is mandatory.
13
+ *
14
+ * Async validation is intentionally NOT part of this core. It can be layered on
15
+ * top with `@streetui/state`'s `resource()` (kick off a resource on value
16
+ * change and surface `resource.error` alongside the field error) without
17
+ * destabilising the synchronous validity model here.
18
+ */
19
+ type Validator = (value: string) => string | undefined;
20
+ /** Fails when the trimmed value is empty. */
21
+ declare function required(message?: string): Validator;
22
+ /** Fails when the value is shorter than `length` characters. */
23
+ declare function minLength(length: number, message?: string): Validator;
24
+ /** Fails when the value is longer than `length` characters. */
25
+ declare function maxLength(length: number, message?: string): Validator;
26
+ /** Fails when a non-empty value is not a plausible email address. */
27
+ declare function email(message?: string): Validator;
28
+ /** Fails when a non-empty value does not match `regex`. */
29
+ declare function pattern(regex: RegExp, message?: string): Validator;
30
+ /** Run a validator (or ordered list) and return the first error, if any. */
31
+ declare function runValidators(value: string, validators: Validator | ReadonlyArray<Validator> | undefined): string | undefined;
32
+
33
+ /**
34
+ * Reactive form model built entirely on `@streetui/state` signals.
35
+ *
36
+ * There is no second state system here: every piece of form state (`values`,
37
+ * `errors`, `touched`, `dirty`, `valid`, submission status) is a signal or a
38
+ * derived signal, so it composes with the renderer's existing reactive bindings.
39
+ * A field's `value` is a writable `Signal<string>`, which plugs straight into
40
+ * the DSL's `input({ bind })` — typing updates form state and programmatic
41
+ * updates update the input, through the one binding the renderer already wires.
42
+ */
43
+
44
+ /** Form values are a flat, typed record of string fields (HTML input values). */
45
+ type FormValues = Record<string, string>;
46
+ interface Field {
47
+ readonly name: string;
48
+ /** Writable value signal — pass to `input({ bind: field.value })`. */
49
+ readonly value: Signal<string>;
50
+ /** Current validation error, or `undefined` when the field is valid. */
51
+ readonly error: ReadonlySignal<string | undefined>;
52
+ /** True once the field has received a genuine user interaction. */
53
+ readonly touched: ReadonlySignal<boolean>;
54
+ /** True when the value differs from its initial value. */
55
+ readonly dirty: ReadonlySignal<boolean>;
56
+ /** True when the field has no validation error. */
57
+ readonly valid: ReadonlySignal<boolean>;
58
+ /** Programmatically set the value (does not mark the field touched). */
59
+ setValue(next: string): void;
60
+ /** Force the touched flag (defaults to true). */
61
+ markTouched(touched?: boolean): void;
62
+ /** Restore this field's initial value and clear its touched flag. */
63
+ reset(): void;
64
+ }
65
+ type FormValidators<T extends FormValues> = {
66
+ readonly [K in keyof T]?: Validator | ReadonlyArray<Validator>;
67
+ };
68
+ interface FormConfig<T extends FormValues> {
69
+ readonly initialValues: T;
70
+ readonly validators?: FormValidators<T>;
71
+ /** Called by `submit()` once all fields are valid. May be async. */
72
+ readonly onSubmit?: (values: T) => void | Promise<void>;
73
+ }
74
+ type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';
75
+ interface Form<T extends FormValues> {
76
+ readonly values: ReadonlySignal<T>;
77
+ readonly errors: ReadonlySignal<Partial<Record<keyof T, string>>>;
78
+ readonly touched: ReadonlySignal<Partial<Record<keyof T, boolean>>>;
79
+ readonly dirty: ReadonlySignal<boolean>;
80
+ readonly valid: ReadonlySignal<boolean>;
81
+ readonly submitting: ReadonlySignal<boolean>;
82
+ readonly submitted: ReadonlySignal<boolean>;
83
+ readonly status: ReadonlySignal<SubmitStatus>;
84
+ readonly submitError: ReadonlySignal<unknown>;
85
+ /** Access the reactive state + setters for one field. */
86
+ field<K extends keyof T & string>(name: K): Field;
87
+ /** Merge a partial set of values in (does not mark fields touched). */
88
+ setValues(partial: Partial<T>): void;
89
+ /** Validate, mark all fields touched, then run `onSubmit` if valid. */
90
+ submit(): Promise<void>;
91
+ /** Restore initial values and clear errors/touched/dirty/submission state. */
92
+ reset(): void;
93
+ /** Tear down all field subscriptions and derived signals. */
94
+ dispose(): void;
95
+ }
96
+ declare function createForm<T extends FormValues>(config: FormConfig<T>): Form<T>;
97
+
98
+ export { type Field, type Form, type FormConfig, type FormValidators, type FormValues, type SubmitStatus, type Validator, createForm, email, maxLength, minLength, pattern, required, runValidators };
@@ -0,0 +1,98 @@
1
+ import { Signal, ReadonlySignal } from '@streetui/state';
2
+
3
+ /**
4
+ * A small, sync validation system.
5
+ *
6
+ * A `Validator` maps a string field value to an error message, or `undefined`
7
+ * when the value is acceptable. This is deliberately tiny — the built-ins cover
8
+ * the common cases (`required`, `minLength`, `maxLength`, `email`, `pattern`)
9
+ * and anything else is just a plain function `(value: string) => string | undefined`.
10
+ *
11
+ * Validators for a field run in order and the FIRST error wins, so list
12
+ * `required` first if a field is mandatory.
13
+ *
14
+ * Async validation is intentionally NOT part of this core. It can be layered on
15
+ * top with `@streetui/state`'s `resource()` (kick off a resource on value
16
+ * change and surface `resource.error` alongside the field error) without
17
+ * destabilising the synchronous validity model here.
18
+ */
19
+ type Validator = (value: string) => string | undefined;
20
+ /** Fails when the trimmed value is empty. */
21
+ declare function required(message?: string): Validator;
22
+ /** Fails when the value is shorter than `length` characters. */
23
+ declare function minLength(length: number, message?: string): Validator;
24
+ /** Fails when the value is longer than `length` characters. */
25
+ declare function maxLength(length: number, message?: string): Validator;
26
+ /** Fails when a non-empty value is not a plausible email address. */
27
+ declare function email(message?: string): Validator;
28
+ /** Fails when a non-empty value does not match `regex`. */
29
+ declare function pattern(regex: RegExp, message?: string): Validator;
30
+ /** Run a validator (or ordered list) and return the first error, if any. */
31
+ declare function runValidators(value: string, validators: Validator | ReadonlyArray<Validator> | undefined): string | undefined;
32
+
33
+ /**
34
+ * Reactive form model built entirely on `@streetui/state` signals.
35
+ *
36
+ * There is no second state system here: every piece of form state (`values`,
37
+ * `errors`, `touched`, `dirty`, `valid`, submission status) is a signal or a
38
+ * derived signal, so it composes with the renderer's existing reactive bindings.
39
+ * A field's `value` is a writable `Signal<string>`, which plugs straight into
40
+ * the DSL's `input({ bind })` — typing updates form state and programmatic
41
+ * updates update the input, through the one binding the renderer already wires.
42
+ */
43
+
44
+ /** Form values are a flat, typed record of string fields (HTML input values). */
45
+ type FormValues = Record<string, string>;
46
+ interface Field {
47
+ readonly name: string;
48
+ /** Writable value signal — pass to `input({ bind: field.value })`. */
49
+ readonly value: Signal<string>;
50
+ /** Current validation error, or `undefined` when the field is valid. */
51
+ readonly error: ReadonlySignal<string | undefined>;
52
+ /** True once the field has received a genuine user interaction. */
53
+ readonly touched: ReadonlySignal<boolean>;
54
+ /** True when the value differs from its initial value. */
55
+ readonly dirty: ReadonlySignal<boolean>;
56
+ /** True when the field has no validation error. */
57
+ readonly valid: ReadonlySignal<boolean>;
58
+ /** Programmatically set the value (does not mark the field touched). */
59
+ setValue(next: string): void;
60
+ /** Force the touched flag (defaults to true). */
61
+ markTouched(touched?: boolean): void;
62
+ /** Restore this field's initial value and clear its touched flag. */
63
+ reset(): void;
64
+ }
65
+ type FormValidators<T extends FormValues> = {
66
+ readonly [K in keyof T]?: Validator | ReadonlyArray<Validator>;
67
+ };
68
+ interface FormConfig<T extends FormValues> {
69
+ readonly initialValues: T;
70
+ readonly validators?: FormValidators<T>;
71
+ /** Called by `submit()` once all fields are valid. May be async. */
72
+ readonly onSubmit?: (values: T) => void | Promise<void>;
73
+ }
74
+ type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';
75
+ interface Form<T extends FormValues> {
76
+ readonly values: ReadonlySignal<T>;
77
+ readonly errors: ReadonlySignal<Partial<Record<keyof T, string>>>;
78
+ readonly touched: ReadonlySignal<Partial<Record<keyof T, boolean>>>;
79
+ readonly dirty: ReadonlySignal<boolean>;
80
+ readonly valid: ReadonlySignal<boolean>;
81
+ readonly submitting: ReadonlySignal<boolean>;
82
+ readonly submitted: ReadonlySignal<boolean>;
83
+ readonly status: ReadonlySignal<SubmitStatus>;
84
+ readonly submitError: ReadonlySignal<unknown>;
85
+ /** Access the reactive state + setters for one field. */
86
+ field<K extends keyof T & string>(name: K): Field;
87
+ /** Merge a partial set of values in (does not mark fields touched). */
88
+ setValues(partial: Partial<T>): void;
89
+ /** Validate, mark all fields touched, then run `onSubmit` if valid. */
90
+ submit(): Promise<void>;
91
+ /** Restore initial values and clear errors/touched/dirty/submission state. */
92
+ reset(): void;
93
+ /** Tear down all field subscriptions and derived signals. */
94
+ dispose(): void;
95
+ }
96
+ declare function createForm<T extends FormValues>(config: FormConfig<T>): Form<T>;
97
+
98
+ export { type Field, type Form, type FormConfig, type FormValidators, type FormValues, type SubmitStatus, type Validator, createForm, email, maxLength, minLength, pattern, required, runValidators };
package/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ // src/validators.ts
2
+ function required(message = "This field is required") {
3
+ return (value) => value.trim().length === 0 ? message : void 0;
4
+ }
5
+ function minLength(length, message) {
6
+ return (value) => value.length < length ? message ?? `Must be at least ${length} characters` : void 0;
7
+ }
8
+ function maxLength(length, message) {
9
+ return (value) => value.length > length ? message ?? `Must be at most ${length} characters` : void 0;
10
+ }
11
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
+ function email(message = "Enter a valid email address") {
13
+ return (value) => value.length === 0 || EMAIL_RE.test(value) ? void 0 : message;
14
+ }
15
+ function pattern(regex, message = "Invalid format") {
16
+ return (value) => value.length === 0 || regex.test(value) ? void 0 : message;
17
+ }
18
+ function runValidators(value, validators) {
19
+ if (validators === void 0) return void 0;
20
+ const list = Array.isArray(validators) ? validators : [validators];
21
+ for (const validate of list) {
22
+ const error = validate(value);
23
+ if (error !== void 0) return error;
24
+ }
25
+ return void 0;
26
+ }
27
+
28
+ // src/form.ts
29
+ import {
30
+ signal,
31
+ derived,
32
+ batch
33
+ } from "@streetui/state";
34
+ function createForm(config) {
35
+ const names = Object.keys(config.initialValues);
36
+ const validators = config.validators ?? {};
37
+ let programmatic = false;
38
+ const fields = /* @__PURE__ */ new Map();
39
+ for (const name of names) {
40
+ const initial = config.initialValues[name];
41
+ const value = signal(initial);
42
+ const touched = signal(false);
43
+ const error = derived(
44
+ () => runValidators(value.get(), validators[name])
45
+ );
46
+ const valid2 = derived(() => error.get() === void 0);
47
+ const dirty2 = derived(() => value.get() !== initial);
48
+ const unsub = value.subscribe(() => {
49
+ if (!programmatic) touched.set(true);
50
+ });
51
+ const api = {
52
+ name,
53
+ value,
54
+ error,
55
+ touched,
56
+ dirty: dirty2,
57
+ valid: valid2,
58
+ setValue(next) {
59
+ value.set(next);
60
+ },
61
+ markTouched(next = true) {
62
+ touched.set(next);
63
+ },
64
+ reset() {
65
+ programmatic = true;
66
+ try {
67
+ batch(() => {
68
+ value.set(initial);
69
+ touched.set(false);
70
+ });
71
+ } finally {
72
+ programmatic = false;
73
+ }
74
+ }
75
+ };
76
+ fields.set(name, { api, value, touched, error, valid: valid2, dirty: dirty2, initial, unsub });
77
+ }
78
+ const field = (name) => {
79
+ const f = fields.get(name);
80
+ if (f === void 0) throw new Error(`Unknown form field: ${name}`);
81
+ return f;
82
+ };
83
+ const values = derived(() => {
84
+ const out = {};
85
+ for (const name of names) out[name] = field(name).value.get();
86
+ return out;
87
+ });
88
+ const errors = derived(() => {
89
+ const out = {};
90
+ for (const name of names) {
91
+ const e = field(name).error.get();
92
+ if (e !== void 0) out[name] = e;
93
+ }
94
+ return out;
95
+ });
96
+ const touchedMap = derived(() => {
97
+ const out = {};
98
+ for (const name of names) out[name] = field(name).touched.get();
99
+ return out;
100
+ });
101
+ const dirty = derived(() => names.some((n) => field(n).dirty.get()));
102
+ const valid = derived(() => names.every((n) => field(n).valid.get()));
103
+ const status = signal("idle");
104
+ const submitting = derived(() => status.get() === "submitting");
105
+ const submitted = derived(() => status.get() === "success");
106
+ const submitError = signal(void 0);
107
+ function setValues(partial) {
108
+ programmatic = true;
109
+ try {
110
+ batch(() => {
111
+ for (const name of names) {
112
+ const next = partial[name];
113
+ if (next !== void 0) field(name).value.set(next);
114
+ }
115
+ });
116
+ } finally {
117
+ programmatic = false;
118
+ }
119
+ }
120
+ function reset() {
121
+ programmatic = true;
122
+ try {
123
+ batch(() => {
124
+ for (const name of names) {
125
+ const f = field(name);
126
+ f.value.set(f.initial);
127
+ f.touched.set(false);
128
+ }
129
+ status.set("idle");
130
+ submitError.set(void 0);
131
+ });
132
+ } finally {
133
+ programmatic = false;
134
+ }
135
+ }
136
+ async function submit() {
137
+ batch(() => {
138
+ for (const name of names) field(name).touched.set(true);
139
+ });
140
+ if (!valid.peek()) {
141
+ return;
142
+ }
143
+ submitError.set(void 0);
144
+ status.set("submitting");
145
+ try {
146
+ await config.onSubmit?.(values.peek());
147
+ status.set("success");
148
+ } catch (err) {
149
+ submitError.set(err);
150
+ status.set("error");
151
+ }
152
+ }
153
+ function dispose() {
154
+ for (const f of fields.values()) {
155
+ f.unsub();
156
+ f.error.dispose();
157
+ f.valid.dispose();
158
+ f.dirty.dispose();
159
+ }
160
+ values.dispose();
161
+ errors.dispose();
162
+ touchedMap.dispose();
163
+ dirty.dispose();
164
+ valid.dispose();
165
+ submitting.dispose();
166
+ submitted.dispose();
167
+ }
168
+ return {
169
+ values,
170
+ errors,
171
+ touched: touchedMap,
172
+ dirty,
173
+ valid,
174
+ submitting,
175
+ submitted,
176
+ status,
177
+ submitError,
178
+ field: (name) => field(name).api,
179
+ setValues,
180
+ submit,
181
+ reset,
182
+ dispose
183
+ };
184
+ }
185
+ export {
186
+ createForm,
187
+ email,
188
+ maxLength,
189
+ minLength,
190
+ pattern,
191
+ required,
192
+ runValidators
193
+ };
194
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/validators.ts","../src/form.ts"],"sourcesContent":["/**\n * A small, sync validation system.\n *\n * A `Validator` maps a string field value to an error message, or `undefined`\n * when the value is acceptable. This is deliberately tiny — the built-ins cover\n * the common cases (`required`, `minLength`, `maxLength`, `email`, `pattern`)\n * and anything else is just a plain function `(value: string) => string | undefined`.\n *\n * Validators for a field run in order and the FIRST error wins, so list\n * `required` first if a field is mandatory.\n *\n * Async validation is intentionally NOT part of this core. It can be layered on\n * top with `@streetui/state`'s `resource()` (kick off a resource on value\n * change and surface `resource.error` alongside the field error) without\n * destabilising the synchronous validity model here.\n */\n\nexport type Validator = (value: string) => string | undefined;\n\n/** Fails when the trimmed value is empty. */\nexport function required(message = 'This field is required'): Validator {\n return (value) => (value.trim().length === 0 ? message : undefined);\n}\n\n/** Fails when the value is shorter than `length` characters. */\nexport function minLength(length: number, message?: string): Validator {\n return (value) =>\n value.length < length\n ? (message ?? `Must be at least ${length} characters`)\n : undefined;\n}\n\n/** Fails when the value is longer than `length` characters. */\nexport function maxLength(length: number, message?: string): Validator {\n return (value) =>\n value.length > length\n ? (message ?? `Must be at most ${length} characters`)\n : undefined;\n}\n\n// Pragmatic, dependency-free email shape check (not a full RFC 5322 parser).\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** Fails when a non-empty value is not a plausible email address. */\nexport function email(message = 'Enter a valid email address'): Validator {\n return (value) => (value.length === 0 || EMAIL_RE.test(value) ? undefined : message);\n}\n\n/** Fails when a non-empty value does not match `regex`. */\nexport function pattern(regex: RegExp, message = 'Invalid format'): Validator {\n return (value) => (value.length === 0 || regex.test(value) ? undefined : message);\n}\n\n/** Run a validator (or ordered list) and return the first error, if any. */\nexport function runValidators(\n value: string,\n validators: Validator | ReadonlyArray<Validator> | undefined,\n): string | undefined {\n if (validators === undefined) return undefined;\n const list = Array.isArray(validators) ? validators : [validators];\n for (const validate of list) {\n const error = validate(value);\n if (error !== undefined) return error;\n }\n return undefined;\n}\n","/**\n * Reactive form model built entirely on `@streetui/state` signals.\n *\n * There is no second state system here: every piece of form state (`values`,\n * `errors`, `touched`, `dirty`, `valid`, submission status) is a signal or a\n * derived signal, so it composes with the renderer's existing reactive bindings.\n * A field's `value` is a writable `Signal<string>`, which plugs straight into\n * the DSL's `input({ bind })` — typing updates form state and programmatic\n * updates update the input, through the one binding the renderer already wires.\n */\n\nimport {\n signal,\n derived,\n batch,\n DerivedSignal,\n type Signal,\n type ReadonlySignal,\n type Unsubscribe,\n} from '@streetui/state';\nimport { type Validator, runValidators } from './validators.js';\n\n/** Form values are a flat, typed record of string fields (HTML input values). */\nexport type FormValues = Record<string, string>;\n\nexport interface Field {\n readonly name: string;\n /** Writable value signal — pass to `input({ bind: field.value })`. */\n readonly value: Signal<string>;\n /** Current validation error, or `undefined` when the field is valid. */\n readonly error: ReadonlySignal<string | undefined>;\n /** True once the field has received a genuine user interaction. */\n readonly touched: ReadonlySignal<boolean>;\n /** True when the value differs from its initial value. */\n readonly dirty: ReadonlySignal<boolean>;\n /** True when the field has no validation error. */\n readonly valid: ReadonlySignal<boolean>;\n /** Programmatically set the value (does not mark the field touched). */\n setValue(next: string): void;\n /** Force the touched flag (defaults to true). */\n markTouched(touched?: boolean): void;\n /** Restore this field's initial value and clear its touched flag. */\n reset(): void;\n}\n\nexport type FormValidators<T extends FormValues> = {\n readonly [K in keyof T]?: Validator | ReadonlyArray<Validator>;\n};\n\nexport interface FormConfig<T extends FormValues> {\n readonly initialValues: T;\n readonly validators?: FormValidators<T>;\n /** Called by `submit()` once all fields are valid. May be async. */\n readonly onSubmit?: (values: T) => void | Promise<void>;\n}\n\nexport type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error';\n\nexport interface Form<T extends FormValues> {\n readonly values: ReadonlySignal<T>;\n readonly errors: ReadonlySignal<Partial<Record<keyof T, string>>>;\n readonly touched: ReadonlySignal<Partial<Record<keyof T, boolean>>>;\n readonly dirty: ReadonlySignal<boolean>;\n readonly valid: ReadonlySignal<boolean>;\n readonly submitting: ReadonlySignal<boolean>;\n readonly submitted: ReadonlySignal<boolean>;\n readonly status: ReadonlySignal<SubmitStatus>;\n readonly submitError: ReadonlySignal<unknown>;\n /** Access the reactive state + setters for one field. */\n field<K extends keyof T & string>(name: K): Field;\n /** Merge a partial set of values in (does not mark fields touched). */\n setValues(partial: Partial<T>): void;\n /** Validate, mark all fields touched, then run `onSubmit` if valid. */\n submit(): Promise<void>;\n /** Restore initial values and clear errors/touched/dirty/submission state. */\n reset(): void;\n /** Tear down all field subscriptions and derived signals. */\n dispose(): void;\n}\n\ninterface FieldInternal {\n readonly api: Field;\n readonly value: Signal<string>;\n readonly touched: Signal<boolean>;\n readonly error: DerivedSignal<string | undefined>;\n readonly valid: DerivedSignal<boolean>;\n readonly dirty: DerivedSignal<boolean>;\n readonly initial: string;\n readonly unsub: Unsubscribe;\n}\n\n// __FORM_IMPL__\n\nexport function createForm<T extends FormValues>(config: FormConfig<T>): Form<T> {\n const names = Object.keys(config.initialValues) as Array<keyof T & string>;\n const validators = config.validators ?? ({} as FormValidators<T>);\n\n // While true, value changes originate from setValues()/reset() and must NOT\n // mark a field touched. Batch flushes run synchronously inside batch(), i.e.\n // before this flag is reset, so guarding around batch() is sound.\n let programmatic = false;\n\n const fields = new Map<string, FieldInternal>();\n\n for (const name of names) {\n // Keys come from Object.keys(initialValues), so the value is always present;\n // the annotation defeats noUncheckedIndexedAccess widening to `| undefined`.\n const initial: string = config.initialValues[name] as string;\n const value = signal<string>(initial);\n const touched = signal<boolean>(false);\n const error = derived<string | undefined>(() =>\n runValidators(value.get(), validators[name]),\n );\n const valid = derived<boolean>(() => error.get() === undefined);\n const dirty = derived<boolean>(() => value.get() !== initial);\n\n // Mark touched on the first genuine (non-programmatic) value change.\n const unsub = value.subscribe(() => {\n if (!programmatic) touched.set(true);\n });\n\n const api: Field = {\n name,\n value,\n error,\n touched,\n dirty,\n valid,\n setValue(next: string): void {\n value.set(next);\n },\n markTouched(next = true): void {\n touched.set(next);\n },\n reset(): void {\n programmatic = true;\n try {\n batch(() => {\n value.set(initial);\n touched.set(false);\n });\n } finally {\n programmatic = false;\n }\n },\n };\n\n fields.set(name, { api, value, touched, error, valid, dirty, initial, unsub });\n }\n\n const field = (name: string): FieldInternal => {\n const f = fields.get(name);\n if (f === undefined) throw new Error(`Unknown form field: ${name}`);\n return f;\n };\n\n const values = derived<T>(() => {\n const out: Record<string, string> = {};\n for (const name of names) out[name] = field(name).value.get();\n return out as T;\n });\n\n const errors = derived<Partial<Record<keyof T, string>>>(() => {\n const out: Partial<Record<keyof T, string>> = {};\n for (const name of names) {\n const e = field(name).error.get();\n if (e !== undefined) out[name] = e;\n }\n return out;\n });\n\n const touchedMap = derived<Partial<Record<keyof T, boolean>>>(() => {\n const out: Partial<Record<keyof T, boolean>> = {};\n for (const name of names) out[name] = field(name).touched.get();\n return out;\n });\n\n const dirty = derived<boolean>(() => names.some((n) => field(n).dirty.get()));\n const valid = derived<boolean>(() => names.every((n) => field(n).valid.get()));\n\n const status = signal<SubmitStatus>('idle');\n const submitting = derived<boolean>(() => status.get() === 'submitting');\n const submitted = derived<boolean>(() => status.get() === 'success');\n const submitError = signal<unknown>(undefined);\n\n function setValues(partial: Partial<T>): void {\n programmatic = true;\n try {\n batch(() => {\n for (const name of names) {\n const next = partial[name];\n if (next !== undefined) field(name).value.set(next);\n }\n });\n } finally {\n programmatic = false;\n }\n }\n\n function reset(): void {\n programmatic = true;\n try {\n batch(() => {\n for (const name of names) {\n const f = field(name);\n f.value.set(f.initial);\n f.touched.set(false);\n }\n status.set('idle');\n submitError.set(undefined);\n });\n } finally {\n programmatic = false;\n }\n }\n\n async function submit(): Promise<void> {\n // Touch every field so validation errors become visible on submit attempts.\n batch(() => {\n for (const name of names) field(name).touched.set(true);\n });\n if (!valid.peek()) {\n // Invalid — do not enter the submitting lifecycle; field errors now show.\n return;\n }\n submitError.set(undefined);\n status.set('submitting');\n try {\n await config.onSubmit?.(values.peek());\n status.set('success');\n } catch (err) {\n submitError.set(err);\n status.set('error');\n }\n }\n\n function dispose(): void {\n for (const f of fields.values()) {\n f.unsub();\n f.error.dispose();\n f.valid.dispose();\n f.dirty.dispose();\n }\n values.dispose();\n errors.dispose();\n touchedMap.dispose();\n dirty.dispose();\n valid.dispose();\n submitting.dispose();\n submitted.dispose();\n }\n\n return {\n values,\n errors,\n touched: touchedMap,\n dirty,\n valid,\n submitting,\n submitted,\n status,\n submitError,\n field: (name) => field(name).api,\n setValues,\n submit,\n reset,\n dispose,\n };\n}\n\n"],"mappings":"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","names":["valid","dirty"]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@streetui/forms",
3
+ "version": "1.0.0",
4
+ "description": "StreetUI forms — reactive form state, validation, and submission lifecycle built on signals",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "vitest run",
25
+ "clean": "rm -rf dist"
26
+ },
27
+ "dependencies": {
28
+ "@streetui/state": "1.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "*",
32
+ "tsup": "*",
33
+ "vitest": "*"
34
+ },
35
+ "license": "MIT",
36
+ "sideEffects": false,
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md",
43
+ "LICENSE"
44
+ ]
45
+ }