foorm 0.0.3 → 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,222 +1,101 @@
1
1
  'use strict';
2
2
 
3
- var ftring$1 = require('@prostojs/ftring');
4
-
5
- function isFtring(input) {
6
- return (typeof input === 'object' &&
7
- input.__is_ftring__ &&
8
- typeof input.v === 'string');
9
- }
10
- function ftring(strings, __type__) {
11
- return {
12
- __is_ftring__: true,
13
- v: strings.join(''),
14
- __type__,
15
- };
16
- }
17
-
18
- class Foorm {
19
- constructor(opts) {
20
- this.entries = (opts === null || opts === void 0 ? void 0 : opts.entries) || [];
21
- this.submit = opts === null || opts === void 0 ? void 0 : opts.submit;
22
- this.title = (opts === null || opts === void 0 ? void 0 : opts.title) || '';
23
- this.context = (opts === null || opts === void 0 ? void 0 : opts.context) || {};
24
- }
25
- addEntry(entry) {
26
- this.entries.push(entry);
27
- }
28
- setTitle(title) {
29
- this.title = title;
30
- }
31
- setSubmit(submit) {
32
- this.submit = submit;
33
- }
34
- setContext(context) {
35
- this.context = context;
36
- }
37
- /**
38
- * Normalizes form metadata and removes all the functions
39
- * from validators.
40
- *
41
- * @param replaceContext a context to be transported along with metadata
42
- * @returns form metadata without functions
43
- */
44
- transportable(replaceContext, replaceValues) {
45
- var _a, _b;
46
- return {
47
- title: (_a = this.title) !== null && _a !== void 0 ? _a : '',
48
- submit: (_b = this.submit) !== null && _b !== void 0 ? _b : { text: 'Submit' },
49
- context: replaceContext || this.context,
50
- entries: this.entries.map(e => (Object.assign(Object.assign({}, e), { value: replaceValues ? replaceValues[e.field] : e.value, validators: (e.validators || []).filter(v => isFtring(v)) }))),
51
- };
52
- }
53
- normalizeEntry(e) {
54
- return Object.assign(Object.assign({}, e), { name: e.name || e.field, label: e.label || e.field, type: e.type || 'text' });
55
- }
56
- /**
57
- * Evaluates all the ftrings into functions, makes it ready for execution
58
- *
59
- * @returns form metadata with functions
60
- */
61
- executable() {
62
- var _a, _b;
63
- if (!this.fns) {
64
- this.fns = new ftring$1.FtringsPool();
65
- }
66
- return {
67
- title: transformFtrings(this.title || '', this.fns),
68
- submit: {
69
- text: transformFtrings(((_a = this.submit) === null || _a === void 0 ? void 0 : _a.text) || 'Submit', this.fns),
70
- disabled: transformFtrings((_b = this.submit) === null || _b === void 0 ? void 0 : _b.disabled, this.fns),
71
- },
72
- context: this.context,
73
- entries: this.entries
74
- .map(e => this.normalizeEntry(e))
75
- .map(e => (Object.assign(Object.assign({}, e), {
76
- // strings
77
- label: transformFtrings(e.label, this.fns), description: transformFtrings(e.description, this.fns), hint: transformFtrings(e.hint, this.fns), placeholder: transformFtrings(e.placeholder, this.fns),
78
- // strings || objects
79
- classes: transformFtringsInObj(e.classes, this.fns), styles: transformFtringsInObj(e.styles, this.fns),
80
- // booleans
81
- optional: transformFtrings(e.optional, this.fns), disabled: transformFtrings(e.disabled, this.fns), hidden: transformFtrings(e.hidden, this.fns), validators: this.prepareValidators(e.validators),
82
- // options
83
- options: transformFtrings(e.options, this.fns),
84
- // attrs
85
- attrs: transformFtringsInObj(e.attrs, this.fns) }))),
86
- };
87
- }
88
- createFormData() {
89
- const data = {};
90
- for (const entry of this.entries) {
91
- if (entry.type !== 'action') {
92
- data[entry.field] = (entry.value || undefined);
93
- }
94
- }
95
- return data;
96
- }
97
- prepareValidators(_validators) {
98
- const validators = (_validators || []).map(v => (isFtring(v) ? this.fns.getFn(v.v) : v));
99
- validators.unshift(this.fns.getFn('entry.optional || !!v || "Required"'));
100
- return validators;
101
- }
102
- supportsAltAction(altAction) {
103
- return !!this.entries.some(e => e.altAction === altAction);
104
- }
105
- getFormValidator() {
106
- if (!this.fns) {
107
- this.fns = new ftring$1.FtringsPool();
108
- }
109
- const entries = this.executable().entries;
110
- const fields = {};
111
- for (const entry of entries) {
112
- if (entry.field) {
113
- fields[entry.field] = {
114
- entry,
115
- validators: this.prepareValidators(entry.validators),
116
- };
117
- }
118
- fields[entry.field].validators.unshift(this.fns.getFn('entry.optional || !!v || "Required"'));
119
- }
120
- return (data) => {
121
- let passed = true;
122
- const errors = {};
123
- for (const [key, value] of Object.entries(fields)) {
124
- const evalEntry = Object.assign({}, value.entry);
125
- const scope = {
126
- v: data[key],
127
- context: this.context,
128
- entry: {
129
- field: evalEntry.field,
130
- type: evalEntry.type,
131
- component: evalEntry.component,
132
- name: evalEntry.name,
133
- length: evalEntry.length,
134
- },
135
- data,
136
- };
137
- if (scope.entry) {
138
- if (typeof evalEntry.disabled === 'function') {
139
- scope.entry.disabled = evalEntry.disabled = evalEntry.disabled(scope);
140
- }
141
- else {
142
- scope.entry.disabled = evalEntry.disabled;
143
- }
144
- if (typeof evalEntry.optional === 'function') {
145
- scope.entry.optional = evalEntry.optional = evalEntry.optional(scope);
146
- }
147
- else {
148
- scope.entry.optional = evalEntry.optional;
149
- }
150
- if (typeof evalEntry.hidden === 'function') {
151
- scope.entry.hidden = evalEntry.hidden = evalEntry.hidden(scope);
152
- }
153
- else {
154
- scope.entry.hidden = evalEntry.hidden;
155
- }
156
- }
157
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
- const result = validate({
159
- v: data[key],
160
- context: this.context,
161
- validators: value.validators,
162
- entry: scope.entry,
163
- data,
164
- });
165
- if (!result.passed) {
166
- passed = false;
167
- if (!errors[key]) {
168
- errors[key] = result.error || 'Wrong value';
169
- }
170
- }
171
- }
172
- return {
173
- passed,
174
- errors,
175
- };
176
- };
3
+ function evalComputed(value, scope) {
4
+ if (typeof value === 'function') {
5
+ return value(scope);
177
6
  }
7
+ return value;
178
8
  }
179
- function validate(opts) {
180
- for (const validator of opts.validators || []) {
181
- const result = validator({
182
- v: opts.v,
183
- context: opts.context,
184
- data: opts.data,
185
- entry: opts.entry,
186
- });
9
+
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);
187
16
  if (result !== true) {
188
17
  return {
189
18
  passed: false,
190
- error: result || 'Wrong value',
19
+ error: typeof result === 'string' ? result : 'Invalid value',
191
20
  };
192
21
  }
193
22
  }
194
23
  return { passed: true };
195
24
  }
196
- function transformFtrings(value, fns) {
197
- if (value === undefined) {
198
- return value;
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;
37
+ }
199
38
  }
200
- return isFtring(value) ? fns.getFn(value.v) : value;
39
+ return data;
201
40
  }
202
- function transformFtringsInObj(value, fns) {
203
- if (isFtring(value)) {
204
- return transformFtrings(value, fns);
205
- }
206
- if (typeof value === 'function') {
207
- return value;
208
- }
209
- if (typeof value === 'object' && value !== null) {
210
- const obj = {};
211
- for (const [key, val] of Object.entries(value)) {
212
- obj[key] = transformFtrings(val, fns);
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
+ }
213
86
  }
214
- return obj;
215
- }
216
- return value;
87
+ return { passed, errors };
88
+ };
89
+ }
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);
217
95
  }
218
96
 
219
- exports.Foorm = Foorm;
220
- exports.ftring = ftring;
221
- exports.isFtring = isFtring;
97
+ exports.createFormData = createFormData;
98
+ exports.evalComputed = evalComputed;
99
+ exports.getFormValidator = getFormValidator;
100
+ exports.supportsAltAction = supportsAltAction;
222
101
  exports.validate = validate;
package/dist/index.d.ts CHANGED
@@ -1,126 +1,115 @@
1
- interface TFtring {
2
- __is_ftring__: true;
3
- v: string;
4
- __type__?: 'boolean' | 'string' | 'number';
5
- }
6
- type StringOrFtring = string | TFtring;
7
- type ObjSOF = Record<string, StringOrFtring>;
8
- interface TFoormFnScope<T = string> {
9
- v?: T;
10
- data: Record<string, unknown>;
11
- context: Record<string, unknown>;
12
- entry?: Pick<TFoormEntry<T, unknown, string, boolean>, TRelevantFields> & {
13
- optional?: boolean;
14
- disabled?: boolean;
15
- hidden?: boolean;
16
- };
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>> {
7
+ v?: V;
8
+ data: D;
9
+ context: C;
10
+ entry?: TFoormFieldEvaluated;
17
11
  action?: string;
18
12
  }
19
- type TFoormValidatorFn<T = string> = (ctx: TFoormFnScope<T>) => string | boolean;
20
- type TFoormFn<T = string, R = string | boolean> = (ctx: TFoormFnScope<T>) => R;
21
- type TRelevantFields = 'field' | 'type' | 'component' | 'name' | 'attrs' | 'length';
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
+ }
22
30
  type TFoormEntryOptions = {
23
31
  key: string;
24
32
  label: string;
25
33
  } | string;
26
- interface TFoormEntry<T = string, O = TFoormEntryOptions, SFTR = TFtring, BFTR = TFtring, FNFTR = TFtring, OFTR = TFtring> {
34
+ /**
35
+ * A single form field definition with static or computed properties.
36
+ */
37
+ interface TFoormField {
27
38
  field: string;
28
- altAction?: string;
29
- label?: string | SFTR;
30
- description?: string | SFTR;
31
- hint?: string | SFTR;
32
- placeholder?: string | SFTR;
33
- classes?: (string | SFTR) | Record<string, boolean | BFTR>;
34
- styles?: (string | SFTR) | Record<string, string | SFTR>;
35
- type?: string;
39
+ type: string;
36
40
  component?: string;
37
41
  autocomplete?: string;
42
+ altAction?: string;
43
+ order?: number;
38
44
  name?: string;
39
- value?: T;
40
- options?: O[] | OFTR;
41
- attrs?: Record<string, string | SFTR>;
42
- optional?: boolean | BFTR;
43
- disabled?: boolean | BFTR;
44
- hidden?: boolean | BFTR;
45
- length?: number;
46
- validators?: Array<FNFTR | TFoormValidatorFn<T>>;
47
- }
48
- type TFoormEntryExecutable<T = unknown, O = TFoormEntryOptions> = TFoormEntry<T, O, TFoormFn<T, string>, TFoormFn<T, boolean>, TFoormValidatorFn<T>, TFoormFn<T, O[]>> & {
49
- name: string;
50
- label: string | TFoormFn<T, string>;
51
- type: string;
52
- };
53
- interface TFoormMetaExecutable {
54
- title: string | TFoormFn<undefined, string>;
55
- submit: {
56
- text: string | TFoormFn<undefined, string>;
57
- disabled: boolean | TFoormFn<undefined, boolean>;
58
- };
59
- context: Record<string, unknown>;
60
- entries: TFoormEntryExecutable[];
61
- }
62
-
63
- interface TFoormSubmit<S = TFtring, B = TFtring> {
64
- text: string | S;
65
- disabled?: boolean | B;
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;
66
62
  }
67
- interface TFoormOptions {
68
- title?: StringOrFtring;
69
- entries: TFoormEntry[];
70
- submit?: TFoormSubmit;
71
- context?: Record<string, unknown>;
63
+ /**
64
+ * Submit button configuration.
65
+ */
66
+ interface TFoormSubmit {
67
+ text: TComputed<string>;
68
+ disabled?: TComputed<boolean>;
72
69
  }
73
- declare class Foorm {
74
- protected entries: TFoormEntry[];
75
- protected submit?: TFoormSubmit;
76
- protected title?: StringOrFtring;
77
- protected context: Record<string, unknown>;
78
- private fns;
79
- constructor(opts?: TFoormOptions);
80
- addEntry(entry: TFoormEntry): void;
81
- setTitle(title: string): void;
82
- setSubmit(submit: TFoormSubmit): void;
83
- setContext<T extends Record<string, unknown>>(context: T): void;
84
- /**
85
- * Normalizes form metadata and removes all the functions
86
- * from validators.
87
- *
88
- * @param replaceContext a context to be transported along with metadata
89
- * @returns form metadata without functions
90
- */
91
- transportable<T extends Record<string, unknown>>(replaceContext?: T, replaceValues?: Record<string, unknown>): Required<TFoormOptions> & {
92
- context?: Record<string, unknown>;
93
- };
94
- protected normalizeEntry<T, O>(e: TFoormEntry<T, O>): TFoormEntry<T, O> & {
95
- name: string;
96
- label: string | TFtring;
97
- type: string;
98
- };
99
- /**
100
- * Evaluates all the ftrings into functions, makes it ready for execution
101
- *
102
- * @returns form metadata with functions
103
- */
104
- executable(): TFoormMetaExecutable;
105
- createFormData<T extends Record<string, unknown>>(): T;
106
- prepareValidators(_validators: TFoormEntry['validators']): TFoormValidatorFn<string>[];
107
- supportsAltAction(altAction: string): boolean;
108
- getFormValidator(): (inputs: Record<string, unknown>) => {
109
- passed: boolean;
110
- errors: Record<string, string>;
111
- };
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[];
112
77
  }
113
- declare function validate<T = string>(opts: TFoormFnScope<T> & {
114
- validators: Array<TFoormValidatorFn<T>>;
115
- }): {
116
- passed: boolean;
117
- error: string;
78
+
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;
118
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>) => {
119
99
  passed: boolean;
120
- error?: undefined;
100
+ errors: Record<string, string>;
121
101
  };
102
+ /**
103
+ * Checks if any field in the model declares the given altAction.
104
+ */
105
+ declare function supportsAltAction(model: TFoormModel, altAction: string): boolean;
122
106
 
123
- declare function isFtring(input: unknown): input is TFtring;
124
- declare function ftring(strings: TemplateStringsArray, __type__?: TFtring['__type__']): TFtring;
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;
125
113
 
126
- export { Foorm, type ObjSOF, type StringOrFtring, type TFoormEntry, type TFoormEntryExecutable, type TFoormEntryOptions, type TFoormFn, type TFoormFnScope, type TFoormMetaExecutable, type TFoormOptions, type TFoormSubmit, type TFoormValidatorFn, type TFtring, ftring, isFtring, validate };
114
+ export { createFormData, evalComputed, getFormValidator, supportsAltAction, validate };
115
+ export type { TComputed, TFoormEntryOptions, TFoormField, TFoormFieldEvaluated, TFoormFnScope, TFoormModel, TFoormSubmit };
package/dist/index.mjs CHANGED
@@ -1,217 +1,95 @@
1
- import { FtringsPool } from '@prostojs/ftring';
2
-
3
- function isFtring(input) {
4
- return (typeof input === 'object' &&
5
- input.__is_ftring__ &&
6
- typeof input.v === 'string');
7
- }
8
- function ftring(strings, __type__) {
9
- return {
10
- __is_ftring__: true,
11
- v: strings.join(''),
12
- __type__,
13
- };
14
- }
15
-
16
- class Foorm {
17
- constructor(opts) {
18
- this.entries = (opts === null || opts === void 0 ? void 0 : opts.entries) || [];
19
- this.submit = opts === null || opts === void 0 ? void 0 : opts.submit;
20
- this.title = (opts === null || opts === void 0 ? void 0 : opts.title) || '';
21
- this.context = (opts === null || opts === void 0 ? void 0 : opts.context) || {};
22
- }
23
- addEntry(entry) {
24
- this.entries.push(entry);
25
- }
26
- setTitle(title) {
27
- this.title = title;
28
- }
29
- setSubmit(submit) {
30
- this.submit = submit;
31
- }
32
- setContext(context) {
33
- this.context = context;
34
- }
35
- /**
36
- * Normalizes form metadata and removes all the functions
37
- * from validators.
38
- *
39
- * @param replaceContext a context to be transported along with metadata
40
- * @returns form metadata without functions
41
- */
42
- transportable(replaceContext, replaceValues) {
43
- var _a, _b;
44
- return {
45
- title: (_a = this.title) !== null && _a !== void 0 ? _a : '',
46
- submit: (_b = this.submit) !== null && _b !== void 0 ? _b : { text: 'Submit' },
47
- context: replaceContext || this.context,
48
- entries: this.entries.map(e => (Object.assign(Object.assign({}, e), { value: replaceValues ? replaceValues[e.field] : e.value, validators: (e.validators || []).filter(v => isFtring(v)) }))),
49
- };
50
- }
51
- normalizeEntry(e) {
52
- return Object.assign(Object.assign({}, e), { name: e.name || e.field, label: e.label || e.field, type: e.type || 'text' });
53
- }
54
- /**
55
- * Evaluates all the ftrings into functions, makes it ready for execution
56
- *
57
- * @returns form metadata with functions
58
- */
59
- executable() {
60
- var _a, _b;
61
- if (!this.fns) {
62
- this.fns = new FtringsPool();
63
- }
64
- return {
65
- title: transformFtrings(this.title || '', this.fns),
66
- submit: {
67
- text: transformFtrings(((_a = this.submit) === null || _a === void 0 ? void 0 : _a.text) || 'Submit', this.fns),
68
- disabled: transformFtrings((_b = this.submit) === null || _b === void 0 ? void 0 : _b.disabled, this.fns),
69
- },
70
- context: this.context,
71
- entries: this.entries
72
- .map(e => this.normalizeEntry(e))
73
- .map(e => (Object.assign(Object.assign({}, e), {
74
- // strings
75
- label: transformFtrings(e.label, this.fns), description: transformFtrings(e.description, this.fns), hint: transformFtrings(e.hint, this.fns), placeholder: transformFtrings(e.placeholder, this.fns),
76
- // strings || objects
77
- classes: transformFtringsInObj(e.classes, this.fns), styles: transformFtringsInObj(e.styles, this.fns),
78
- // booleans
79
- optional: transformFtrings(e.optional, this.fns), disabled: transformFtrings(e.disabled, this.fns), hidden: transformFtrings(e.hidden, this.fns), validators: this.prepareValidators(e.validators),
80
- // options
81
- options: transformFtrings(e.options, this.fns),
82
- // attrs
83
- attrs: transformFtringsInObj(e.attrs, this.fns) }))),
84
- };
85
- }
86
- createFormData() {
87
- const data = {};
88
- for (const entry of this.entries) {
89
- if (entry.type !== 'action') {
90
- data[entry.field] = (entry.value || undefined);
91
- }
92
- }
93
- return data;
94
- }
95
- prepareValidators(_validators) {
96
- const validators = (_validators || []).map(v => (isFtring(v) ? this.fns.getFn(v.v) : v));
97
- validators.unshift(this.fns.getFn('entry.optional || !!v || "Required"'));
98
- return validators;
99
- }
100
- supportsAltAction(altAction) {
101
- return !!this.entries.some(e => e.altAction === altAction);
102
- }
103
- getFormValidator() {
104
- if (!this.fns) {
105
- this.fns = new FtringsPool();
106
- }
107
- const entries = this.executable().entries;
108
- const fields = {};
109
- for (const entry of entries) {
110
- if (entry.field) {
111
- fields[entry.field] = {
112
- entry,
113
- validators: this.prepareValidators(entry.validators),
114
- };
115
- }
116
- fields[entry.field].validators.unshift(this.fns.getFn('entry.optional || !!v || "Required"'));
117
- }
118
- return (data) => {
119
- let passed = true;
120
- const errors = {};
121
- for (const [key, value] of Object.entries(fields)) {
122
- const evalEntry = Object.assign({}, value.entry);
123
- const scope = {
124
- v: data[key],
125
- context: this.context,
126
- entry: {
127
- field: evalEntry.field,
128
- type: evalEntry.type,
129
- component: evalEntry.component,
130
- name: evalEntry.name,
131
- length: evalEntry.length,
132
- },
133
- data,
134
- };
135
- if (scope.entry) {
136
- if (typeof evalEntry.disabled === 'function') {
137
- scope.entry.disabled = evalEntry.disabled = evalEntry.disabled(scope);
138
- }
139
- else {
140
- scope.entry.disabled = evalEntry.disabled;
141
- }
142
- if (typeof evalEntry.optional === 'function') {
143
- scope.entry.optional = evalEntry.optional = evalEntry.optional(scope);
144
- }
145
- else {
146
- scope.entry.optional = evalEntry.optional;
147
- }
148
- if (typeof evalEntry.hidden === 'function') {
149
- scope.entry.hidden = evalEntry.hidden = evalEntry.hidden(scope);
150
- }
151
- else {
152
- scope.entry.hidden = evalEntry.hidden;
153
- }
154
- }
155
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
- const result = validate({
157
- v: data[key],
158
- context: this.context,
159
- validators: value.validators,
160
- entry: scope.entry,
161
- data,
162
- });
163
- if (!result.passed) {
164
- passed = false;
165
- if (!errors[key]) {
166
- errors[key] = result.error || 'Wrong value';
167
- }
168
- }
169
- }
170
- return {
171
- passed,
172
- errors,
173
- };
174
- };
1
+ function evalComputed(value, scope) {
2
+ if (typeof value === 'function') {
3
+ return value(scope);
175
4
  }
5
+ return value;
176
6
  }
177
- function validate(opts) {
178
- for (const validator of opts.validators || []) {
179
- const result = validator({
180
- v: opts.v,
181
- context: opts.context,
182
- data: opts.data,
183
- entry: opts.entry,
184
- });
7
+
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);
185
14
  if (result !== true) {
186
15
  return {
187
16
  passed: false,
188
- error: result || 'Wrong value',
17
+ error: typeof result === 'string' ? result : 'Invalid value',
189
18
  };
190
19
  }
191
20
  }
192
21
  return { passed: true };
193
22
  }
194
- function transformFtrings(value, fns) {
195
- if (value === undefined) {
196
- return value;
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;
35
+ }
197
36
  }
198
- return isFtring(value) ? fns.getFn(value.v) : value;
37
+ return data;
199
38
  }
200
- function transformFtringsInObj(value, fns) {
201
- if (isFtring(value)) {
202
- return transformFtrings(value, fns);
203
- }
204
- if (typeof value === 'function') {
205
- return value;
206
- }
207
- if (typeof value === 'object' && value !== null) {
208
- const obj = {};
209
- for (const [key, val] of Object.entries(value)) {
210
- obj[key] = transformFtrings(val, fns);
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
+ }
211
84
  }
212
- return obj;
213
- }
214
- return value;
85
+ return { passed, errors };
86
+ };
87
+ }
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);
215
93
  }
216
94
 
217
- export { Foorm, ftring, isFtring, 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.0.3",
3
+ "version": "0.2.0",
4
4
  "description": "foorm",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -34,7 +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/ftring": "^0.0.4"
37
+ "scripts": {
38
+ "pub": "pnpm publish --access public --no-git-checks"
39
39
  }
40
- }
40
+ }