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 +21 -0
- package/README.md +263 -1
- package/dist/index.cjs +83 -204
- package/dist/index.d.ts +97 -108
- package/dist/index.mjs +80 -202
- package/package.json +4 -4
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
|
-
#
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
19
|
+
error: typeof result === 'string' ? result : 'Invalid value',
|
|
191
20
|
};
|
|
192
21
|
}
|
|
193
22
|
}
|
|
194
23
|
return { passed: true };
|
|
195
24
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
39
|
+
return data;
|
|
201
40
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
for (const
|
|
212
|
-
|
|
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
|
|
215
|
-
}
|
|
216
|
-
|
|
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.
|
|
220
|
-
exports.
|
|
221
|
-
exports.
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
34
|
+
/**
|
|
35
|
+
* A single form field definition with static or computed properties.
|
|
36
|
+
*/
|
|
37
|
+
interface TFoormField {
|
|
27
38
|
field: string;
|
|
28
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Submit button configuration.
|
|
65
|
+
*/
|
|
66
|
+
interface TFoormSubmit {
|
|
67
|
+
text: TComputed<string>;
|
|
68
|
+
disabled?: TComputed<boolean>;
|
|
72
69
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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 {
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
17
|
+
error: typeof result === 'string' ? result : 'Invalid value',
|
|
189
18
|
};
|
|
190
19
|
}
|
|
191
20
|
}
|
|
192
21
|
return { passed: true };
|
|
193
22
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
37
|
+
return data;
|
|
199
38
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
for (const
|
|
210
|
-
|
|
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
|
|
213
|
-
}
|
|
214
|
-
|
|
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 {
|
|
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
|
+
"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
|
-
"
|
|
38
|
-
"
|
|
37
|
+
"scripts": {
|
|
38
|
+
"pub": "pnpm publish --access public --no-git-checks"
|
|
39
39
|
}
|
|
40
|
-
}
|
|
40
|
+
}
|