foorm 0.1.0 → 0.2.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) 2023 foormjs
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 +1,263 @@
1
- # @foormjs/foorm
1
+ # foorm
2
+
3
+ Core form model for building validatable, reactive forms in any JavaScript environment.
4
+
5
+ Forms are more than just inputs on a screen. They have conditional logic, validation rules that depend on other fields, labels that change based on context, and fields that appear or disappear based on user input. `foorm` captures all of this in a single, portable model where every property can be either a static value or a computed function. The same model works on the server for validation and on the client for rendering, with zero framework dependencies.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install foorm
11
+ # or
12
+ pnpm add foorm
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ Define a form model and validate it:
18
+
19
+ ```ts
20
+ import { createFormData, getFormValidator } from 'foorm'
21
+ import type { TFoormModel } from 'foorm'
22
+
23
+ const form: TFoormModel = {
24
+ title: 'Registration',
25
+ submit: { text: 'Create Account' },
26
+ fields: [
27
+ {
28
+ field: 'email',
29
+ type: 'text',
30
+ label: 'Email',
31
+ optional: false,
32
+ disabled: false,
33
+ hidden: false,
34
+ validators: [
35
+ s => !!s.v || 'Email is required',
36
+ s => String(s.v).includes('@') || 'Must be a valid email',
37
+ ],
38
+ },
39
+ {
40
+ field: 'age',
41
+ type: 'number',
42
+ label: 'Age',
43
+ optional: false,
44
+ disabled: false,
45
+ hidden: false,
46
+ validators: [
47
+ s => !!s.v || 'Age is required',
48
+ s => Number(s.v) >= 18 || 'Must be 18 or older',
49
+ ],
50
+ },
51
+ ],
52
+ }
53
+
54
+ // Create initial data from field defaults
55
+ const data = createFormData(form.fields)
56
+ // => { email: undefined, age: undefined }
57
+
58
+ // Validate the entire form
59
+ const validator = getFormValidator(form)
60
+ const result = validator({ email: 'alice@example.com', age: 25 })
61
+ // => { passed: true, errors: {} }
62
+ ```
63
+
64
+ ## Computed Properties
65
+
66
+ The core idea behind foorm is `TComputed<T>` -- any field property can be either a static value or a function that reacts to form state:
67
+
68
+ ```ts
69
+ import type { TFoormField } from 'foorm'
70
+
71
+ const passwordField: TFoormField = {
72
+ field: 'password',
73
+ type: 'password',
74
+ label: 'Password',
75
+ optional: false,
76
+ hidden: false,
77
+
78
+ // Static placeholder
79
+ placeholder: 'Enter a strong password',
80
+
81
+ // Computed: disabled until name is filled
82
+ disabled: scope => !scope.data.name,
83
+
84
+ // Computed: hint changes based on value
85
+ hint: scope =>
86
+ scope.v ? `${8 - String(scope.v).length} more characters needed` : 'At least 8 characters',
87
+
88
+ validators: [
89
+ s => !!s.v || 'Password is required',
90
+ s => String(s.v).length >= 8 || 'At least 8 characters',
91
+ ],
92
+ }
93
+ ```
94
+
95
+ Every computed function receives a `TFoormFnScope` object:
96
+
97
+ ```ts
98
+ interface TFoormFnScope {
99
+ v?: unknown // Current field value
100
+ data: Record<string, unknown> // All form data
101
+ context: Record<string, unknown> // External context (user info, locale, etc.)
102
+ entry?: TFoormFieldEvaluated // Evaluated field metadata
103
+ }
104
+ ```
105
+
106
+ The `context` object is your escape hatch for passing in external data -- user roles, locale strings, API-fetched options -- anything the form needs but doesn't own.
107
+
108
+ ## Using with ATScript
109
+
110
+ While you can build `TFoormModel` objects by hand, the recommended approach is to use `@foormjs/atscript` to define forms declaratively in `.as` files:
111
+
112
+ ```
113
+ @foorm.fn.title '(data) => "Hello, " + (data.name || "stranger")'
114
+ @foorm.submit.text 'Register'
115
+ export interface RegistrationForm {
116
+ @meta.label 'Name'
117
+ @foorm.validate '(v) => !!v || "Name is required"'
118
+ name: string
119
+
120
+ @meta.label 'Country'
121
+ @foorm.options 'United States', 'us'
122
+ @foorm.options 'Canada', 'ca'
123
+ country?: foorm.select
124
+ }
125
+ ```
126
+
127
+ Then convert it to a runtime model:
128
+
129
+ ```ts
130
+ import { createFoorm } from '@foormjs/atscript'
131
+ import { RegistrationForm } from './registration.as'
132
+
133
+ const form = createFoorm(RegistrationForm)
134
+ ```
135
+
136
+ See [`@foormjs/atscript`](../atscript) for the full annotation reference.
137
+
138
+ ## API Reference
139
+
140
+ ### `createFormData(fields)`
141
+
142
+ Creates an initial data object from field definitions. Each field's `value` property becomes the default. Non-data fields (`action`, `paragraph`) are excluded.
143
+
144
+ ```ts
145
+ const data = createFormData(form.fields)
146
+ // => { email: undefined, name: 'Default Name', ... }
147
+ ```
148
+
149
+ ### `getFormValidator(model, context?)`
150
+
151
+ Returns a reusable validator function for the entire form. The validator evaluates computed constraints per field, skips disabled/hidden fields, enforces required checks, then runs custom validators.
152
+
153
+ ```ts
154
+ const validate = getFormValidator(form, { locale: 'en' })
155
+
156
+ const result = validate(formData)
157
+ // => { passed: false, errors: { email: 'Email is required' } }
158
+ ```
159
+
160
+ Validation order per field:
161
+
162
+ 1. Skip if field type is `action` or `paragraph`
163
+ 2. Evaluate `disabled`, `optional`, `hidden` (may be computed)
164
+ 3. Skip if disabled or hidden
165
+ 4. If not optional and value is falsy, return `"Required"` error
166
+ 5. Run custom validators in order, stop on first failure
167
+
168
+ ### `validate(validators, scope)`
169
+
170
+ Validates a single field by running its validators in sequence. Returns on first failure.
171
+
172
+ ```ts
173
+ import { validate } from 'foorm'
174
+
175
+ const result = validate(field.validators, { v: 'test', data, context: {} })
176
+ if (!result.passed) {
177
+ console.log(result.error) // "Too short"
178
+ }
179
+ ```
180
+
181
+ ### `evalComputed(value, scope)`
182
+
183
+ Resolves a `TComputed<T>` value. If it's a function, calls it with the scope. Otherwise returns the static value.
184
+
185
+ ```ts
186
+ import { evalComputed } from 'foorm'
187
+
188
+ evalComputed('Hello', scope) // => 'Hello'
189
+ evalComputed(s => `Hi ${s.data.name}`, scope) // => 'Hi Alice'
190
+ ```
191
+
192
+ ### `supportsAltAction(model, actionName)`
193
+
194
+ Checks if any field in the model declares the given alternate action name.
195
+
196
+ ```ts
197
+ if (supportsAltAction(form, 'save-draft')) {
198
+ // Show "Save Draft" button
199
+ }
200
+ ```
201
+
202
+ ## Types
203
+
204
+ ### `TFoormModel`
205
+
206
+ The complete form model:
207
+
208
+ ```ts
209
+ interface TFoormModel {
210
+ title?: TComputed<string>
211
+ submit: TFoormSubmit
212
+ fields: TFoormField[]
213
+ }
214
+ ```
215
+
216
+ ### `TFoormField`
217
+
218
+ A single field definition. All description and constraint properties support `TComputed<T>`:
219
+
220
+ | Property | Type | Description |
221
+ | -------------- | ---------------------------------------------- | ----------------------------------------------------------------- |
222
+ | `field` | `string` | Field identifier (required) |
223
+ | `type` | `string` | Input type: text, password, number, select, radio, checkbox, etc. |
224
+ | `label` | `TComputed<string>` | Field label |
225
+ | `description` | `TComputed<string>` | Descriptive text below the label |
226
+ | `hint` | `TComputed<string>` | Hint text (shown when no error) |
227
+ | `placeholder` | `TComputed<string>` | Input placeholder |
228
+ | `optional` | `TComputed<boolean>` | Whether the field is optional |
229
+ | `disabled` | `TComputed<boolean>` | Whether the field is disabled |
230
+ | `hidden` | `TComputed<boolean>` | Whether the field is hidden |
231
+ | `classes` | `TComputed<string \| Record<string, boolean>>` | CSS classes |
232
+ | `styles` | `TComputed<string \| Record<string, string>>` | Inline styles |
233
+ | `options` | `TComputed<TFoormEntryOptions[]>` | Options for select/radio fields |
234
+ | `validators` | `Array<(scope) => boolean \| string>` | Validation functions |
235
+ | `component` | `string` | Named component override |
236
+ | `autocomplete` | `string` | HTML autocomplete attribute |
237
+ | `altAction` | `string` | Alternate submit action name |
238
+ | `order` | `number` | Rendering order |
239
+ | `value` | `unknown` | Default value |
240
+ | `maxLength` | `number` | HTML maxlength constraint |
241
+ | `minLength` | `number` | HTML minlength constraint |
242
+ | `min` | `number` | HTML min constraint |
243
+ | `max` | `number` | HTML max constraint |
244
+
245
+ ### `TFoormEntryOptions`
246
+
247
+ Options for select and radio fields. Can be a simple string (used as both key and display label) or an object:
248
+
249
+ ```ts
250
+ type TFoormEntryOptions = string | { key: string; label: string }
251
+ ```
252
+
253
+ ### `TComputed<T>`
254
+
255
+ The union type that powers reactive properties:
256
+
257
+ ```ts
258
+ type TComputed<T> = T | ((scope: TFoormFnScope) => T)
259
+ ```
260
+
261
+ ## License
262
+
263
+ MIT
package/dist/index.cjs CHANGED
@@ -1,246 +1,101 @@
1
1
  'use strict';
2
2
 
3
- var deserializeFn = require('@prostojs/deserialize-fn');
4
- var serializeFn = require('@prostojs/serialize-fn');
5
-
6
- function evalParameter(fn, scope, forField) {
7
- if (typeof fn === 'function') {
8
- if (fn.__deserialized) {
9
- return fn(scope);
10
- }
11
- else {
12
- const args = (forField
13
- ? [scope.v, scope.data, scope.context, scope.entry]
14
- : [scope.data, scope.context, scope.entry]);
15
- return fn(...args);
16
- }
3
+ function evalComputed(value, scope) {
4
+ if (typeof value === 'function') {
5
+ return value(scope);
17
6
  }
18
- return fn;
7
+ return value;
19
8
  }
20
9
 
21
- /* eslint-disable no-inner-declarations */
22
- /* eslint-disable @typescript-eslint/naming-convention */
23
- /* eslint-disable @typescript-eslint/no-explicit-any */
24
- class Foorm {
25
- // private fns!: FNPool<string | boolean, TFoormFnScope>
26
- constructor(opts) {
27
- this.entries = (opts === null || opts === void 0 ? void 0 : opts.entries) || [];
28
- this.submit = opts === null || opts === void 0 ? void 0 : opts.submit;
29
- this.title = (opts === null || opts === void 0 ? void 0 : opts.title) || '';
30
- this.context = (opts === null || opts === void 0 ? void 0 : opts.context) || {};
31
- }
32
- addEntry(entry) {
33
- this.entries.push(entry);
34
- }
35
- setTitle(title) {
36
- this.title = title;
37
- }
38
- setSubmit(submit) {
39
- this.submit = submit;
40
- }
41
- setContext(context) {
42
- this.context = context;
43
- }
44
- getDefinition() {
45
- return {
46
- title: this.title,
47
- submit: this.submit || { text: 'Submit' },
48
- context: this.context,
49
- entries: this.entries,
50
- };
51
- }
52
- normalizeEntry(e) {
53
- return Object.assign(Object.assign({}, e), { name: e.name || e.field, label: (e.label || e.field), type: e.type || 'text' });
54
- }
55
- executable() {
56
- return {
57
- title: this.title || '',
58
- submit: this.submit || { text: 'Submit' },
59
- context: this.context,
60
- entries: this.entries.map(e => this.normalizeEntry(e)),
61
- };
62
- }
63
- createFormData() {
64
- const data = {};
65
- for (const entry of this.entries) {
66
- if (entry.type !== 'action') {
67
- data[entry.field] = (entry.value || undefined);
68
- }
69
- }
70
- return data;
71
- }
72
- prepareValidators(_validators) {
73
- const validators = _validators || [];
74
- validators.unshift((v, _d, _c, entry) => entry.optional || !!v || 'Required');
75
- return validators;
76
- }
77
- supportsAltAction(altAction) {
78
- return !!this.entries.some(e => e.altAction === altAction);
79
- }
80
- getFormValidator() {
81
- const entries = this.executable().entries;
82
- const fields = {};
83
- for (const entry of entries) {
84
- if (entry.field) {
85
- const validators = this.prepareValidators(entry.validators);
86
- fields[entry.field] = {
87
- entry,
88
- validators,
89
- };
90
- fields[entry.field].validators = validators;
91
- }
92
- }
93
- return (data) => {
94
- let passed = true;
95
- const errors = {};
96
- for (const [key, value] of Object.entries(fields)) {
97
- const evalEntry = Object.assign({}, value.entry);
98
- const scope = {
99
- v: data[key],
100
- context: this.context,
101
- entry: {
102
- field: evalEntry.field,
103
- type: evalEntry.type,
104
- component: evalEntry.component,
105
- name: evalEntry.name,
106
- length: evalEntry.length,
107
- },
108
- data: data,
109
- };
110
- if (scope.entry) {
111
- scope.entry.disabled = evalParameter(evalEntry.disabled, scope, true);
112
- scope.entry.optional = evalParameter(evalEntry.optional, scope, true);
113
- scope.entry.hidden = evalParameter(evalEntry.hidden, scope, true);
114
- }
115
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
- const result = validate({
117
- v: data[key],
118
- context: this.context,
119
- validators: value.validators,
120
- entry: scope.entry,
121
- data: data,
122
- });
123
- if (!result.passed) {
124
- passed = false;
125
- if (!errors[key]) {
126
- errors[key] = result.error || 'Wrong value';
127
- }
128
- }
129
- }
130
- return {
131
- passed,
132
- errors,
133
- };
134
- };
135
- }
136
- }
137
- function validate(opts) {
138
- for (const validator of opts.validators || []) {
139
- const result = evalParameter(validator, {
140
- v: opts.v,
141
- context: opts.context,
142
- data: opts.data,
143
- entry: opts.entry,
144
- }, true);
10
+ /**
11
+ * Runs validators for a single field. Returns on first failure.
12
+ */
13
+ function validate(validators, scope) {
14
+ for (const validator of validators) {
15
+ const result = validator(scope);
145
16
  if (result !== true) {
146
17
  return {
147
18
  passed: false,
148
- error: result || 'Wrong value',
19
+ error: typeof result === 'string' ? result : 'Invalid value',
149
20
  };
150
21
  }
151
22
  }
152
23
  return { passed: true };
153
24
  }
154
-
155
- /* eslint-disable @typescript-eslint/naming-convention */
156
- const pool = new deserializeFn.FNPool();
157
- function deserializeForm(form) {
158
- return new Foorm({
159
- title: deserializeComputedFn(form.title),
160
- context: form.context,
161
- submit: deserializeComputedFn(form.submit, true),
162
- entries: form.entries.map(e => (Object.assign(Object.assign({}, e), {
163
- // strings
164
- label: deserializeComputedFn(e.label), description: deserializeComputedFn(e.description), hint: deserializeComputedFn(e.hint), placeholder: deserializeComputedFn(e.placeholder),
165
- // strings || objects
166
- classes: deserializeComputedFn(e.classes, true), styles: deserializeComputedFn(e.styles, true),
167
- // booleans
168
- optional: deserializeComputedFn(e.optional), disabled: deserializeComputedFn(e.disabled), hidden: deserializeComputedFn(e.hidden),
169
- // options
170
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
171
- options: deserializeComputedFn(e.options),
172
- // attrs
173
- attrs: deserializeComputedFn(e.attrs, true),
174
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
175
- value: e.value, validators: (e.validators || []).map(fn => deserializeComputedFn(fn)) }))),
176
- });
177
- }
178
- function deserializeComputedFn(v, asObj) {
179
- if (typeof v === 'object' && typeof v.__fn__ === 'string') {
180
- const fn = pool.getFn(v.__fn__);
181
- fn.__deserialized = true;
182
- return fn;
183
- }
184
- if (typeof v === 'object' && asObj) {
185
- const o = {};
186
- for (const [key, val] of Object.entries(v)) {
187
- o[key] = deserializeComputedFn(val);
25
+ /** Field types that are UI-only elements, excluded from form data and validation. */
26
+ const NON_DATA_TYPES = new Set(['action', 'paragraph']);
27
+ /**
28
+ * Creates initial form data from field default values.
29
+ * Skips non-data field types (action, paragraph).
30
+ */
31
+ function createFormData(fields) {
32
+ var _a;
33
+ const data = {};
34
+ for (const f of fields) {
35
+ if (!NON_DATA_TYPES.has(f.type)) {
36
+ data[f.field] = (_a = f.value) !== null && _a !== void 0 ? _a : undefined;
188
37
  }
189
- return o;
190
38
  }
191
- return v;
39
+ return data;
192
40
  }
193
-
194
- /* eslint-disable @typescript-eslint/naming-convention */
195
- function serializeForm(form, opts) {
196
- const def = form.getDefinition();
197
- return {
198
- title: serializeComputedFn(def.title),
199
- submit: {
200
- text: serializeComputedFn(def.submit.text || 'Submit'),
201
- disabled: serializeComputedFn(def.submit.disabled),
202
- },
203
- context: (opts === null || opts === void 0 ? void 0 : opts.replaceContext) || def.context,
204
- entries: def.entries.map(e => (Object.assign(Object.assign({}, e), {
205
- // strings
206
- label: serializeComputedFnWithVal(e.label), description: serializeComputedFnWithVal(e.description), hint: serializeComputedFnWithVal(e.hint), placeholder: serializeComputedFnWithVal(e.placeholder),
207
- // strings || objects
208
- classes: serializeComputedFnWithVal(e.classes, true), styles: serializeComputedFnWithVal(e.styles, true),
209
- // booleans
210
- optional: serializeComputedFnWithVal(e.optional), disabled: serializeComputedFnWithVal(e.disabled), hidden: serializeComputedFnWithVal(e.hidden),
211
- // options
212
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
213
- options: serializeComputedFnWithVal(e.options),
214
- // attrs
215
- attrs: serializeComputedFnWithVal(e.attrs, true),
216
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
217
- value: (opts === null || opts === void 0 ? void 0 : opts.replaceValues) ? opts.replaceValues[e.field] : e.value, validators: (e.validators || [])
218
- .filter(v => serializeFn.isCleanFn(v))
219
- .map(fn => serializeComputedFnWithVal(fn)) }))),
41
+ /**
42
+ * Returns a validator function for the whole form.
43
+ * Evaluates disabled/hidden/optional per field, skips disabled/hidden,
44
+ * enforces required, then runs custom validators.
45
+ */
46
+ function getFormValidator(model, context) {
47
+ return (data) => {
48
+ let passed = true;
49
+ const errors = {};
50
+ for (const f of model.fields) {
51
+ if (NON_DATA_TYPES.has(f.type)) {
52
+ continue;
53
+ }
54
+ const entry = {
55
+ field: f.field,
56
+ type: f.type,
57
+ component: f.component,
58
+ name: f.name || f.field,
59
+ };
60
+ const scope = {
61
+ v: data[f.field],
62
+ data,
63
+ context: (context !== null && context !== void 0 ? context : {}),
64
+ entry,
65
+ };
66
+ // Resolve computed constraints
67
+ entry.disabled = evalComputed(f.disabled, scope);
68
+ entry.optional = evalComputed(f.optional, scope);
69
+ entry.hidden = evalComputed(f.hidden, scope);
70
+ // Skip disabled and hidden fields
71
+ if (entry.disabled || entry.hidden) {
72
+ continue;
73
+ }
74
+ // Required check
75
+ if (!entry.optional && !data[f.field]) {
76
+ errors[f.field] = 'Required';
77
+ passed = false;
78
+ continue;
79
+ }
80
+ // Custom validators
81
+ const result = validate(f.validators, scope);
82
+ if (!result.passed) {
83
+ errors[f.field] = result.error;
84
+ passed = false;
85
+ }
86
+ }
87
+ return { passed, errors };
220
88
  };
221
89
  }
222
- function serializeComputedFn(fn) {
223
- if (typeof fn === 'function') {
224
- return { __fn__: serializeFn.serializeFn(fn, 'data', 'context') };
225
- }
226
- return fn;
227
- }
228
- function serializeComputedFnWithVal(fn, inObj) {
229
- if (inObj && typeof fn === 'object') {
230
- const o = {};
231
- for (const [key, val] of Object.entries(fn)) {
232
- o[key] = serializeComputedFnWithVal(val);
233
- }
234
- return o;
235
- }
236
- if (typeof fn === 'function') {
237
- return { __fn__: serializeFn.serializeFn(fn, 'v', 'data', 'context', 'entry') };
238
- }
239
- return fn;
90
+ /**
91
+ * Checks if any field in the model declares the given altAction.
92
+ */
93
+ function supportsAltAction(model, altAction) {
94
+ return model.fields.some(f => f.altAction === altAction);
240
95
  }
241
96
 
242
- exports.Foorm = Foorm;
243
- exports.deserializeForm = deserializeForm;
244
- exports.evalParameter = evalParameter;
245
- exports.serializeForm = serializeForm;
97
+ exports.createFormData = createFormData;
98
+ exports.evalComputed = evalComputed;
99
+ exports.getFormValidator = getFormValidator;
100
+ exports.supportsAltAction = supportsAltAction;
246
101
  exports.validate = validate;
package/dist/index.d.ts CHANGED
@@ -1,158 +1,115 @@
1
- type TFoormFnTop<OF, D, C> = (data: D, ctx: C) => OF;
2
- type TFoormFnField<OF, V, D, C> = (v: V, data: D, ctx: C, entry: TFoormEntryEvaluated) => OF;
3
- interface TFoormFnSerializedTop<OF, D, C> {
4
- (ctx: TFoormFnScope<undefined, D, C>): OF;
5
- __deserialized?: boolean;
6
- }
7
- interface TFoormFnSerializedField<OF, V, D, C> {
8
- (ctx: TFoormFnScope<V, D, C>): OF;
9
- __deserialized?: boolean;
10
- }
11
- type TComputed<OF, D, C> = OF | TFoormFnTop<OF, D, C> | TFoormFnSerializedTop<OF, D, C>;
12
- type TComputedWithVal<OF, V, D, C> = OF | TFoormFnField<OF, V, D, C> | TFoormFnSerializedField<OF, V, D, C>;
13
- interface TFoormFnScope<V = string, D = Record<string, unknown>, C = Record<string, unknown>> {
1
+ /**
2
+ * Scope object passed to computed functions.
3
+ * Properties become variables inside compiled function strings:
4
+ * v, data, context, entry
5
+ */
6
+ interface TFoormFnScope<V = unknown, D = Record<string, unknown>, C = Record<string, unknown>> {
14
7
  v?: V;
15
8
  data: D;
16
9
  context: C;
17
- entry?: TFoormEntryEvaluated;
10
+ entry?: TFoormFieldEvaluated;
18
11
  action?: string;
19
12
  }
20
- type TFoormValidatorFn<V, D, C> = Exclude<Exclude<TComputedWithVal<boolean | string, V, D, C>, boolean>, string>;
13
+ /**
14
+ * A value that is either static or a function of the form scope.
15
+ */
16
+ type TComputed<T> = T | ((scope: TFoormFnScope) => T);
17
+ /**
18
+ * Minimal evaluated snapshot of a field — passed to validators and
19
+ * computed functions as `entry`.
20
+ */
21
+ interface TFoormFieldEvaluated {
22
+ field: string;
23
+ type: string;
24
+ component?: string;
25
+ name: string;
26
+ disabled?: boolean;
27
+ optional?: boolean;
28
+ hidden?: boolean;
29
+ }
21
30
  type TFoormEntryOptions = {
22
31
  key: string;
23
32
  label: string;
24
33
  } | string;
25
- interface TFoormEntry<V, D, C, O extends TFoormEntryOptions> {
34
+ /**
35
+ * A single form field definition with static or computed properties.
36
+ */
37
+ interface TFoormField {
26
38
  field: string;
27
- altAction?: string;
28
- label?: TComputedWithVal<string, V, D, C>;
29
- description?: TComputedWithVal<string, V, D, C>;
30
- hint?: TComputedWithVal<string, V, D, C>;
31
- placeholder?: TComputedWithVal<string, V, D, C>;
32
- classes?: TComputedWithVal<string, V, D, C> | Record<string, TComputedWithVal<boolean, V, D, C>>;
33
- styles?: TComputedWithVal<string, V, D, C> | Record<string, TComputedWithVal<string, V, D, C>>;
34
- type?: string;
39
+ type: string;
35
40
  component?: string;
36
41
  autocomplete?: string;
42
+ altAction?: string;
43
+ order?: number;
37
44
  name?: string;
38
- value?: V;
39
- options?: O[] | TComputedWithVal<O[], V, D, C>;
40
- attrs?: Record<string, TComputedWithVal<any, V, D, C>>;
41
- optional?: TComputedWithVal<boolean, V, D, C>;
42
- disabled?: TComputedWithVal<boolean, V, D, C>;
43
- hidden?: TComputedWithVal<boolean, V, D, C>;
44
- length?: number;
45
- validators?: Array<TFoormValidatorFn<V, D, C>>;
46
- }
47
- interface TFoormMetaExecutable<D, C> {
48
- title: TComputed<string, D, C>;
49
- submit: {
50
- text: TComputed<string, D, C>;
51
- disabled?: TComputed<boolean, D, C>;
52
- };
53
- context: C;
54
- entries: Array<TFoormEntry<any, D, C, TFoormEntryOptions>>;
45
+ label: TComputed<string>;
46
+ description?: TComputed<string>;
47
+ hint?: TComputed<string>;
48
+ placeholder?: TComputed<string>;
49
+ optional: TComputed<boolean>;
50
+ disabled: TComputed<boolean>;
51
+ hidden: TComputed<boolean>;
52
+ classes?: TComputed<string | Record<string, boolean>>;
53
+ styles?: TComputed<string | Record<string, string>>;
54
+ options?: TComputed<TFoormEntryOptions[]>;
55
+ attrs?: Record<string, TComputed<unknown>>;
56
+ value?: unknown;
57
+ validators: Array<(scope: TFoormFnScope) => boolean | string>;
58
+ maxLength?: number;
59
+ minLength?: number;
60
+ min?: number;
61
+ max?: number;
55
62
  }
56
- interface TSerializedFn {
57
- __fn__: string;
63
+ /**
64
+ * Submit button configuration.
65
+ */
66
+ interface TFoormSubmit {
67
+ text: TComputed<string>;
68
+ disabled?: TComputed<boolean>;
58
69
  }
59
- interface TFoormSerialized<V = any, O = any> {
60
- title?: string | TSerializedFn;
61
- submit: {
62
- text: string | TSerializedFn;
63
- disabled: boolean | TSerializedFn;
64
- };
65
- context: any;
66
- entries: Array<{
67
- field: string;
68
- altAction?: string;
69
- label?: string | TSerializedFn;
70
- description?: string | TSerializedFn;
71
- hint?: string | TSerializedFn;
72
- placeholder?: string | TSerializedFn;
73
- classes?: string | TSerializedFn | Record<string, TSerializedFn>;
74
- styles?: string | TSerializedFn | Record<string, string | TSerializedFn>;
75
- type?: string;
76
- component?: string;
77
- autocomplete?: string;
78
- name?: string;
79
- value?: V;
80
- options?: O[] | TSerializedFn;
81
- attrs?: Record<string, string | TSerializedFn>;
82
- optional?: TSerializedFn;
83
- disabled?: TSerializedFn;
84
- hidden?: TSerializedFn;
85
- length?: number;
86
- validators?: TSerializedFn[];
87
- }>;
88
- }
89
- interface TFoormEntryEvaluated {
90
- field: string;
91
- type: string;
92
- component?: string;
93
- name: string;
94
- length?: number;
95
- disabled?: boolean;
96
- optional?: boolean;
97
- hidden?: boolean;
70
+ /**
71
+ * Complete form model — produced by createFoorm() in @foormjs/atscript.
72
+ */
73
+ interface TFoormModel {
74
+ title?: TComputed<string>;
75
+ submit: TFoormSubmit;
76
+ fields: TFoormField[];
98
77
  }
99
78
 
100
- interface TFoormOptions<D, C> {
101
- title?: TComputed<string, D, C>;
102
- entries: Array<TFoormEntry<any, D, C, TFoormEntryOptions>>;
103
- submit?: TFoormMetaExecutable<D, C>['submit'];
104
- context?: C;
105
- }
106
- declare class Foorm<D = any, C = any> {
107
- protected entries: Array<TFoormEntry<any, D, C, TFoormEntryOptions>>;
108
- protected submit?: TFoormMetaExecutable<D, C>['submit'];
109
- protected title?: TComputed<string, D, C>;
110
- protected context: C;
111
- constructor(opts?: TFoormOptions<D, C>);
112
- addEntry<V>(entry: TFoormEntry<V, D, C, TFoormEntryOptions>): void;
113
- setTitle(title: string): void;
114
- setSubmit(submit: TFoormMetaExecutable<D, C>['submit']): void;
115
- setContext(context: C): void;
116
- getDefinition(): {
117
- title: TComputed<string, D, C> | undefined;
118
- submit: {
119
- text: TComputed<string, D, C>;
120
- disabled?: TComputed<boolean, D, C> | undefined;
121
- };
122
- context: C;
123
- entries: TFoormEntry<any, D, C, TFoormEntryOptions>[];
124
- };
125
- protected normalizeEntry<V, O extends TFoormEntryOptions>(e: TFoormEntry<V, D, C, O>): TFoormEntry<V, D, C, O> & {
126
- name: string;
127
- label: TComputed<string, D, C>;
128
- type: string;
129
- };
130
- executable(): TFoormMetaExecutable<D, C>;
131
- createFormData<T extends Record<string, unknown>>(): T;
132
- prepareValidators(_validators: TFoormEntry<any, D, C, TFoormEntryOptions>['validators']): TFoormValidatorFn<any, D, C>[];
133
- supportsAltAction(altAction: string): boolean;
134
- getFormValidator(): (inputs: Record<string, unknown>) => {
135
- passed: boolean;
136
- errors: Record<string, string>;
137
- };
138
- }
139
- declare function validate<V, D, C>(opts: TFoormFnScope<V, D, C> & {
140
- validators: TFoormEntry<any, D, C, TFoormEntryOptions>['validators'];
141
- }): {
142
- passed: boolean;
143
- error: string;
79
+ /**
80
+ * Runs validators for a single field. Returns on first failure.
81
+ */
82
+ declare function validate(validators: TFoormField['validators'], scope: TFoormFnScope): {
83
+ passed: true;
144
84
  } | {
85
+ passed: false;
86
+ error: string;
87
+ };
88
+ /**
89
+ * Creates initial form data from field default values.
90
+ * Skips non-data field types (action, paragraph).
91
+ */
92
+ declare function createFormData<T = Record<string, unknown>>(fields: TFoormField[]): T;
93
+ /**
94
+ * Returns a validator function for the whole form.
95
+ * Evaluates disabled/hidden/optional per field, skips disabled/hidden,
96
+ * enforces required, then runs custom validators.
97
+ */
98
+ declare function getFormValidator(model: TFoormModel, context?: unknown): (data: Record<string, unknown>) => {
145
99
  passed: boolean;
146
- error?: undefined;
100
+ errors: Record<string, string>;
147
101
  };
102
+ /**
103
+ * Checks if any field in the model declares the given altAction.
104
+ */
105
+ declare function supportsAltAction(model: TFoormModel, altAction: string): boolean;
148
106
 
149
- declare function deserializeForm<D = unknown, C = unknown>(form: TFoormSerialized): Foorm<D, C>;
150
-
151
- declare function serializeForm<D, C>(form: Foorm<D, C>, opts?: {
152
- replaceContext?: C;
153
- replaceValues?: Record<string, unknown>;
154
- }): TFoormSerialized;
155
-
156
- declare function evalParameter<OF>(fn: TComputed<OF, any, any> | TComputedWithVal<OF, any, any, any>, scope: TFoormFnScope<any, any, any>, forField?: boolean): OF | undefined;
107
+ /**
108
+ * Resolves a TComputed value: if it's a function, calls it with
109
+ * the scope. Otherwise returns the static value as-is.
110
+ */
111
+ declare function evalComputed<T>(value: TComputed<T>, scope: TFoormFnScope): T;
112
+ declare function evalComputed<T>(value: TComputed<T> | undefined, scope: TFoormFnScope): T | undefined;
157
113
 
158
- export { Foorm, type TComputed, type TComputedWithVal, type TFoormEntry, type TFoormEntryEvaluated, type TFoormEntryOptions, type TFoormFnField, type TFoormFnScope, type TFoormFnSerializedField, type TFoormFnSerializedTop, type TFoormFnTop, type TFoormMetaExecutable, type TFoormOptions, type TFoormSerialized, type TFoormValidatorFn, type TSerializedFn, deserializeForm, evalParameter, serializeForm, validate };
114
+ export { createFormData, evalComputed, getFormValidator, supportsAltAction, validate };
115
+ export type { TComputed, TFoormEntryOptions, TFoormField, TFoormFieldEvaluated, TFoormFnScope, TFoormModel, TFoormSubmit };
package/dist/index.mjs CHANGED
@@ -1,240 +1,95 @@
1
- import { FNPool } from '@prostojs/deserialize-fn';
2
- import { isCleanFn, serializeFn } from '@prostojs/serialize-fn';
3
-
4
- function evalParameter(fn, scope, forField) {
5
- if (typeof fn === 'function') {
6
- if (fn.__deserialized) {
7
- return fn(scope);
8
- }
9
- else {
10
- const args = (forField
11
- ? [scope.v, scope.data, scope.context, scope.entry]
12
- : [scope.data, scope.context, scope.entry]);
13
- return fn(...args);
14
- }
1
+ function evalComputed(value, scope) {
2
+ if (typeof value === 'function') {
3
+ return value(scope);
15
4
  }
16
- return fn;
5
+ return value;
17
6
  }
18
7
 
19
- /* eslint-disable no-inner-declarations */
20
- /* eslint-disable @typescript-eslint/naming-convention */
21
- /* eslint-disable @typescript-eslint/no-explicit-any */
22
- class Foorm {
23
- // private fns!: FNPool<string | boolean, TFoormFnScope>
24
- constructor(opts) {
25
- this.entries = (opts === null || opts === void 0 ? void 0 : opts.entries) || [];
26
- this.submit = opts === null || opts === void 0 ? void 0 : opts.submit;
27
- this.title = (opts === null || opts === void 0 ? void 0 : opts.title) || '';
28
- this.context = (opts === null || opts === void 0 ? void 0 : opts.context) || {};
29
- }
30
- addEntry(entry) {
31
- this.entries.push(entry);
32
- }
33
- setTitle(title) {
34
- this.title = title;
35
- }
36
- setSubmit(submit) {
37
- this.submit = submit;
38
- }
39
- setContext(context) {
40
- this.context = context;
41
- }
42
- getDefinition() {
43
- return {
44
- title: this.title,
45
- submit: this.submit || { text: 'Submit' },
46
- context: this.context,
47
- entries: this.entries,
48
- };
49
- }
50
- normalizeEntry(e) {
51
- return Object.assign(Object.assign({}, e), { name: e.name || e.field, label: (e.label || e.field), type: e.type || 'text' });
52
- }
53
- executable() {
54
- return {
55
- title: this.title || '',
56
- submit: this.submit || { text: 'Submit' },
57
- context: this.context,
58
- entries: this.entries.map(e => this.normalizeEntry(e)),
59
- };
60
- }
61
- createFormData() {
62
- const data = {};
63
- for (const entry of this.entries) {
64
- if (entry.type !== 'action') {
65
- data[entry.field] = (entry.value || undefined);
66
- }
67
- }
68
- return data;
69
- }
70
- prepareValidators(_validators) {
71
- const validators = _validators || [];
72
- validators.unshift((v, _d, _c, entry) => entry.optional || !!v || 'Required');
73
- return validators;
74
- }
75
- supportsAltAction(altAction) {
76
- return !!this.entries.some(e => e.altAction === altAction);
77
- }
78
- getFormValidator() {
79
- const entries = this.executable().entries;
80
- const fields = {};
81
- for (const entry of entries) {
82
- if (entry.field) {
83
- const validators = this.prepareValidators(entry.validators);
84
- fields[entry.field] = {
85
- entry,
86
- validators,
87
- };
88
- fields[entry.field].validators = validators;
89
- }
90
- }
91
- return (data) => {
92
- let passed = true;
93
- const errors = {};
94
- for (const [key, value] of Object.entries(fields)) {
95
- const evalEntry = Object.assign({}, value.entry);
96
- const scope = {
97
- v: data[key],
98
- context: this.context,
99
- entry: {
100
- field: evalEntry.field,
101
- type: evalEntry.type,
102
- component: evalEntry.component,
103
- name: evalEntry.name,
104
- length: evalEntry.length,
105
- },
106
- data: data,
107
- };
108
- if (scope.entry) {
109
- scope.entry.disabled = evalParameter(evalEntry.disabled, scope, true);
110
- scope.entry.optional = evalParameter(evalEntry.optional, scope, true);
111
- scope.entry.hidden = evalParameter(evalEntry.hidden, scope, true);
112
- }
113
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
- const result = validate({
115
- v: data[key],
116
- context: this.context,
117
- validators: value.validators,
118
- entry: scope.entry,
119
- data: data,
120
- });
121
- if (!result.passed) {
122
- passed = false;
123
- if (!errors[key]) {
124
- errors[key] = result.error || 'Wrong value';
125
- }
126
- }
127
- }
128
- return {
129
- passed,
130
- errors,
131
- };
132
- };
133
- }
134
- }
135
- function validate(opts) {
136
- for (const validator of opts.validators || []) {
137
- const result = evalParameter(validator, {
138
- v: opts.v,
139
- context: opts.context,
140
- data: opts.data,
141
- entry: opts.entry,
142
- }, true);
8
+ /**
9
+ * Runs validators for a single field. Returns on first failure.
10
+ */
11
+ function validate(validators, scope) {
12
+ for (const validator of validators) {
13
+ const result = validator(scope);
143
14
  if (result !== true) {
144
15
  return {
145
16
  passed: false,
146
- error: result || 'Wrong value',
17
+ error: typeof result === 'string' ? result : 'Invalid value',
147
18
  };
148
19
  }
149
20
  }
150
21
  return { passed: true };
151
22
  }
152
-
153
- /* eslint-disable @typescript-eslint/naming-convention */
154
- const pool = new FNPool();
155
- function deserializeForm(form) {
156
- return new Foorm({
157
- title: deserializeComputedFn(form.title),
158
- context: form.context,
159
- submit: deserializeComputedFn(form.submit, true),
160
- entries: form.entries.map(e => (Object.assign(Object.assign({}, e), {
161
- // strings
162
- label: deserializeComputedFn(e.label), description: deserializeComputedFn(e.description), hint: deserializeComputedFn(e.hint), placeholder: deserializeComputedFn(e.placeholder),
163
- // strings || objects
164
- classes: deserializeComputedFn(e.classes, true), styles: deserializeComputedFn(e.styles, true),
165
- // booleans
166
- optional: deserializeComputedFn(e.optional), disabled: deserializeComputedFn(e.disabled), hidden: deserializeComputedFn(e.hidden),
167
- // options
168
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
169
- options: deserializeComputedFn(e.options),
170
- // attrs
171
- attrs: deserializeComputedFn(e.attrs, true),
172
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
173
- value: e.value, validators: (e.validators || []).map(fn => deserializeComputedFn(fn)) }))),
174
- });
175
- }
176
- function deserializeComputedFn(v, asObj) {
177
- if (typeof v === 'object' && typeof v.__fn__ === 'string') {
178
- const fn = pool.getFn(v.__fn__);
179
- fn.__deserialized = true;
180
- return fn;
181
- }
182
- if (typeof v === 'object' && asObj) {
183
- const o = {};
184
- for (const [key, val] of Object.entries(v)) {
185
- o[key] = deserializeComputedFn(val);
23
+ /** Field types that are UI-only elements, excluded from form data and validation. */
24
+ const NON_DATA_TYPES = new Set(['action', 'paragraph']);
25
+ /**
26
+ * Creates initial form data from field default values.
27
+ * Skips non-data field types (action, paragraph).
28
+ */
29
+ function createFormData(fields) {
30
+ var _a;
31
+ const data = {};
32
+ for (const f of fields) {
33
+ if (!NON_DATA_TYPES.has(f.type)) {
34
+ data[f.field] = (_a = f.value) !== null && _a !== void 0 ? _a : undefined;
186
35
  }
187
- return o;
188
36
  }
189
- return v;
37
+ return data;
190
38
  }
191
-
192
- /* eslint-disable @typescript-eslint/naming-convention */
193
- function serializeForm(form, opts) {
194
- const def = form.getDefinition();
195
- return {
196
- title: serializeComputedFn(def.title),
197
- submit: {
198
- text: serializeComputedFn(def.submit.text || 'Submit'),
199
- disabled: serializeComputedFn(def.submit.disabled),
200
- },
201
- context: (opts === null || opts === void 0 ? void 0 : opts.replaceContext) || def.context,
202
- entries: def.entries.map(e => (Object.assign(Object.assign({}, e), {
203
- // strings
204
- label: serializeComputedFnWithVal(e.label), description: serializeComputedFnWithVal(e.description), hint: serializeComputedFnWithVal(e.hint), placeholder: serializeComputedFnWithVal(e.placeholder),
205
- // strings || objects
206
- classes: serializeComputedFnWithVal(e.classes, true), styles: serializeComputedFnWithVal(e.styles, true),
207
- // booleans
208
- optional: serializeComputedFnWithVal(e.optional), disabled: serializeComputedFnWithVal(e.disabled), hidden: serializeComputedFnWithVal(e.hidden),
209
- // options
210
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
211
- options: serializeComputedFnWithVal(e.options),
212
- // attrs
213
- attrs: serializeComputedFnWithVal(e.attrs, true),
214
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
215
- value: (opts === null || opts === void 0 ? void 0 : opts.replaceValues) ? opts.replaceValues[e.field] : e.value, validators: (e.validators || [])
216
- .filter(v => isCleanFn(v))
217
- .map(fn => serializeComputedFnWithVal(fn)) }))),
39
+ /**
40
+ * Returns a validator function for the whole form.
41
+ * Evaluates disabled/hidden/optional per field, skips disabled/hidden,
42
+ * enforces required, then runs custom validators.
43
+ */
44
+ function getFormValidator(model, context) {
45
+ return (data) => {
46
+ let passed = true;
47
+ const errors = {};
48
+ for (const f of model.fields) {
49
+ if (NON_DATA_TYPES.has(f.type)) {
50
+ continue;
51
+ }
52
+ const entry = {
53
+ field: f.field,
54
+ type: f.type,
55
+ component: f.component,
56
+ name: f.name || f.field,
57
+ };
58
+ const scope = {
59
+ v: data[f.field],
60
+ data,
61
+ context: (context !== null && context !== void 0 ? context : {}),
62
+ entry,
63
+ };
64
+ // Resolve computed constraints
65
+ entry.disabled = evalComputed(f.disabled, scope);
66
+ entry.optional = evalComputed(f.optional, scope);
67
+ entry.hidden = evalComputed(f.hidden, scope);
68
+ // Skip disabled and hidden fields
69
+ if (entry.disabled || entry.hidden) {
70
+ continue;
71
+ }
72
+ // Required check
73
+ if (!entry.optional && !data[f.field]) {
74
+ errors[f.field] = 'Required';
75
+ passed = false;
76
+ continue;
77
+ }
78
+ // Custom validators
79
+ const result = validate(f.validators, scope);
80
+ if (!result.passed) {
81
+ errors[f.field] = result.error;
82
+ passed = false;
83
+ }
84
+ }
85
+ return { passed, errors };
218
86
  };
219
87
  }
220
- function serializeComputedFn(fn) {
221
- if (typeof fn === 'function') {
222
- return { __fn__: serializeFn(fn, 'data', 'context') };
223
- }
224
- return fn;
225
- }
226
- function serializeComputedFnWithVal(fn, inObj) {
227
- if (inObj && typeof fn === 'object') {
228
- const o = {};
229
- for (const [key, val] of Object.entries(fn)) {
230
- o[key] = serializeComputedFnWithVal(val);
231
- }
232
- return o;
233
- }
234
- if (typeof fn === 'function') {
235
- return { __fn__: serializeFn(fn, 'v', 'data', 'context', 'entry') };
236
- }
237
- return fn;
88
+ /**
89
+ * Checks if any field in the model declares the given altAction.
90
+ */
91
+ function supportsAltAction(model, altAction) {
92
+ return model.fields.some(f => f.altAction === altAction);
238
93
  }
239
94
 
240
- export { Foorm, deserializeForm, evalParameter, serializeForm, validate };
95
+ export { createFormData, evalComputed, getFormValidator, supportsAltAction, validate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foorm",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "foorm",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -34,8 +34,7 @@
34
34
  "url": "https://github.com/foormjs/foormjs/issues"
35
35
  },
36
36
  "homepage": "https://github.com/foormjs/foormjs/tree/main/packages/foorm#readme",
37
- "dependencies": {
38
- "@prostojs/serialize-fn": "^0.0.5",
39
- "@prostojs/deserialize-fn": "^0.0.5"
37
+ "scripts": {
38
+ "pub": "pnpm publish --access public --no-git-checks"
40
39
  }
41
- }
40
+ }