valixa 0.0.0-stage → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADVANCED.md +149 -0
- package/API.md +206 -0
- package/LICENSE +21 -0
- package/README.md +29 -3
- package/dist/behaviors.d.ts +20 -0
- package/dist/behaviors.d.ts.map +1 -0
- package/dist/behaviors.js +50 -0
- package/dist/behaviors.js.map +1 -0
- package/dist/blueprint.d.ts +33 -0
- package/dist/blueprint.d.ts.map +1 -0
- package/dist/blueprint.js +133 -0
- package/dist/blueprint.js.map +1 -0
- package/dist/collection.d.ts +72 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +189 -0
- package/dist/collection.js.map +1 -0
- package/dist/data.d.ts +10 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +85 -0
- package/dist/data.js.map +1 -0
- package/dist/events.d.ts +17 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/form.d.ts +125 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +848 -0
- package/dist/form.js.map +1 -0
- package/dist/graph.d.ts +15 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +101 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/locales.d.ts +16 -0
- package/dist/locales.d.ts.map +1 -0
- package/dist/locales.js +27 -0
- package/dist/locales.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +57 -0
- package/dist/paths.js.map +1 -0
- package/dist/rules.d.ts +13 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +69 -0
- package/dist/rules.js.map +1 -0
- package/dist/types.d.ts +185 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -3
- package/src/behaviors.ts +38 -0
- package/src/blueprint.ts +120 -0
- package/src/collection.ts +138 -0
- package/src/data.ts +68 -0
- package/src/events.ts +8 -0
- package/src/form.ts +547 -0
- package/src/graph.ts +78 -0
- package/src/index.ts +13 -0
- package/src/locales.ts +35 -0
- package/src/paths.ts +38 -0
- package/src/rules.ts +56 -0
- package/src/types.ts +125 -0
package/ADVANCED.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Advanced reactive forms
|
|
2
|
+
|
|
3
|
+
These APIs extend the existing `valixa` store. No framework or DOM is imported into core. Existing tuple/definition fields and atomic object/array values retain their meaning.
|
|
4
|
+
|
|
5
|
+
## Dependencies and automatic validation
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const form = createValixa({
|
|
9
|
+
subtotal: [0],
|
|
10
|
+
discount: { initial: 0, dependsOn: ['subtotal'],
|
|
11
|
+
validate: (value, { values }) => value <= Number(values.subtotal) },
|
|
12
|
+
password: ['', 'required | min:8'],
|
|
13
|
+
confirm: ['', 'same:password']
|
|
14
|
+
}, { validation: { mode: 'onChange', debounce: 300, timeout: 5000 } });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`same` infers an edge; explicit `dependsOn` adds edges. Omitted dependencies on a custom validator mean it may read any value; every mutation invalidates that validator and its dependents. `dependsOn: []` explicitly declares independence. Declare **all** values a custom validator reads. Reverse traversal includes transitive dependents and visits each once. Unrelated errors and field snapshot identities remain intact. Error invalidation includes mapped server errors on affected fields.
|
|
18
|
+
|
|
19
|
+
`form.dependencies` exposes frozen `dependencies`, `dependents`, `unknown` and `cycles` indexes. Missing references and cycles containing a new explicit `dependsOn` edge throw during blueprint construction, with an actionable cycle path. Existing inferred-only `same` cycles remain supported: validators compare a frozen snapshot and each affected field is traversed once, without recursive revalidation; diagnostics list those cycles. Unknown-dependency wildcards are not declared graph edges and do not create artificial cycles. Graphs describe validation reads, never derived-value computations.
|
|
20
|
+
|
|
21
|
+
| Mode | Trigger |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| omitted / `manual` | Explicit `validate` and `submit` only |
|
|
24
|
+
| `onSubmit` | Same automatic policy as manual; submit always validates |
|
|
25
|
+
| `onChange` | Changed and dependent fields, after a value mutation |
|
|
26
|
+
| `onBlur` | A field when `setTouched(name, true)`/binding blur is called |
|
|
27
|
+
| `immediate` | Initially, and changed/dependent fields on mutation |
|
|
28
|
+
|
|
29
|
+
Legacy `validateOn: 'change' | 'blur' | 'submit'` remains supported. An explicit new `validation.mode` takes precedence. No trigger is added by default. A no-op value write does not validate. On blur mode, a value change invalidates dependent errors, but waits for an explicit blur/validation trigger to revalidate them.
|
|
30
|
+
|
|
31
|
+
Debounce is independently owned **per field**, including the initial immediate-mode run. `pending` includes scheduled timers and running validation; `validating` means running work. The optional form validator has its own single debounce timer. `validate(name)` bypasses debounce for that field; `validate()` and submit cancel queued field/form timers and validate everything freshly. Automatic and explicit requests for independent fields coexist. Replacing a request for a field aborts its previous owner. Validators within one field remain sequential, after built-in constraints; independent fields execute concurrently. Results ready in the same microtask turn share a snapshot publication.
|
|
32
|
+
|
|
33
|
+
`timeout` is a finite positive duration **per running field/form request**, excluding debounce. Timeout aborts the signal and produces the structured `timeout` error. Omit it for no timeout. Cancellation also settles core validation promises when application validators ignore signals; it cannot undo remote side effects. `cancelValidation()`, reset, disposal and superseding error operations release owned timers. Cancelled/obsolete results return `stale` and cannot commit. `cancelValidation` is idempotent after disposal; other mutations still reject.
|
|
34
|
+
|
|
35
|
+
## Conditional state
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const form = createValixa({ country: [''], city: [''] }, {
|
|
39
|
+
behaviors: { city: {
|
|
40
|
+
dependsOn: ['country'],
|
|
41
|
+
visibleWhen: ({ values }) => Boolean(values.country),
|
|
42
|
+
enabledWhen: ({ values }) => Boolean(values.country),
|
|
43
|
+
requiredWhen: ({ values }) => Boolean(values.country),
|
|
44
|
+
readonlyWhen: ({ values }) => values.country === 'locked',
|
|
45
|
+
validateWhen: ({ values }) => Boolean(values.country)
|
|
46
|
+
} }
|
|
47
|
+
});
|
|
48
|
+
form.getFieldState('city'); // visible, disabled, readonly, required, validationEnabled
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Predicates are synchronous, pure functions over frozen value snapshots. Explicit dependencies limit reevaluation; omitted dependencies are conservative. Dependencies/targets must name existing fields in that store. Behavior dependencies control reevaluation, separately from validator dependency declarations.
|
|
52
|
+
|
|
53
|
+
| State | Values | Validation | Submission |
|
|
54
|
+
| --- | --- | --- | --- |
|
|
55
|
+
| Hidden | Retained | Continues unless `validateWhen` returns false | Included |
|
|
56
|
+
| Disabled | Retained; binding user writes blocked | Continues, matching existing behavior | Included; a globally disabled form cannot submit |
|
|
57
|
+
| Readonly | Retained; binding user writes blocked | Continues | Included |
|
|
58
|
+
| Conditionally required | Retained | Adds the required rule; does not remove an existing static required rule | Included |
|
|
59
|
+
| `validateWhen: false` | Retained | Skips field rules/custom validators; fresh validation clears its field errors | Included |
|
|
60
|
+
|
|
61
|
+
Visibility is application state: use `visible` in React conditional rendering, Vue `v-if`, or Angular `@if`. No adapter removes DOM automatically. Binding flags are projected; core programmatic writes remain allowed. MUI and Angular native controls expose `aria-required`; Vuetify accepts explicit required metadata and otherwise uses core metadata. Applications can choose visual required markers without enabling a second validation owner.
|
|
62
|
+
|
|
63
|
+
`resetWhen` resets a field **to its initial value on a false-to-true transition**, including initial evaluation. It does not repeatedly erase subsequent edits while true. Reset cascades evaluate on a draft, and only converge before publication; nonconvergent transitions throw. Ordinary reset restores initial values and reevaluates all behaviors without enabling automatic validation. It clears errors/touched/submission state; locale and global flags remain. Server errors are literals, survive presentation-only changes, and are invalidated by affected value or validation-eligibility changes. There is no automatic submission exclusion or configurable hidden-field erasure policy; express clearing with `resetWhen` and an appropriate initial value.
|
|
64
|
+
|
|
65
|
+
## Explicit nested schemas and paths
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createValixa, nested } from 'valixa';
|
|
69
|
+
const form = createValixa({
|
|
70
|
+
user: nested({ name: ['', 'required'], email: ['', 'email'] }),
|
|
71
|
+
profile: { initial: { city: 'Cairo' as string } }, // atomic
|
|
72
|
+
tags: { initial: [] as string[] } // atomic
|
|
73
|
+
});
|
|
74
|
+
form.setValue('user.name', 'Ahmed');
|
|
75
|
+
form.setValues({ user: { email: 'a@example.com' } });
|
|
76
|
+
form.state.values.user.name;
|
|
77
|
+
form.field('user.email').state.errors;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Grammar is unambiguous: `[initial, rules?]` or `{initial, ...}` defines an atomic field; **only** `nested(schema)` introduces addressable children. Bare nested objects are rejected. An atomic object cannot be addressed via its internal properties. Nested patches preserve siblings. Values/submission payloads are nested data; compatibility `errors`/`touched` maps use canonical leaf path keys. `nestedErrors`, `nestedTouched`, and `nestedDirty` provide nested projections. `field(name)` is a cached core controller with a current `state` getter, `setValue`, `onBlur` and `subscribe`; its writes have core programmatic semantics.
|
|
81
|
+
|
|
82
|
+
Nested path segments escape `.` as `\.` and `\` as `\\` (double these backslashes inside JavaScript string literals). `escapePath(segment)` and `parsePath(path)` are exported. Existing top-level literal names—including dots/backslashes—retain exact lookup priority. Conflicting canonical paths are rejected. Empty nested segments and `__proto__`, `prototype`, `constructor` segments are rejected. Malformed escapes are rejected by `parsePath`; no expression evaluation or prototype traversal occurs. Atomic values can contain plain data properties without becoming navigable paths. Type inference supports up to eight nested levels beneath a top-level nested container; deeper schemas require a shallower grouping boundary. `FieldName<V>`, `FieldValue<V, K>`, and `ValuesPatch<V>` are shared by framework packages.
|
|
83
|
+
|
|
84
|
+
## Dynamic collections
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { collection, createValixa } from 'valixa';
|
|
88
|
+
const form = createValixa({ addresses: collection({ city: ['', 'required'], street: [''] }) });
|
|
89
|
+
const addresses = form.array('addresses');
|
|
90
|
+
addresses.append({ city: '', street: '' });
|
|
91
|
+
const row = addresses.items[0]!;
|
|
92
|
+
row.form.setValue('city', 'Cairo');
|
|
93
|
+
addresses.prepend({ city: 'Giza', street: '' });
|
|
94
|
+
addresses.move(0, 1);
|
|
95
|
+
// row.key and row.form remain stable across moves.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`collection(itemSchema, initialItems?)` opts into row management. Existing atomic arrays stay atomic. Each row has `{key, form}`: an opaque collection-local monotonic UI key and an independently typed `ValixaForm`. Current position is its array index; **an index is never its identity**. Bind controls to `row.form` and use `row.key` for reconciliation. Nested item schemas and nested collections are supported. `ValueInput<T>` strips schema-only metadata from input values.
|
|
99
|
+
|
|
100
|
+
Operations are `append(item)`, `prepend(item)`, `insert(index, item)`, `remove(index)`, `move(from, to)`, `swap(a, b)`, `replace(items)`, `clear()` and `reset()`. Invalid indexes throw. Replace resets row lifetimes; move/swap preserve each surviving row's values, errors, initial values, touched/dirty state, graph, subscriptions and pending validation. Removed/replaced rows dispose, abort and can never commit into a later row. Core programmatic collection replacement through `setValues` also creates fresh row lifetimes; malformed collection patches are checked before changing form values.
|
|
101
|
+
|
|
102
|
+
`items`, frozen `values`, `getSnapshot()` and `subscribe()` expose collection state. A collection owns its row stores; application view unmounts should unsubscribe, not dispose surviving rows. Parent reset restores the originally configured items with **fresh** keys, clears row state and cancels timers without restarting initial immediate-mode validation. Parent global disabled/readonly and locale changes propagate to rows, including future appended rows. Parent submit touches and validates all rows and submits their ordered nested values. Parent errors aggregate row messages with current `[index]` prefixes; nested projections preserve per-row error/touched/dirty maps. A collection-root `validateWhen: false` skips row validation and excludes row errors from parent validity/projections while retaining each row's independently owned error state. Ordinary row writes schedule their own field closure; parent automatic work targets other dependents and form constraints, avoiding duplicate row requests. Structural changes can validate the complete collection.
|
|
103
|
+
|
|
104
|
+
Graph namespaces are explicit: item dependencies name fields **inside the row**. Parent dependencies name the collection root. Adding/removing rows creates/disposes their row graphs; reordering leaves graphs intact. Use parent `validateForm` (optionally `formDependsOn: ['addresses']`) for cross-row/aggregate constraints. Parent `form.setValue('addresses.0.city', ...)` is deliberately not an index-based field API; use `addresses.items[0].form.setValue('city', ...)`. No wildcard path expression or arbitrary index-identity rewriting is implemented. A parent complete validation becomes stale on a concurrent structural/value edit, even when surviving row work remains independently valid.
|
|
105
|
+
|
|
106
|
+
React: `useValixaField(row.form, 'city')` or `useMuiField`. Vue: `useValixaField` / `useVuetifyField` in a keyed row component. Subscribe to parent state or the collection for structural rendering. Angular: `form.array('addresses')` delegates to core, then `bindValixa(row.form, {id, destroyRef})` supplies **borrowed** signal projections in the keyed row component. Borrowed-view destruction releases projections while parent collection ownership retains the row store.
|
|
107
|
+
|
|
108
|
+
## Form validation and error precedence
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const form = createValixa({ startDate: [''], endDate: [''] }, {
|
|
112
|
+
formDependsOn: ['startDate', 'endDate'],
|
|
113
|
+
validateForm: async (values, { signal }) => {
|
|
114
|
+
// Forward signal to any remote request.
|
|
115
|
+
return values.startDate > values.endDate
|
|
116
|
+
? { endDate: 'End date must follow start date', $form: 'Review the date range' }
|
|
117
|
+
: {};
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The callback returns a field-path map of strings/string arrays; undefined entries are ignored. `$form` supplies form-wide strings in `state.formErrors`, unless `$form` is an actual schema field, in which case it retains normal field meaning. Unknown returned field names become an actionable form-wide validation failure. Exceptions become a generic structured form error without leaking exception contents. Only explicit field paths are accepted; errors inside collection rows belong to row stores. Use a collection-root form error for an aggregate constraint.
|
|
123
|
+
|
|
124
|
+
Full validation/submit runs field and form validation concurrently against frozen values, including form validation when field rules fail. `validate(name)` runs only that field. Automatic form work is one independent request/timer per form, restricted by `formDependsOn`; omitted dependencies mean any value may affect it. A timeout applies to form work as well. Reset/disposal/obsolete values prevent late commits. Precedence is field messages before form-field messages. `setError` cancels active work and supersedes form-field messages on that target; the **next fresh explicit validation** may replace it, preserving existing retry semantics. Form-wide failures contribute to `valid`/`invalid` even when `errors` is empty; render `state.formErrors` in form-level UI.
|
|
125
|
+
|
|
126
|
+
## Localization
|
|
127
|
+
|
|
128
|
+
English is the default. `locale`, `fallbackLocale`, locale-keyed `labels`, and existing rule `messages` configure presentation. `setLocale(locale)` rerenders structured built-in/custom-false/exception/timeout messages without revalidation. `state.errorDetails` retains `code`, `field`, `label`, `param` and `source`; literal custom/server/form-returned messages additionally retain `message`. String validators and server errors are application literals and are not implicitly translated. `messages` overrides rule catalogs in every locale.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { registerLocale } from 'valixa';
|
|
132
|
+
registerLocale('fr', { messages: { required: '{label} est obligatoire.' }, labels: { email: 'Courriel' } });
|
|
133
|
+
const form = createValixa({ email: ['', 'required'] }, {
|
|
134
|
+
locale: 'ar', fallbackLocale: 'en', labels: { ar: { email: 'البريد الإلكتروني' } }
|
|
135
|
+
});
|
|
136
|
+
await form.validate();
|
|
137
|
+
form.state.errors.email; // ['البريد الإلكتروني مطلوب.']
|
|
138
|
+
form.setLocale('fr');
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Catalog fallback order is custom messages → selected locale → fallback locale → English. Label fallback follows selected/fallback locale overrides/catalogs and then the blueprint label. Catalog registration is process-wide configuration; the selected locale is owned by each form, not global user state. Register catalogs during application setup. Document direction is never changed. Applications choose `dir="rtl"`, logical CSS properties, component-library RTL/theme settings and translated literal messages themselves.
|
|
142
|
+
|
|
143
|
+
## Verification and scope
|
|
144
|
+
|
|
145
|
+
`pnpm test:advanced` runs focused core and representative framework tests; `pnpm test:performance` checks deterministic identity/notification contracts. The repository retains strict builds, all original tests, production browser flows and isolated installed-package consumers. `pnpm benchmark` retains its original workloads in `benchmarks/results.json`; `pnpm benchmark:compare` pairs preserved Phase 5 with current core and identical correctness assertions.
|
|
146
|
+
|
|
147
|
+
Phase 5.1 structurally shares immutable branches and reuses unchanged snapshots. Pending/error/locale transitions retain `values` identity; unrelated nested branches, error arrays and field projections remain stable. External values are still copied before becoming owned state, including shallow-frozen application objects. Native cancellation signals are allocated lazily and retain existing abort/stale behavior. Genuinely redundant operations do not create new snapshots or callbacks, while effective field/pending transitions retain subscription notifications.
|
|
148
|
+
|
|
149
|
+
Value-record copying, nested path projection and ordered collection operations can still cost O(form/collection size); active field subscribers require projection checks. Graph selectivity does not imply constant-time whole-form work. Profiling and retained-heap measurements are separate from end-to-end timings, and no cross-library speed claim is made. Auto Forms and DevTools UI are outside this phase.
|
package/API.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Core public API
|
|
2
|
+
|
|
3
|
+
All exports below come from `valixa`. Core performs no rendering and imports
|
|
4
|
+
no framework or DOM APIs. Values must be acyclic plain objects, arrays or primitive
|
|
5
|
+
data; class instances, dates, files, functions, and symbols are rejected as values.
|
|
6
|
+
Use strings for dates until value normalization is implemented.
|
|
7
|
+
|
|
8
|
+
## Definitions and inference
|
|
9
|
+
|
|
10
|
+
`blueprint(input)` returns a reusable, deeply frozen `Blueprint<S>`. Its constructor
|
|
11
|
+
also accepts the same input. `createValixa(inputOrBlueprint, options?)` returns a
|
|
12
|
+
`ValixaForm<InferValues<S>>`. Each instance owns independent values and state.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { blueprint, createValixa } from 'valixa';
|
|
16
|
+
|
|
17
|
+
const signup = blueprint({
|
|
18
|
+
email: ['', 'required | email'],
|
|
19
|
+
age: { initial: 18, rules: ['minValue:18'], label: 'Age' },
|
|
20
|
+
password: ['', 'required | min:8'],
|
|
21
|
+
confirm: ['', 'required | same:password'],
|
|
22
|
+
username: {
|
|
23
|
+
initial: '',
|
|
24
|
+
rules: 'required',
|
|
25
|
+
validate: async (value: string, { signal, values, field }) => {
|
|
26
|
+
// Replace this with your availability check; forward signal to your API.
|
|
27
|
+
return value !== 'reserved' || 'Username is unavailable.';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const form = createValixa(signup, {
|
|
32
|
+
validateOn: 'submit',
|
|
33
|
+
messages: { required: '{label} cannot be empty.' }
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A field is `[initial, rules?]` or `{ initial, rules?, validate?, label?, dependsOn? }`.
|
|
38
|
+
Explicit `nested(schema)` and `collection(itemSchema, initialItems?)` introduce
|
|
39
|
+
addressable children and keyed row stores; existing object/array values remain atomic.
|
|
40
|
+
See [the advanced guide](ADVANCED.md) for their typed grammar and lifecycle.
|
|
41
|
+
`validate` accepts one callback or an array. Callbacks run sequentially within a
|
|
42
|
+
field, concurrently across fields, after built-in rules pass. `true` or `undefined`
|
|
43
|
+
passes; `false` uses the custom message template; a string becomes an error.
|
|
44
|
+
Exceptions become generic validation errors without exposing exception contents.
|
|
45
|
+
Custom validators should be pure and use `context.values` for cross-field reads.
|
|
46
|
+
|
|
47
|
+
Top-level string, number and boolean literals widen to their primitive types.
|
|
48
|
+
Annotate nullable or union initial values when necessary, e.g.
|
|
49
|
+
`initial: null as string | null`. Exported `InferValues`, `DeepReadonly`,
|
|
50
|
+
`FieldName`, `FieldInput`, `FieldDefinition`, `BlueprintInput`, `RuleInput`,
|
|
51
|
+
`Validator`, `ValidationOutcome` and `ValidationContext` describe the boundary.
|
|
52
|
+
Compiled blueprint metadata is readable but is not an extension point.
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
`parseRules(string | readonly string[])` returns readonly `ParsedRule[]`.
|
|
57
|
+
`RuleSyntaxError` identifies invalid syntax, unknown rules and invalid parameters.
|
|
58
|
+
Definitions are checked eagerly, including missing `same` references.
|
|
59
|
+
|
|
60
|
+
Grammar: `identifier (":" parameter)? ("|" identifier (":" parameter)?)*`.
|
|
61
|
+
An empty expression means no rules. Arrays compose pipelines. There is no dynamic
|
|
62
|
+
execution, quoted-string expression language or implicit rule registration.
|
|
63
|
+
|
|
64
|
+
| Rule | Behavior |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `required` | Reject null, undefined, empty string, empty array and false; accept zero |
|
|
67
|
+
| `email` | Basic email shape; does not verify deliverability or full RFC grammar |
|
|
68
|
+
| `url` | Platform URL parser, HTTP(S) protocol and nonempty hostname; no whitespace, host resolution or reachability check |
|
|
69
|
+
| `min:n`, `max:n` | String length (UTF-16 code units) or array length; n is a nonnegative integer |
|
|
70
|
+
| `minValue:n`, `maxValue:n` | Finite numeric values; no implicit string coercion |
|
|
71
|
+
| `same:field` | `Object.is` comparison against another top-level field |
|
|
72
|
+
|
|
73
|
+
Non-required rules skip null, undefined, empty strings and empty arrays. Required
|
|
74
|
+
does not trim whitespace. Normalize values before writing them if needed. Pattern
|
|
75
|
+
checks and custom named rules can be expressed as custom validators in this phase.
|
|
76
|
+
|
|
77
|
+
`defaultMessages` exposes the immutable English message catalog.
|
|
78
|
+
`options.messages` overrides templates by rule name, `custom`, or `exception`;
|
|
79
|
+
`{label}` and `{param}` interpolate. Locale switching and Arabic catalogs are
|
|
80
|
+
deferred. Arbitrary validator strings are displayed as text by future UI bindings,
|
|
81
|
+
never HTML.
|
|
82
|
+
|
|
83
|
+
## State and writes
|
|
84
|
+
|
|
85
|
+
`form.state` and `form.getSnapshot()` expose the same deeply immutable
|
|
86
|
+
`FormState<V>`. Snapshot identity is stable between transitions.
|
|
87
|
+
|
|
88
|
+
| State | Meaning |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `values`, `initialValues` | Current data and original immutable defaults |
|
|
91
|
+
| `errors` | Sparse field-to-message-array map, no field values or exception text |
|
|
92
|
+
| `touched` | Set explicitly, on blur via bindings, or for every field on submit |
|
|
93
|
+
| `dirty`, `pristine` | Deep comparison with original defaults; revert makes pristine |
|
|
94
|
+
| `valid`, `invalid` | Absence/presence of known errors; initial valid is not proof of validation |
|
|
95
|
+
| `validating` | One or more field/form/row requests are running |
|
|
96
|
+
| `pending` | Running work or debounced automatic validation is scheduled |
|
|
97
|
+
| `formErrors`, `errorDetails`, `locale` | Form-wide messages, structured metadata and selected locale |
|
|
98
|
+
| `submitting` | Submission owns validation or an active handler |
|
|
99
|
+
| `submitted` | Most recent submission succeeded; false when a new attempt starts |
|
|
100
|
+
| `submitCount` | Accepted attempts, including invalid or failed attempts |
|
|
101
|
+
| `disabled`, `readonly` | Global flags available to bindings |
|
|
102
|
+
|
|
103
|
+
- `setValue(name, value)` writes a typed field.
|
|
104
|
+
- `setValues(partial)` validates field names and copies data before applying one
|
|
105
|
+
batch. Equal writes do nothing. Changes invalidate the changed/dependent closure;
|
|
106
|
+
custom validators with unknown dependencies retain conservative invalidation.
|
|
107
|
+
- `setTouched(name, touched = true)` changes touched state.
|
|
108
|
+
- `setError(name, messageOrMessages)` applies server/application errors.
|
|
109
|
+
- `clearErrors(name?)` clears one field or all fields. Error writes invalidate
|
|
110
|
+
pending validation, protecting server errors from older responses.
|
|
111
|
+
- `setDisabled(boolean)`, `setReadonly(boolean)` update global metadata. Programmatic
|
|
112
|
+
writes and explicit validation remain allowed. Disabled forms cannot submit.
|
|
113
|
+
Fields remain in data. Declarative behaviors can add per-field state.
|
|
114
|
+
- `reset()` restores original defaults, clears errors/touched/submitted/count and
|
|
115
|
+
aborts validation/submission. Flags and subscriptions remain.
|
|
116
|
+
- `dispose()` is idempotent, aborts work and clears subscriptions. Subsequent writes,
|
|
117
|
+
subscriptions, validation and submission reject. Final snapshots remain readable.
|
|
118
|
+
|
|
119
|
+
Atomic nested objects/arrays are copied/frozen as before. Explicit nested schemas
|
|
120
|
+
expose leaf paths; dynamic collections expose independently owned row stores.
|
|
121
|
+
|
|
122
|
+
## Subscriptions
|
|
123
|
+
|
|
124
|
+
`subscribe(listener)` returns an idempotent unsubscribe function and fires for
|
|
125
|
+
every state transition. `subscribeField(name, listener)` fires only when that
|
|
126
|
+
field's `FieldState` changes. Neither invokes its callback immediately. Call
|
|
127
|
+
`getSnapshot()` / `getFieldState(name)` to read initial state and read again in the
|
|
128
|
+
callback. `getFieldState` includes `value`, `initialValue`, `errors`, `touched`,
|
|
129
|
+
`dirty`, `validating`, `pending`, `disabled`, `readonly`, `visible`, `required`
|
|
130
|
+
and `validationEnabled`.
|
|
131
|
+
|
|
132
|
+
Unchanged field snapshots retain identity. Notifications are synchronous, with
|
|
133
|
+
`setValues` batching multiple writes; validation has separate pending/result
|
|
134
|
+
transitions. Listeners must not throw. Unsubscribe on consumer teardown, then
|
|
135
|
+
dispose the form when its owner is destroyed. React hooks live in `valixa-react`;
|
|
136
|
+
the core package itself has no framework integration dependencies.
|
|
137
|
+
|
|
138
|
+
## Validation and cancellation
|
|
139
|
+
|
|
140
|
+
`await validate(name?)` returns `ValidationResult<V>`:
|
|
141
|
+
`{ valid, errors, stale }`. With no name it validates all fields; with a name it
|
|
142
|
+
updates only that field's errors, while `valid` still reflects all known errors.
|
|
143
|
+
Unvalidated fields are not implicitly valid. No touched state changes here.
|
|
144
|
+
|
|
145
|
+
The default remains manual/submit. Legacy `validateOn: 'change'` validates the
|
|
146
|
+
changed/dependent closure; `'blur'` validates a field when touched. The additive
|
|
147
|
+
`validation` option supports modes, per-field debounce and running-request timeout.
|
|
148
|
+
|
|
149
|
+
A newer request supersedes previous work for its target fields; independent field
|
|
150
|
+
requests coexist. Value changes invalidate their dependency closure. Reset, error
|
|
151
|
+
operations and disposal cancel owned work. Every callback receives an `AbortSignal`;
|
|
152
|
+
ignored cancellation cannot allow stale commits. A stale result has `valid: false,
|
|
153
|
+
stale: true`. Full validate/submit is fresh and also runs optional form validation.
|
|
154
|
+
See [scheduling, behaviors, nested schemas, collections and localization](ADVANCED.md)
|
|
155
|
+
for complete semantics, error precedence and explicit limitations.
|
|
156
|
+
|
|
157
|
+
## Submission
|
|
158
|
+
|
|
159
|
+
`await submit(handler)` executes immediately. `handleSubmit(handler)` returns an
|
|
160
|
+
event callback accepting an optional structural `{ preventDefault() }` event.
|
|
161
|
+
This avoids overloading an immediate Promise API with callback-factory behavior.
|
|
162
|
+
|
|
163
|
+
`handler(values, { signal })` receives an isolated mutable copy of validated data.
|
|
164
|
+
`SubmitResult<V, R>` is a discriminated union:
|
|
165
|
+
|
|
166
|
+
- `{ status: 'success', data: R }`: handler resolved.
|
|
167
|
+
- `{ status: 'invalid', errors }`: validation failed; handler was not called.
|
|
168
|
+
- `{ status: 'busy' }`: another submission owns the form.
|
|
169
|
+
- `{ status: 'stale' }`: data or validation changed before handler execution.
|
|
170
|
+
- `{ status: 'cancelled' }`: disabled form or submission was cancelled.
|
|
171
|
+
|
|
172
|
+
Handler failures reject the Promise; `submitting` clears in `finally`. The caller
|
|
173
|
+
owns error UI. `cancelSubmit()` signals cancellation and invalidates validation.
|
|
174
|
+
Reset and disposal also cancel. Abort is cooperative: a callback ignoring its
|
|
175
|
+
signal may continue its external side effects. The submission lock remains until
|
|
176
|
+
the pending callback settles, preventing overlapping requests.
|
|
177
|
+
|
|
178
|
+
Edits during the handler are allowed; it keeps the already-validated data copy.
|
|
179
|
+
Success does not reset automatically. Call `reset()` after success if desired.
|
|
180
|
+
Retry validates again, replacing previous server errors. There is no HTTP-client
|
|
181
|
+
dependency or automatic 422 payload guesser. Map your API's known payload explicitly:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
try {
|
|
185
|
+
await form.submit(async data => {
|
|
186
|
+
// const response = await yourApi(data);
|
|
187
|
+
// if (response.status === 422) {
|
|
188
|
+
// form.setError('email', response.errors.email);
|
|
189
|
+
// throw new Error('Server rejected submission');
|
|
190
|
+
// }
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// Show a request-level message; field errors remain in form.state.errors.
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Other exported types: `Errors`, `FormState`, `FieldState`, `ValidationResult`,
|
|
198
|
+
`FormOptions`, `SubmitHandler`, `SubmitContext`, `SubmitResult`.
|
|
199
|
+
|
|
200
|
+
## Readonly descriptions and optional lifecycle observation
|
|
201
|
+
|
|
202
|
+
`form.describe()` returns a lazily cached value-free blueprint description (paths, labels, parsed rules, validator counts, child collection schemas). `form.getBehaviorStates()` returns readonly configured behavior states without creating all field projections.
|
|
203
|
+
|
|
204
|
+
`form.observeEvents(listener, {types?})` returns an unsubscribe function. Immutable events contain per-form sequence/type and optional field/request/reason/index/to/key metadata; no values, messages or handler output. Observer exceptions are isolated. Actual cancelled timers/jobs emit cancellation; independent async work keeps actual resolution order. Invalid validation emits failed then completed; cancelled runs do not commit completion. Root disposal emits then clears observers. Use listeners for diagnostic reads, since reentrant writes can interleave ordinary operations. No event objects are allocated without an interested listener.
|
|
205
|
+
|
|
206
|
+
Optional valixa-auto and valixa-devtools consume these contracts without introducing framework/DOM imports into core. See the repository Phase 6 guide for full event ordering and privacy.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valixa 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,3 +1,29 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# valixa
|
|
2
|
+
|
|
3
|
+
Framework-independent form state, validation, subscriptions and submission.
|
|
4
|
+
ESM and TypeScript declarations, with no runtime dependencies.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import { createValixa } from 'valixa';
|
|
8
|
+
|
|
9
|
+
const form = createValixa({ email: ['', 'required | email'] });
|
|
10
|
+
form.setValue('email', 'user@example.com');
|
|
11
|
+
const result = await form.validate();
|
|
12
|
+
if (result.valid) await form.submit(async data => data.email);
|
|
13
|
+
form.dispose();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Core API: `blueprint`, `createValixa`, `nested`, `collection`, `registerLocale`, `parseRules`, `RuleSyntaxError`,
|
|
17
|
+
`defaultMessages`, `Blueprint`, `ValixaForm`, plus public TypeScript contracts.
|
|
18
|
+
Use `form.state` to read immutable snapshots, `subscribe` / `subscribeField` to
|
|
19
|
+
observe them, and `handleSubmit` for an event callback.
|
|
20
|
+
|
|
21
|
+
Supported rules: required, email, url, min/max length, minValue/maxValue, same.
|
|
22
|
+
Custom and asynchronous validators are supported with abort signals and stale-result
|
|
23
|
+
protection. Values are acyclic plain data. Existing object/array fields stay atomic;
|
|
24
|
+
explicit `nested` and `collection` constructors opt into typed paths and stable rows.
|
|
25
|
+
Dependency graphs, field scheduling, conditional behaviors, form validation and
|
|
26
|
+
English/Arabic localization are included. Framework bindings remain separate packages.
|
|
27
|
+
See [API.md](API.md) and the [advanced guide](ADVANCED.md) for semantics and examples.
|
|
28
|
+
|
|
29
|
+
MIT license. This initial version has not been published.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FieldBehavior } from './types.js';
|
|
2
|
+
export interface BehaviorState {
|
|
3
|
+
readonly visible: boolean;
|
|
4
|
+
readonly enabled: boolean;
|
|
5
|
+
readonly required: boolean;
|
|
6
|
+
readonly readonly: boolean;
|
|
7
|
+
readonly validate: boolean;
|
|
8
|
+
readonly reset: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare class BehaviorEngine {
|
|
11
|
+
private readonly config;
|
|
12
|
+
private readonly inflate;
|
|
13
|
+
private states;
|
|
14
|
+
readonly configured: boolean;
|
|
15
|
+
constructor(config: Readonly<Record<string, FieldBehavior>>, names: readonly string[], inflate?: (flat: Record<string, unknown>) => Record<string, unknown>);
|
|
16
|
+
get(name: string): BehaviorState | undefined;
|
|
17
|
+
/** Evaluate on a draft; commit only after reset transitions converge. */
|
|
18
|
+
evaluate<V extends Record<string, unknown>>(input: V, initial: V, changed?: ReadonlySet<string>, reset?: boolean): V;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=behaviors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"behaviors.d.ts","sourceRoot":"","sources":["../src/behaviors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,aAAa;IAAG,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AACpM,qBAAa,cAAc;IAGb,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAqE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFhI,OAAO,CAAC,MAAM,CAAoC;IAClD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;gBACA,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAmB,OAAO,GAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAkB;IAO7M,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAC5C,yEAAyE;IACzE,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,KAAK,UAAQ,GAAG,CAAC;CAqBnH"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { copy, equal, freeze } from './data.js';
|
|
2
|
+
export class BehaviorEngine {
|
|
3
|
+
config;
|
|
4
|
+
inflate;
|
|
5
|
+
states = new Map();
|
|
6
|
+
configured;
|
|
7
|
+
constructor(config, names, inflate = value => value) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.inflate = inflate;
|
|
10
|
+
this.configured = Object.keys(config).length > 0;
|
|
11
|
+
for (const [name, behavior] of Object.entries(config)) {
|
|
12
|
+
if (!names.includes(name))
|
|
13
|
+
throw new TypeError(`Behavior references unknown field ${name}`);
|
|
14
|
+
for (const dependency of behavior.dependsOn ?? [])
|
|
15
|
+
if (!names.includes(dependency))
|
|
16
|
+
throw new TypeError(`Behavior ${name} depends on unknown field ${dependency}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
get(name) { return this.states.get(name); }
|
|
20
|
+
/** Evaluate on a draft; commit only after reset transitions converge. */
|
|
21
|
+
evaluate(input, initial, changed, reset = false) {
|
|
22
|
+
if (!this.configured)
|
|
23
|
+
return input;
|
|
24
|
+
const draft = { ...input }, next = reset ? new Map() : new Map(this.states);
|
|
25
|
+
let affected = changed;
|
|
26
|
+
for (let pass = 0; pass <= Object.keys(this.config).length + 1; pass++) {
|
|
27
|
+
const context = { values: freeze(this.inflate(draft)) };
|
|
28
|
+
const resets = [];
|
|
29
|
+
for (const [name, behavior] of Object.entries(this.config)) {
|
|
30
|
+
if (affected && behavior.dependsOn !== undefined && !behavior.dependsOn.some(dependency => affected.has(dependency)) && next.has(name))
|
|
31
|
+
continue;
|
|
32
|
+
const state = { visible: behavior.visibleWhen?.(context) ?? true, enabled: behavior.enabledWhen?.(context) ?? true,
|
|
33
|
+
required: behavior.requiredWhen?.(context) ?? false, readonly: behavior.readonlyWhen?.(context) ?? false,
|
|
34
|
+
validate: behavior.validateWhen?.(context) ?? true, reset: behavior.resetWhen?.(context) ?? false };
|
|
35
|
+
if (state.reset && !next.get(name)?.reset && !equal(draft[name], initial[name]))
|
|
36
|
+
resets.push(name);
|
|
37
|
+
next.set(name, Object.freeze(state));
|
|
38
|
+
}
|
|
39
|
+
if (!resets.length) {
|
|
40
|
+
this.states = next;
|
|
41
|
+
return draft;
|
|
42
|
+
}
|
|
43
|
+
for (const name of resets)
|
|
44
|
+
draft[name] = copy(initial[name]);
|
|
45
|
+
affected = new Set(resets);
|
|
46
|
+
}
|
|
47
|
+
throw new TypeError('Conditional reset behaviors did not converge; remove cyclic resetWhen transitions');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=behaviors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"behaviors.js","sourceRoot":"","sources":["../src/behaviors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAIhD,MAAM,OAAO,cAAc;IAGI;IAA4F;IAFjH,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;IACzC,UAAU,CAAU;IAC7B,YAA6B,MAA+C,EAAE,KAAwB,EAAmB,UAAsE,KAAK,CAAC,EAAE,CAAC,KAAK;QAAhL,WAAM,GAAN,MAAM,CAAyC;QAA6C,YAAO,GAAP,OAAO,CAA6E;QAC3M,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QACjD,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACtD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,MAAM,IAAI,SAAS,CAAC,qCAAqC,IAAI,EAAE,CAAC,CAAC;YAC5F,KAAK,MAAM,UAAU,IAAI,QAAQ,CAAC,SAAS,IAAI,EAAE;gBAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC;oBAAE,MAAM,IAAI,SAAS,CAAC,YAAY,IAAI,6BAA6B,UAAU,EAAE,CAAC,CAAC;QACrK,CAAC;IACH,CAAC;IACD,GAAG,CAAC,IAAY,IAA+B,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9E,yEAAyE;IACzE,QAAQ,CAAoC,KAAQ,EAAE,OAAU,EAAE,OAA6B,EAAE,KAAK,GAAG,KAAK;QAC5G,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAC;QACnC,MAAM,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,EAAE,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAyB,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnG,IAAI,QAAQ,GAAG,OAAO,CAAC;QACvB,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;YACvE,MAAM,OAAO,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACxD,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3D,IAAI,QAAQ,IAAI,QAAQ,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,QAAS,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,SAAS;gBAClJ,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI;oBAChH,QAAQ,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK;oBACxG,QAAQ,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtG,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;oBAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACnG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACvC,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;gBAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;gBAAC,OAAO,KAAK,CAAC;YAAC,CAAC;YACzD,KAAK,MAAM,IAAI,IAAI,MAAM;gBAAG,KAAiC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YAC1F,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC7B,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,mFAAmF,CAAC,CAAC;IAC3G,CAAC;CACF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DependencyGraph } from './graph.js';
|
|
2
|
+
import type { ParsedRule } from './rules.js';
|
|
3
|
+
import type { BlueprintInput, CollectionSchema, FormDescription, InferValues, NestedSchema, Validator, ValueInput } from './types.js';
|
|
4
|
+
export interface CompiledField {
|
|
5
|
+
readonly initial: unknown;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly rules: readonly ParsedRule[];
|
|
8
|
+
readonly validators: readonly Validator[];
|
|
9
|
+
readonly dependsOn?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/** Immutable reusable definition. Each createValixa call owns independent state. */
|
|
12
|
+
export declare class Blueprint<S extends BlueprintInput> {
|
|
13
|
+
readonly __values: InferValues<S>;
|
|
14
|
+
readonly fields: Readonly<Record<string, CompiledField>>;
|
|
15
|
+
readonly graph: DependencyGraph;
|
|
16
|
+
readonly paths: Readonly<Record<string, readonly string[]>>;
|
|
17
|
+
readonly collections: Readonly<Record<string, Blueprint<BlueprintInput>>>;
|
|
18
|
+
readonly flat: boolean;
|
|
19
|
+
/** Owned frozen defaults can be shared across independent copy-on-write form instances. */
|
|
20
|
+
readonly initialValues: Readonly<Record<string, unknown>>;
|
|
21
|
+
constructor(input: S);
|
|
22
|
+
get description(): FormDescription;
|
|
23
|
+
inflate(flat: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
24
|
+
/** Internal immutable projection: leaves are already owned and deeply frozen. */
|
|
25
|
+
project(flat: Readonly<Record<string, unknown>>, previous?: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
26
|
+
/** Validate collection replacements before allocating row lifetimes or mutating a form. */
|
|
27
|
+
assertCollections(flat: Readonly<Record<string, unknown>>): void;
|
|
28
|
+
flattenPatch(patch: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export declare function blueprint<const S extends BlueprintInput>(input: S): Blueprint<S>;
|
|
31
|
+
export declare function nested<const S extends BlueprintInput>(schema: S): NestedSchema<S>;
|
|
32
|
+
export declare function collection<const S extends BlueprintInput>(schema: S, initial?: readonly ValueInput<InferValues<S>>[]): CollectionSchema<S>;
|
|
33
|
+
//# sourceMappingURL=blueprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blueprint.d.ts","sourceRoot":"","sources":["../src/blueprint.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAmB,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAKvJ,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AACD,oFAAoF;AACpF,qBAAa,SAAS,CAAC,CAAC,SAAS,cAAc;IAC7C,SAAiB,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IAC1C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;IAC1E,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,2FAA2F;IAC3F,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC9C,KAAK,EAAE,CAAC;IAoCpB,IAAI,WAAW,IAAI,eAAe,CAOjC;IACD,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAQzE,iFAAiF;IACjF,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAQvH,2FAA2F;IAC3F,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI;IAYhE,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAehF;AACD,wBAAgB,SAAS,CAAC,KAAK,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAiC;AAClH,wBAAgB,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,cAAc,EAAE,MAAM,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAA6D;AAC/I,wBAAgB,UAAU,CAAC,KAAK,CAAC,CAAC,SAAS,cAAc,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,GAAE,SAAS,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAO,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAA0E"}
|