@foormjs/atscript 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 +316 -0
- package/dist/index.cjs +233 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.mjs +227 -0
- package/dist/plugin.cjs +303 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.mjs +299 -0
- package/package.json +73 -0
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
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# @foormjs/atscript
|
|
2
|
+
|
|
3
|
+
ATScript plugin for foormjs -- define forms declaratively with type-safe annotations.
|
|
4
|
+
|
|
5
|
+
Writing form definitions in plain TypeScript means juggling objects, validator functions, and computed property boilerplate. ATScript lets you write all of that as annotations directly on your interface fields. The result is a `.as` file that reads like a specification: labels, placeholders, validators, computed properties, and options are all visible at a glance, right next to the fields they describe.
|
|
6
|
+
|
|
7
|
+
The plugin provides primitives (`foorm.select`, `foorm.radio`, `foorm.checkbox`, `foorm.action`, `foorm.paragraph`), a full set of annotations for field metadata and computed behavior, and `createFoorm()` to convert annotated types into runtime `TFoormModel` objects.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @foormjs/atscript
|
|
13
|
+
# or
|
|
14
|
+
pnpm add @foormjs/atscript
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peer dependency: `@atscript/typescript` (for type generation).
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Configure the plugin
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// atscript.config.ts
|
|
25
|
+
import { defineConfig } from '@atscript/core'
|
|
26
|
+
import ts from '@atscript/typescript'
|
|
27
|
+
import { foormPlugin } from '@foormjs/atscript/plugin'
|
|
28
|
+
|
|
29
|
+
export default defineConfig({
|
|
30
|
+
rootDir: 'src',
|
|
31
|
+
plugins: [ts(), foormPlugin()],
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Define a form
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
// login-form.as
|
|
39
|
+
@foorm.title 'Sign In'
|
|
40
|
+
@foorm.submit.text 'Log In'
|
|
41
|
+
export interface LoginForm {
|
|
42
|
+
@meta.label 'Email'
|
|
43
|
+
@meta.placeholder 'you@example.com'
|
|
44
|
+
@foorm.autocomplete 'email'
|
|
45
|
+
@foorm.validate '(v) => !!v || "Email is required"'
|
|
46
|
+
@foorm.validate '(v) => v.includes("@") || "Invalid email"'
|
|
47
|
+
email: string
|
|
48
|
+
|
|
49
|
+
@meta.label 'Password'
|
|
50
|
+
@foorm.type 'password'
|
|
51
|
+
@foorm.validate '(v) => !!v || "Password is required"'
|
|
52
|
+
password: string
|
|
53
|
+
|
|
54
|
+
@meta.label 'Remember me'
|
|
55
|
+
rememberMe?: foorm.checkbox
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Use at runtime
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createFoorm } from '@foormjs/atscript'
|
|
63
|
+
import { createFormData, getFormValidator } from 'foorm'
|
|
64
|
+
import { LoginForm } from './login-form.as'
|
|
65
|
+
|
|
66
|
+
const form = createFoorm(LoginForm)
|
|
67
|
+
const data = createFormData(form.fields)
|
|
68
|
+
const validator = getFormValidator(form)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Practical Examples
|
|
72
|
+
|
|
73
|
+
### Static form with validation
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
@foorm.title 'Contact Us'
|
|
77
|
+
@foorm.submit.text 'Send Message'
|
|
78
|
+
export interface ContactForm {
|
|
79
|
+
@meta.label 'Your Name'
|
|
80
|
+
@meta.placeholder 'John Doe'
|
|
81
|
+
@foorm.validate '(v) => !!v || "Name is required"'
|
|
82
|
+
@foorm.order 1
|
|
83
|
+
name: string
|
|
84
|
+
|
|
85
|
+
@meta.label 'Email'
|
|
86
|
+
@foorm.autocomplete 'email'
|
|
87
|
+
@foorm.validate '(v) => !!v || "Email is required"'
|
|
88
|
+
@foorm.validate '(v) => v.includes("@") || "Enter a valid email"'
|
|
89
|
+
@foorm.order 2
|
|
90
|
+
email: string
|
|
91
|
+
|
|
92
|
+
@meta.label 'Message'
|
|
93
|
+
@foorm.validate '(v) => !!v || "Message is required"'
|
|
94
|
+
@foorm.validate '(v) => v.length >= 10 || "At least 10 characters"'
|
|
95
|
+
@foorm.order 3
|
|
96
|
+
message: string
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Reactive form with computed properties
|
|
101
|
+
|
|
102
|
+
Computed annotations (`@foorm.fn.*`) accept JavaScript function strings. Field-level functions receive `(value, data, context, entry)`, form-level functions receive `(data, context)`.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
@foorm.fn.title '(data) => "Welcome, " + (data.firstName || "Guest")'
|
|
106
|
+
@foorm.submit.text 'Register'
|
|
107
|
+
@foorm.fn.submit.disabled '(data) => !data.firstName || !data.lastName'
|
|
108
|
+
export interface SignupForm {
|
|
109
|
+
@meta.label 'First Name'
|
|
110
|
+
@meta.placeholder 'Alice'
|
|
111
|
+
@foorm.validate '(v) => !!v || "Required"'
|
|
112
|
+
@foorm.order 1
|
|
113
|
+
firstName: string
|
|
114
|
+
|
|
115
|
+
@meta.label 'Last Name'
|
|
116
|
+
@foorm.fn.placeholder '(v, data) => data.firstName ? "Same as " + data.firstName + "?" : "Doe"'
|
|
117
|
+
@foorm.validate '(v) => !!v || "Required"'
|
|
118
|
+
@foorm.order 2
|
|
119
|
+
lastName: string
|
|
120
|
+
|
|
121
|
+
@meta.label 'Password'
|
|
122
|
+
@foorm.type 'password'
|
|
123
|
+
@foorm.fn.disabled '(v, data) => !data.firstName || !data.lastName'
|
|
124
|
+
@foorm.validate '(v) => !!v || "Required"'
|
|
125
|
+
@foorm.validate '(v) => v.length >= 8 || "At least 8 characters"'
|
|
126
|
+
@foorm.order 3
|
|
127
|
+
password: string
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Select, radio, and checkbox fields
|
|
132
|
+
|
|
133
|
+
Use `foorm.select`, `foorm.radio`, and `foorm.checkbox` primitives with `@foorm.options` for static choices or `@foorm.fn.options` for dynamic choices from context:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
export interface PreferencesForm {
|
|
137
|
+
// Static options
|
|
138
|
+
@meta.label 'Country'
|
|
139
|
+
@meta.placeholder 'Select a country'
|
|
140
|
+
@foorm.options 'United States', 'us'
|
|
141
|
+
@foorm.options 'Canada', 'ca'
|
|
142
|
+
@foorm.options 'United Kingdom', 'uk'
|
|
143
|
+
country?: foorm.select
|
|
144
|
+
|
|
145
|
+
// Options from context (backend-provided)
|
|
146
|
+
@meta.label 'City'
|
|
147
|
+
@meta.placeholder 'Select a city'
|
|
148
|
+
@foorm.fn.options '(v, data, context) => context.cityOptions || []'
|
|
149
|
+
city?: foorm.select
|
|
150
|
+
|
|
151
|
+
// Radio group
|
|
152
|
+
@meta.label 'Theme'
|
|
153
|
+
@foorm.options 'Light', 'light'
|
|
154
|
+
@foorm.options 'Dark', 'dark'
|
|
155
|
+
@foorm.options 'System', 'system'
|
|
156
|
+
theme?: foorm.radio
|
|
157
|
+
|
|
158
|
+
// Boolean checkbox
|
|
159
|
+
@meta.label 'I agree to the terms and conditions'
|
|
160
|
+
@foorm.validate '(v) => v === true || "You must agree"'
|
|
161
|
+
agreeToTerms: foorm.checkbox
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
When using `@foorm.fn.options`, the backend passes option lists through the context object:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const context = {
|
|
169
|
+
cityOptions: [
|
|
170
|
+
{ key: 'nyc', label: 'New York' },
|
|
171
|
+
{ key: 'la', label: 'Los Angeles' },
|
|
172
|
+
],
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Non-data fields: paragraphs and actions
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
export interface WizardStep {
|
|
180
|
+
@meta.label 'Please review your information before submitting.'
|
|
181
|
+
info: foorm.paragraph
|
|
182
|
+
|
|
183
|
+
@meta.label 'First Name'
|
|
184
|
+
firstName: string
|
|
185
|
+
|
|
186
|
+
@meta.label 'Reset Form'
|
|
187
|
+
@foorm.altAction 'reset'
|
|
188
|
+
resetBtn: foorm.action
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Paragraphs render as static text. Actions render as buttons that emit events instead of submitting the form.
|
|
193
|
+
|
|
194
|
+
## Annotations Reference
|
|
195
|
+
|
|
196
|
+
### Form-Level Annotations
|
|
197
|
+
|
|
198
|
+
| Annotation | Description |
|
|
199
|
+
| ------------------------------------------------ | ----------------------------------------------- |
|
|
200
|
+
| `@foorm.title 'text'` | Static form title |
|
|
201
|
+
| `@foorm.submit.text 'text'` | Static submit button text (default: `"Submit"`) |
|
|
202
|
+
| `@foorm.fn.title '(data, ctx) => ...'` | Computed form title |
|
|
203
|
+
| `@foorm.fn.submit.text '(data, ctx) => ...'` | Computed submit button text |
|
|
204
|
+
| `@foorm.fn.submit.disabled '(data, ctx) => ...'` | Computed submit disabled state |
|
|
205
|
+
|
|
206
|
+
### Field-Level Static Annotations
|
|
207
|
+
|
|
208
|
+
| Annotation | Description |
|
|
209
|
+
| ----------------------------- | ----------------------------------------------------- |
|
|
210
|
+
| `@foorm.type 'text'` | Field input type (text, password, number, date, etc.) |
|
|
211
|
+
| `@foorm.component 'name'` | Named component override for rendering |
|
|
212
|
+
| `@foorm.autocomplete 'value'` | HTML autocomplete attribute |
|
|
213
|
+
| `@foorm.altAction 'name'` | Alternate action name (for action fields) |
|
|
214
|
+
| `@foorm.value 'default'` | Default field value |
|
|
215
|
+
| `@foorm.order N` | Rendering order (lower = earlier) |
|
|
216
|
+
| `@foorm.hidden` | Mark field as statically hidden |
|
|
217
|
+
| `@foorm.disabled` | Mark field as statically disabled |
|
|
218
|
+
|
|
219
|
+
### Options Annotation
|
|
220
|
+
|
|
221
|
+
| Annotation | Description |
|
|
222
|
+
| --------------------------------- | -------------------------------------------------------------------------------- |
|
|
223
|
+
| `@foorm.options 'Label', 'value'` | Add a static option. Repeat for each choice. Value defaults to label if omitted. |
|
|
224
|
+
|
|
225
|
+
### Validation Annotation
|
|
226
|
+
|
|
227
|
+
| Annotation | Description |
|
|
228
|
+
| ----------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
229
|
+
| `@foorm.validate '(v, data, ctx) => ...'` | Custom validator. Returns `true` to pass, or an error string. Repeat for multiple validators. |
|
|
230
|
+
|
|
231
|
+
Validators run in declaration order and stop on first failure.
|
|
232
|
+
|
|
233
|
+
### Computed (fn) Annotations
|
|
234
|
+
|
|
235
|
+
All field-level computed functions receive `(value, data, context, entry)`:
|
|
236
|
+
|
|
237
|
+
| Annotation | Return type | Description |
|
|
238
|
+
| ----------------------- | ---------------------- | ------------------------------- |
|
|
239
|
+
| `@foorm.fn.label` | `string` | Computed label |
|
|
240
|
+
| `@foorm.fn.description` | `string` | Computed description |
|
|
241
|
+
| `@foorm.fn.hint` | `string` | Computed hint text |
|
|
242
|
+
| `@foorm.fn.placeholder` | `string` | Computed placeholder |
|
|
243
|
+
| `@foorm.fn.disabled` | `boolean` | Computed disabled state |
|
|
244
|
+
| `@foorm.fn.hidden` | `boolean` | Computed hidden state |
|
|
245
|
+
| `@foorm.fn.optional` | `boolean` | Computed optional state |
|
|
246
|
+
| `@foorm.fn.classes` | `string \| Record` | Computed CSS classes |
|
|
247
|
+
| `@foorm.fn.styles` | `string \| Record` | Computed inline styles |
|
|
248
|
+
| `@foorm.fn.options` | `TFoormEntryOptions[]` | Computed options (select/radio) |
|
|
249
|
+
|
|
250
|
+
### Metadata Annotations (from ATScript core)
|
|
251
|
+
|
|
252
|
+
| Annotation | Description |
|
|
253
|
+
| -------------------------- | ----------------- |
|
|
254
|
+
| `@meta.label 'text'` | Field label |
|
|
255
|
+
| `@meta.description 'text'` | Field description |
|
|
256
|
+
| `@meta.hint 'text'` | Hint text |
|
|
257
|
+
| `@meta.placeholder 'text'` | Input placeholder |
|
|
258
|
+
|
|
259
|
+
### Constraint Annotations (from ATScript core)
|
|
260
|
+
|
|
261
|
+
| Annotation | Description |
|
|
262
|
+
| --------------------- | --------------------- |
|
|
263
|
+
| `@expect.maxLength N` | Maximum string length |
|
|
264
|
+
| `@expect.minLength N` | Minimum string length |
|
|
265
|
+
| `@expect.min N` | Minimum numeric value |
|
|
266
|
+
| `@expect.max N` | Maximum numeric value |
|
|
267
|
+
|
|
268
|
+
## Primitives
|
|
269
|
+
|
|
270
|
+
| Primitive | Underlying type | Description |
|
|
271
|
+
| ----------------- | --------------- | --------------------------------- |
|
|
272
|
+
| `foorm.select` | `string` | Dropdown select field |
|
|
273
|
+
| `foorm.radio` | `string` | Radio button group |
|
|
274
|
+
| `foorm.checkbox` | `boolean` | Single checkbox toggle |
|
|
275
|
+
| `foorm.action` | phantom | Button that emits an action event |
|
|
276
|
+
| `foorm.paragraph` | phantom | Static read-only text |
|
|
277
|
+
|
|
278
|
+
Phantom primitives are excluded from form data -- they exist only for UI rendering.
|
|
279
|
+
|
|
280
|
+
## Plugin Options
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
foormPlugin({
|
|
284
|
+
// Extend allowed values for @foorm.type
|
|
285
|
+
extraTypes: ['tel', 'url', 'color'],
|
|
286
|
+
|
|
287
|
+
// Enable autocomplete for @foorm.component
|
|
288
|
+
components: ['CustomInput', 'DatePicker', 'RichTextEditor'],
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
When `components` is provided, `@foorm.component` gets IDE autocomplete suggestions and compile-time validation against the list. When omitted, any string is accepted.
|
|
293
|
+
|
|
294
|
+
When `extraTypes` is provided, the additional values are added to the built-in `@foorm.type` values (text, password, number, select, textarea, checkbox, radio, date, paragraph, action).
|
|
295
|
+
|
|
296
|
+
## Field Type Resolution
|
|
297
|
+
|
|
298
|
+
`createFoorm()` determines each field's type in this order:
|
|
299
|
+
|
|
300
|
+
1. `@foorm.type` annotation (explicit override)
|
|
301
|
+
2. Foorm primitive tag (`foorm.select`, `foorm.radio`, `foorm.checkbox`, `foorm.action`, `foorm.paragraph`)
|
|
302
|
+
3. Default: `'text'`
|
|
303
|
+
|
|
304
|
+
## Options Resolution
|
|
305
|
+
|
|
306
|
+
For select and radio fields, options are resolved in this order:
|
|
307
|
+
|
|
308
|
+
1. `@foorm.fn.options` (computed) -- if present, compiled as a function
|
|
309
|
+
2. `@foorm.options` (static) -- parsed from annotation values
|
|
310
|
+
3. `undefined` -- no options
|
|
311
|
+
|
|
312
|
+
The computed path takes precedence, so you can define static fallbacks that are overridden when a `fn.options` annotation exists.
|
|
313
|
+
|
|
314
|
+
## License
|
|
315
|
+
|
|
316
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var deserializeFn = require('@prostojs/deserialize-fn');
|
|
4
|
+
|
|
5
|
+
const pool = new deserializeFn.FNPool();
|
|
6
|
+
/**
|
|
7
|
+
* Compiles a field-level function string from a @foorm.fn.* annotation
|
|
8
|
+
* into a callable function. Uses FNPool for caching.
|
|
9
|
+
*
|
|
10
|
+
* The function string should be an arrow or regular function expression:
|
|
11
|
+
* "(v, data, ctx, entry) => !data.firstName"
|
|
12
|
+
*
|
|
13
|
+
* The compiled function receives a single TFoormFnScope object:
|
|
14
|
+
* { v, data, context, entry }
|
|
15
|
+
*/
|
|
16
|
+
function compileFieldFn(fnStr) {
|
|
17
|
+
const code = `return (${fnStr})(v, data, context, entry)`;
|
|
18
|
+
return pool.getFn(code);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compiles a form-level function string from a @foorm.fn.title,
|
|
22
|
+
* @foorm.fn.submit.text, or @foorm.fn.submit.disabled annotation.
|
|
23
|
+
*
|
|
24
|
+
* The function string should be:
|
|
25
|
+
* "(data, ctx) => someExpression"
|
|
26
|
+
*
|
|
27
|
+
* The compiled function receives a single TFoormFnScope object:
|
|
28
|
+
* { data, context }
|
|
29
|
+
*/
|
|
30
|
+
function compileTopFn(fnStr) {
|
|
31
|
+
const code = `return (${fnStr})(data, context)`;
|
|
32
|
+
return pool.getFn(code);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compiles a validator function string from a @foorm.validate annotation.
|
|
36
|
+
*
|
|
37
|
+
* The function string should be:
|
|
38
|
+
* "(v, data, ctx) => boolean | string"
|
|
39
|
+
*
|
|
40
|
+
* The compiled function receives a single TFoormFnScope object:
|
|
41
|
+
* { v, data, context }
|
|
42
|
+
*/
|
|
43
|
+
function compileValidatorFn(fnStr) {
|
|
44
|
+
const code = `return (${fnStr})(v, data, context)`;
|
|
45
|
+
return pool.getFn(code);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function foormValidatorPlugin(foormCtx) {
|
|
49
|
+
return (ctx, def, value) => {
|
|
50
|
+
var _a, _b, _c;
|
|
51
|
+
const validators = (_a = def.metadata) === null || _a === void 0 ? void 0 : _a.get('foorm.validate');
|
|
52
|
+
if (!validators) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const fns = Array.isArray(validators) ? validators : [validators];
|
|
56
|
+
const data = (_b = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.data) !== null && _b !== void 0 ? _b : {};
|
|
57
|
+
const context = (_c = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.context) !== null && _c !== void 0 ? _c : {};
|
|
58
|
+
for (const fnStr of fns) {
|
|
59
|
+
if (typeof fnStr !== 'string') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const fn = compileValidatorFn(fnStr);
|
|
63
|
+
const result = fn({ v: value, data, context });
|
|
64
|
+
if (result !== true) {
|
|
65
|
+
ctx.error(typeof result === 'string' ? result : 'Validation failed');
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Known foorm primitive extension tags that map directly to field types. */
|
|
74
|
+
const FOORM_TAGS = new Set(['action', 'paragraph', 'select', 'radio', 'checkbox']);
|
|
75
|
+
/** Converts a static @foorm.options annotation value to TFoormEntryOptions[]. */
|
|
76
|
+
function parseStaticOptions(raw) {
|
|
77
|
+
const items = Array.isArray(raw) ? raw : [raw];
|
|
78
|
+
return items.map(item => {
|
|
79
|
+
// Multi-arg annotations are stored as { label, value? }
|
|
80
|
+
if (typeof item === 'object' && item !== null && 'label' in item) {
|
|
81
|
+
const { label, value } = item;
|
|
82
|
+
return value !== undefined ? { key: value, label } : label;
|
|
83
|
+
}
|
|
84
|
+
// Plain string fallback (single-arg or raw value)
|
|
85
|
+
return String(item);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolves a static annotation or a @foorm.fn.* computed annotation.
|
|
90
|
+
* If the fn annotation exists, compiles it. Otherwise falls back to the
|
|
91
|
+
* static annotation or the default value.
|
|
92
|
+
*/
|
|
93
|
+
function resolveComputed(staticKey, fnKey, metadata, compileFn, defaultValue) {
|
|
94
|
+
const fnStr = metadata.get(fnKey);
|
|
95
|
+
if (typeof fnStr === 'string') {
|
|
96
|
+
return compileFn(fnStr);
|
|
97
|
+
}
|
|
98
|
+
const staticVal = metadata.get(staticKey);
|
|
99
|
+
if (staticVal !== undefined) {
|
|
100
|
+
return staticVal;
|
|
101
|
+
}
|
|
102
|
+
return defaultValue;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Converts an ATScript annotated type into a TFoormModel.
|
|
106
|
+
*
|
|
107
|
+
* Reads @foorm.*, @meta.*, and @expect.* annotations from the type's
|
|
108
|
+
* metadata to build field definitions with static or computed properties.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* import { RegistrationForm } from './registration.as'
|
|
113
|
+
* import { createFoorm } from '@foormjs/atscript'
|
|
114
|
+
*
|
|
115
|
+
* const model = createFoorm(RegistrationForm)
|
|
116
|
+
* const data = createFormData(model.fields)
|
|
117
|
+
* const validator = getFormValidator(model)
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
function createFoorm(type) {
|
|
121
|
+
var _a, _b;
|
|
122
|
+
const metadata = type.metadata;
|
|
123
|
+
const props = type.type.props;
|
|
124
|
+
// Form-level metadata
|
|
125
|
+
const title = resolveComputed('foorm.title', 'foorm.fn.title', metadata, compileTopFn, '');
|
|
126
|
+
const submitText = resolveComputed('foorm.submit.text', 'foorm.fn.submit.text', metadata, compileTopFn, 'Submit');
|
|
127
|
+
const submitDisabled = (() => {
|
|
128
|
+
const fnStr = metadata.get('foorm.fn.submit.disabled');
|
|
129
|
+
if (typeof fnStr === 'string') {
|
|
130
|
+
return compileTopFn(fnStr);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
})();
|
|
134
|
+
const submit = { text: submitText, disabled: submitDisabled };
|
|
135
|
+
// Build fields from props
|
|
136
|
+
const fields = [];
|
|
137
|
+
for (const [name, prop] of props.entries()) {
|
|
138
|
+
const pm = prop.metadata;
|
|
139
|
+
const tags = (_a = prop.type) === null || _a === void 0 ? void 0 : _a.tags;
|
|
140
|
+
// Determine field type from @foorm.type, foorm primitive tags, or default
|
|
141
|
+
const foormType = pm.get('foorm.type');
|
|
142
|
+
const foormTag = tags ? [...tags].find(t => FOORM_TAGS.has(t)) : undefined;
|
|
143
|
+
const fieldType = (_b = foormType !== null && foormType !== void 0 ? foormType : foormTag) !== null && _b !== void 0 ? _b : 'text';
|
|
144
|
+
// Build validators from @foorm.validate
|
|
145
|
+
const validators = [];
|
|
146
|
+
const validateAnnotation = pm.get('foorm.validate');
|
|
147
|
+
if (validateAnnotation) {
|
|
148
|
+
const fns = Array.isArray(validateAnnotation) ? validateAnnotation : [validateAnnotation];
|
|
149
|
+
for (const fnStr of fns) {
|
|
150
|
+
if (typeof fnStr === 'string') {
|
|
151
|
+
validators.push(compileValidatorFn(fnStr));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const field = {
|
|
156
|
+
field: name,
|
|
157
|
+
type: fieldType,
|
|
158
|
+
component: pm.get('foorm.component'),
|
|
159
|
+
autocomplete: pm.get('foorm.autocomplete'),
|
|
160
|
+
altAction: pm.get('foorm.altAction'),
|
|
161
|
+
order: pm.get('foorm.order'),
|
|
162
|
+
name: name,
|
|
163
|
+
label: resolveComputed('meta.label', 'foorm.fn.label', pm, compileFieldFn, name),
|
|
164
|
+
description: resolveComputed('meta.description', 'foorm.fn.description', pm, compileFieldFn, ''),
|
|
165
|
+
hint: resolveComputed('meta.hint', 'foorm.fn.hint', pm, compileFieldFn, ''),
|
|
166
|
+
placeholder: resolveComputed('meta.placeholder', 'foorm.fn.placeholder', pm, compileFieldFn, ''),
|
|
167
|
+
optional: (() => {
|
|
168
|
+
var _a;
|
|
169
|
+
const fnStr = pm.get('foorm.fn.optional');
|
|
170
|
+
if (typeof fnStr === 'string') {
|
|
171
|
+
return compileFieldFn(fnStr);
|
|
172
|
+
}
|
|
173
|
+
return (_a = prop.optional) !== null && _a !== void 0 ? _a : false;
|
|
174
|
+
})(),
|
|
175
|
+
disabled: (() => {
|
|
176
|
+
const fnStr = pm.get('foorm.fn.disabled');
|
|
177
|
+
if (typeof fnStr === 'string') {
|
|
178
|
+
return compileFieldFn(fnStr);
|
|
179
|
+
}
|
|
180
|
+
return pm.get('foorm.disabled') !== undefined;
|
|
181
|
+
})(),
|
|
182
|
+
hidden: (() => {
|
|
183
|
+
const fnStr = pm.get('foorm.fn.hidden');
|
|
184
|
+
if (typeof fnStr === 'string') {
|
|
185
|
+
return compileFieldFn(fnStr);
|
|
186
|
+
}
|
|
187
|
+
return pm.get('foorm.hidden') !== undefined;
|
|
188
|
+
})(),
|
|
189
|
+
classes: (() => {
|
|
190
|
+
const fnStr = pm.get('foorm.fn.classes');
|
|
191
|
+
if (typeof fnStr === 'string') {
|
|
192
|
+
return compileFieldFn(fnStr);
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
})(),
|
|
196
|
+
styles: (() => {
|
|
197
|
+
const fnStr = pm.get('foorm.fn.styles');
|
|
198
|
+
if (typeof fnStr === 'string') {
|
|
199
|
+
return compileFieldFn(fnStr);
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
})(),
|
|
203
|
+
options: (() => {
|
|
204
|
+
const fnStr = pm.get('foorm.fn.options');
|
|
205
|
+
if (typeof fnStr === 'string') {
|
|
206
|
+
return compileFieldFn(fnStr);
|
|
207
|
+
}
|
|
208
|
+
const staticOpts = pm.get('foorm.options');
|
|
209
|
+
if (staticOpts) {
|
|
210
|
+
return parseStaticOptions(staticOpts);
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
})(),
|
|
214
|
+
value: pm.get('foorm.value'),
|
|
215
|
+
validators,
|
|
216
|
+
// ATScript @expect constraints
|
|
217
|
+
maxLength: pm.get('expect.maxLength'),
|
|
218
|
+
minLength: pm.get('expect.minLength'),
|
|
219
|
+
min: pm.get('expect.min'),
|
|
220
|
+
max: pm.get('expect.max'),
|
|
221
|
+
};
|
|
222
|
+
fields.push(field);
|
|
223
|
+
}
|
|
224
|
+
// Sort by explicit order, preserving original order for unordered fields
|
|
225
|
+
fields.sort((a, b) => { var _a, _b; return ((_a = a.order) !== null && _a !== void 0 ? _a : Infinity) - ((_b = b.order) !== null && _b !== void 0 ? _b : Infinity); });
|
|
226
|
+
return { title, submit, fields };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
exports.compileFieldFn = compileFieldFn;
|
|
230
|
+
exports.compileTopFn = compileTopFn;
|
|
231
|
+
exports.compileValidatorFn = compileValidatorFn;
|
|
232
|
+
exports.createFoorm = createFoorm;
|
|
233
|
+
exports.foormValidatorPlugin = foormValidatorPlugin;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { TFoormFnScope, TFoormModel } from 'foorm';
|
|
2
|
+
import { TAtscriptAnnotatedType, TAtscriptTypeObject } from '@atscript/typescript/utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ATScript validator plugin that processes @foorm.validate annotations.
|
|
6
|
+
*
|
|
7
|
+
* Reads the `foorm.validate` annotation value(s) from the metadata,
|
|
8
|
+
* compiles each function string, and executes it with the current value
|
|
9
|
+
* and form data/context from validator options.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { foormValidatorPlugin } from '@foormjs/atscript'
|
|
13
|
+
*
|
|
14
|
+
* const validator = MyForm.validator({
|
|
15
|
+
* plugins: [foormValidatorPlugin()],
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* Pass form data and context via validator options:
|
|
19
|
+
* validator.validate(fieldValue, true) // safe mode
|
|
20
|
+
*
|
|
21
|
+
* For whole-form validation, iterate props and validate each field.
|
|
22
|
+
*/
|
|
23
|
+
interface TFoormValidatorContext {
|
|
24
|
+
data?: Record<string, unknown>;
|
|
25
|
+
context?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
type TValidatorPlugin = (ctx: any, def: any, value: unknown) => boolean | undefined;
|
|
28
|
+
declare function foormValidatorPlugin(foormCtx?: TFoormValidatorContext): TValidatorPlugin;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compiles a field-level function string from a @foorm.fn.* annotation
|
|
32
|
+
* into a callable function. Uses FNPool for caching.
|
|
33
|
+
*
|
|
34
|
+
* The function string should be an arrow or regular function expression:
|
|
35
|
+
* "(v, data, ctx, entry) => !data.firstName"
|
|
36
|
+
*
|
|
37
|
+
* The compiled function receives a single TFoormFnScope object:
|
|
38
|
+
* { v, data, context, entry }
|
|
39
|
+
*/
|
|
40
|
+
declare function compileFieldFn<R = unknown>(fnStr: string): (scope: TFoormFnScope) => R;
|
|
41
|
+
/**
|
|
42
|
+
* Compiles a form-level function string from a @foorm.fn.title,
|
|
43
|
+
* @foorm.fn.submit.text, or @foorm.fn.submit.disabled annotation.
|
|
44
|
+
*
|
|
45
|
+
* The function string should be:
|
|
46
|
+
* "(data, ctx) => someExpression"
|
|
47
|
+
*
|
|
48
|
+
* The compiled function receives a single TFoormFnScope object:
|
|
49
|
+
* { data, context }
|
|
50
|
+
*/
|
|
51
|
+
declare function compileTopFn<R = unknown>(fnStr: string): (scope: TFoormFnScope) => R;
|
|
52
|
+
/**
|
|
53
|
+
* Compiles a validator function string from a @foorm.validate annotation.
|
|
54
|
+
*
|
|
55
|
+
* The function string should be:
|
|
56
|
+
* "(v, data, ctx) => boolean | string"
|
|
57
|
+
*
|
|
58
|
+
* The compiled function receives a single TFoormFnScope object:
|
|
59
|
+
* { v, data, context }
|
|
60
|
+
*/
|
|
61
|
+
declare function compileValidatorFn(fnStr: string): (scope: TFoormFnScope) => boolean | string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Converts an ATScript annotated type into a TFoormModel.
|
|
65
|
+
*
|
|
66
|
+
* Reads @foorm.*, @meta.*, and @expect.* annotations from the type's
|
|
67
|
+
* metadata to build field definitions with static or computed properties.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { RegistrationForm } from './registration.as'
|
|
72
|
+
* import { createFoorm } from '@foormjs/atscript'
|
|
73
|
+
*
|
|
74
|
+
* const model = createFoorm(RegistrationForm)
|
|
75
|
+
* const data = createFormData(model.fields)
|
|
76
|
+
* const validator = getFormValidator(model)
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare function createFoorm(type: TAtscriptAnnotatedType<TAtscriptTypeObject<any, any>>): TFoormModel;
|
|
80
|
+
|
|
81
|
+
export { compileFieldFn, compileTopFn, compileValidatorFn, createFoorm, foormValidatorPlugin };
|
|
82
|
+
export type { TFoormValidatorContext };
|