@modyra/angular 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 +364 -0
- package/fesm2022/modyra-angular-adapter.mjs +19 -0
- package/fesm2022/modyra-angular-interop.mjs +145 -0
- package/fesm2022/modyra-angular-ui.mjs +18 -0
- package/fesm2022/modyra-angular-zod.mjs +33 -0
- package/fesm2022/modyra-angular.mjs +9556 -0
- package/package.json +94 -0
- package/types/modyra-angular-adapter.d.ts +2 -0
- package/types/modyra-angular-interop.d.ts +43 -0
- package/types/modyra-angular-ui.d.ts +2 -0
- package/types/modyra-angular-zod.d.ts +23 -0
- package/types/modyra-angular.d.ts +2385 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lorenzo muscherà
|
|
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,364 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/brand/readme-banner-dark.png">
|
|
3
|
+
<img src="docs/assets/brand/readme-banner-light.png" alt="Modyra — Model once. Render anywhere.">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
6
|
+
# Modyra
|
|
7
|
+
|
|
8
|
+
**Model once. Render anywhere.** A framework-agnostic, type-safe form
|
|
9
|
+
engine with native bindings for Angular, React, Vue and Lit — one shared
|
|
10
|
+
core, one shared headless widget layer, one shared theme package.
|
|
11
|
+
|
|
12
|
+
- No `FormControl`, `FormGroup` or RxJS — form state is signals and `computed`s
|
|
13
|
+
- Compile-time checked field bindings: `[field]="form.f.email"`, typos don't compile
|
|
14
|
+
- Sync, async (debounced, cancellable, cross-field) and form-level validation
|
|
15
|
+
- Typed field arrays (`array()`) for repeatable rows — push/insert/remove/move
|
|
16
|
+
- Drafts (autosave/restore), undo/redo, minimal-patch change tracking, devtools
|
|
17
|
+
- Headless core or accessible ready-made controls — your design system or ours
|
|
18
|
+
- Incremental Angular adoption through Reactive Forms interop (`mdyCva`)
|
|
19
|
+
|
|
20
|
+
[](https://github.com/modyra/modyra/actions/workflows/ci.yml)
|
|
21
|
+
[](https://www.npmjs.com/package/@modyra/angular)
|
|
22
|
+
[](https://angular.dev)
|
|
23
|
+
[](https://www.typescriptlang.org)
|
|
24
|
+
[](LICENSE)
|
|
25
|
+
[](https://github.com/sponsors/lorenzomusche)
|
|
26
|
+
|
|
27
|
+
## Packages
|
|
28
|
+
|
|
29
|
+
| Package | What it is | Peer deps |
|
|
30
|
+
| :------------------------------------ | :------------------------------------------------------------------------------------------------------- | :--------------------- |
|
|
31
|
+
| [`@modyra/core`](packages/core) | Framework-agnostic form engine: typed field trees, arrays, validation, drafts, undo/redo, i18n utilities | — |
|
|
32
|
+
| [`@modyra/widgets`](packages/widgets) | Headless widget controllers + universal interaction/accessibility contract | — |
|
|
33
|
+
| [`@modyra/angular`](packages/angular) | Angular binding on native signals — the most complete adapter (UI catalog, devtools, wizard, interop) | `@angular/*` ≥21 |
|
|
34
|
+
| [`@modyra/react`](packages/react) | React binding via `useSyncExternalStore` | `react` ≥18 |
|
|
35
|
+
| [`@modyra/vue`](packages/vue) | Vue binding on `@vue/reactivity` | `@vue/reactivity` ≥3.4 |
|
|
36
|
+
| [`@modyra/lit`](packages/lit) | Lit binding — ReactiveController + themable form elements | `lit` ≥3 |
|
|
37
|
+
| [`@modyra/zod`](packages/zod) | Framework-agnostic Zod adapter — schema-first typed forms | `zod` ≥3.25 |
|
|
38
|
+
| [`@modyra/styles`](packages/styles) | CSS themes (`default`, `material`, `ios`, `ionic`, `base`) for every adapter | — |
|
|
39
|
+
|
|
40
|
+
## The engine in 60 seconds (framework-agnostic)
|
|
41
|
+
|
|
42
|
+
Everything below runs in plain TypeScript — Node, a CLI, a worker, a unit
|
|
43
|
+
test, or any of the four framework adapters. No framework in sight:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createForm, field, group, required, email, min } from "@modyra/core";
|
|
47
|
+
|
|
48
|
+
const form = createForm({
|
|
49
|
+
email: field("", [required(), email()]),
|
|
50
|
+
age: field<number | null>(null, [min(18)]),
|
|
51
|
+
address: group({ city: field("Rome"), zip: field("") }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
form.f.email.set("not-an-email");
|
|
55
|
+
form.f.email.errors(); // ["Enter a valid email address"]
|
|
56
|
+
form.getValue().address.city; // "Rome" — fully typed, typos don't compile
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> **Validators are factories, not values:** write `required()`, not
|
|
60
|
+
> `required` (Angular Reactive Forms muscle memory trips here — the
|
|
61
|
+
> resulting TS error is easy to misread). Validation errors come back as
|
|
62
|
+
> **arrays of message strings** (`["Name taken"]`), not `{ required: true }`
|
|
63
|
+
> keyed objects. To stop at the first failing validator instead of
|
|
64
|
+
> collecting all of them, use `composeFirst()` in place of `compose()`.
|
|
65
|
+
|
|
66
|
+
## Real-world scenarios, handled by the engine
|
|
67
|
+
|
|
68
|
+
These are the cases that make form code rot in production — nested data,
|
|
69
|
+
repeatable rows, server round-trips, refreshes, "apply my changes only".
|
|
70
|
+
Each one below is complete and runnable in plain TypeScript.
|
|
71
|
+
|
|
72
|
+
### 1. Checkout: nested groups, repeatable line items, a coupon checked server-side
|
|
73
|
+
|
|
74
|
+
An order form with an address group, a **typed array** of line items, a
|
|
75
|
+
coupon validated against the server (re-checked automatically when the
|
|
76
|
+
country changes), and server errors routed back to fields on submit.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import {
|
|
80
|
+
createForm,
|
|
81
|
+
field,
|
|
82
|
+
group,
|
|
83
|
+
array,
|
|
84
|
+
required,
|
|
85
|
+
min,
|
|
86
|
+
pattern,
|
|
87
|
+
crossField,
|
|
88
|
+
serverValidator,
|
|
89
|
+
} from "@modyra/core";
|
|
90
|
+
|
|
91
|
+
const form = createForm(
|
|
92
|
+
{
|
|
93
|
+
country: field("IT"),
|
|
94
|
+
shipping: group({
|
|
95
|
+
city: field("", [required()]),
|
|
96
|
+
zip: field("", [required(), pattern(/^\d{5}$/, "5 digits")]),
|
|
97
|
+
}),
|
|
98
|
+
// Typed repeatable rows: form.f.items.rows(), push/insert/remove/move
|
|
99
|
+
items: array(
|
|
100
|
+
group({
|
|
101
|
+
sku: field("", [required()]),
|
|
102
|
+
qty: field<number>(1, [min(1)]),
|
|
103
|
+
}),
|
|
104
|
+
{ initial: [{ sku: "TSHIRT-BLK-M", qty: 2 }] },
|
|
105
|
+
),
|
|
106
|
+
coupon: field(
|
|
107
|
+
"",
|
|
108
|
+
[],
|
|
109
|
+
serverValidator(
|
|
110
|
+
async (code, ctx) => {
|
|
111
|
+
if (!code) return null; // optional field — skip the call
|
|
112
|
+
// ctx.form reads the rest of the form; ctx.signal cancels stale calls
|
|
113
|
+
const res = await api.coupons.check(
|
|
114
|
+
code,
|
|
115
|
+
ctx.form.fieldValue("country"),
|
|
116
|
+
{
|
|
117
|
+
signal: ctx.signal,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
return res.valid ? null : "Coupon not valid for your country";
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
dependsOn: ["country"], // country flips → coupon re-validates itself
|
|
124
|
+
debounceMs: 400,
|
|
125
|
+
timeoutMs: 5000, // never a "pending" that lasts forever
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
validators: [
|
|
132
|
+
// Form-level rule over the whole typed value
|
|
133
|
+
crossField(["items"], (v) =>
|
|
134
|
+
v.items.length === 0 ? "Add at least one item to the order" : null,
|
|
135
|
+
),
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
form.f.items.push({ sku: "MUG-WHT", qty: 1 });
|
|
141
|
+
form.f.items.rows()[1].sku.errors(); // per-row, per-field errors
|
|
142
|
+
form.f.items.remove(0);
|
|
143
|
+
form.f.items.length(); // 1
|
|
144
|
+
form.getValue().items[0].qty; // number — a typo here is a compile error
|
|
145
|
+
|
|
146
|
+
const result = await form.submit(async (order) => {
|
|
147
|
+
const res = await api.orders.create(order);
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
// Server errors land on the matching field (or the form, with path: null)
|
|
150
|
+
return res.errors.map((e) => ({
|
|
151
|
+
path: e.field,
|
|
152
|
+
kind: "server",
|
|
153
|
+
message: e.message,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
What the engine did for you: `items.0.sku`-style paths stay compile-checked;
|
|
160
|
+
`state.valid()` includes per-row validators _and_ the form-level rule; the
|
|
161
|
+
coupon re-checks itself when `country` flips and aborts the previous HTTP
|
|
162
|
+
call when you keep typing; `state.pending()` covers every async run, so a
|
|
163
|
+
submit button bound to `state.canSubmit()` can't fire mid-validation.
|
|
164
|
+
|
|
165
|
+
### 2. A long insurance claim: survive refresh, undo mistakes, patch only what changed
|
|
166
|
+
|
|
167
|
+
A 40-field claim form. The user refreshes mid-way (drafts), deletes the
|
|
168
|
+
wrong paragraph (undo/redo), and your backend only wants the diff
|
|
169
|
+
(`getChanges`).
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { createForm, field, group, required, minLength } from "@modyra/core";
|
|
173
|
+
|
|
174
|
+
const form = createForm(
|
|
175
|
+
{
|
|
176
|
+
policyNumber: field("", [required()]),
|
|
177
|
+
incident: group({
|
|
178
|
+
date: field("", [required()]), // ISO yyyy-MM-dd
|
|
179
|
+
description: field("", [required(), minLength(30)]),
|
|
180
|
+
}),
|
|
181
|
+
iban: field("", [required()]),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
// Autosave to localStorage every 500 ms of idle; restored on reload.
|
|
185
|
+
// Sensitive fields are masked in devtools and excluded from the draft.
|
|
186
|
+
draft: { key: "claim-form", exclude: ["iban"] },
|
|
187
|
+
history: true, // undo()/redo()
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
form.f.incident.description.set("The kitchen pipe burst and…");
|
|
192
|
+
form.undo(); // oops — bring the paragraph back
|
|
193
|
+
form.redo();
|
|
194
|
+
|
|
195
|
+
form.getChanges(); // { incident: { description: "The kitchen pipe…" } }
|
|
196
|
+
// → the minimal PATCH body, typed
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Drafts are versioned envelopes with a 7-day TTL: File/BigInt values are
|
|
200
|
+
refused, quota errors never crash the form, and `__proto__`-style paths in
|
|
201
|
+
a tampered `localStorage` entry are discarded at restore.
|
|
202
|
+
|
|
203
|
+
### 3. Schema-first: the backend already speaks Zod
|
|
204
|
+
|
|
205
|
+
The validation contract lives in one Zod schema shared with your API — the
|
|
206
|
+
form derives fields, initial values, validators and cross-field rules from
|
|
207
|
+
it, arrays included:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { z } from "zod";
|
|
211
|
+
import { createZodForm } from "@modyra/zod";
|
|
212
|
+
|
|
213
|
+
const passengerSchema = z.object({
|
|
214
|
+
fullName: z.string().min(1, "Required"),
|
|
215
|
+
infant: z.boolean(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const bookingSchema = z.object({
|
|
219
|
+
flight: z.string().min(1, "Pick a flight"),
|
|
220
|
+
passengers: z.array(passengerSchema).min(1, "At least one passenger"),
|
|
221
|
+
contact: z.object({
|
|
222
|
+
email: z.string().email(),
|
|
223
|
+
phone: z.string().optional(),
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const form = createZodForm(bookingSchema);
|
|
228
|
+
form.f.passengers.push({ fullName: "Ada Lovelace", infant: false });
|
|
229
|
+
// z.array() → typed field array; .min(1) → array-level validator gating submit
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`zod` is an _optional_ peer (`>=3.25`, Zod 4 supported): apps that don't
|
|
233
|
+
use schemas never download it.
|
|
234
|
+
|
|
235
|
+
## The same app, four frameworks
|
|
236
|
+
|
|
237
|
+
The engine is identical everywhere — only the reactive binding and the
|
|
238
|
+
rendering idiom change. A complete checkout form (nested groups, typed
|
|
239
|
+
array rows, cancellable server validation, submit with server errors) is
|
|
240
|
+
implemented end-to-end, side by side, in:
|
|
241
|
+
|
|
242
|
+
- [Angular](docs/examples/angular.md) — `mdyForm` + the UI catalog, `@for` over `rows()`
|
|
243
|
+
- [React](docs/examples/react.md) — `useMdyForm` / `useMdyField`, array rows with `.map()`
|
|
244
|
+
- [Vue](docs/examples/vue.md) — `createVueForm` on `@vue/reactivity`
|
|
245
|
+
- [Lit](docs/examples/lit.md) — `createLitForm` + `<mdy-*-field>` custom elements
|
|
246
|
+
|
|
247
|
+
Adapter recipes, the four-primitive reactive contract and the Astro note:
|
|
248
|
+
[Multi-framework architecture](docs/guides/multi-framework.md).
|
|
249
|
+
|
|
250
|
+
## Why not Reactive Forms?
|
|
251
|
+
|
|
252
|
+
Reactive Forms is official, mature and battle-tested — if that is what your
|
|
253
|
+
team needs, keep it. This library trades ecosystem maturity for:
|
|
254
|
+
compile-checked field paths, signal-based state (zoneless-friendly, no RxJS)
|
|
255
|
+
and built-in async/cross-field validation, typed arrays, drafts, undo/redo
|
|
256
|
+
and devtools.
|
|
257
|
+
|
|
258
|
+
"No RxJS / no `@angular/forms`" means precisely: no runtime dependency, no
|
|
259
|
+
Observables in the public API, none used internally. The optional `/interop`
|
|
260
|
+
entry point is the single exception — it declares `@angular/forms` as an
|
|
261
|
+
_optional_ peer for CVA-based migration.
|
|
262
|
+
|
|
263
|
+
Full, honest comparison: [Compared with Reactive Forms](docs/guides/comparison-reactive-forms.md).
|
|
264
|
+
|
|
265
|
+
## Layers
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
Typed API (mdyForm) Declarative API Dynamic JSON config
|
|
269
|
+
\ | /
|
|
270
|
+
Shared Form Engine (@modyra/core)
|
|
271
|
+
|
|
|
272
|
+
Headless widget layer (@modyra/widgets)
|
|
273
|
+
|
|
|
274
|
+
Angular ─ React ─ Vue ─ Lit (one native binding each)
|
|
275
|
+
|
|
|
276
|
+
Headless integrations or UI catalogs + @modyra/styles themes
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The form engine — typed field trees, arrays, validation, drafts, undo/redo —
|
|
280
|
+
lives in [`@modyra/core`](packages/core), a zero-dependency package that
|
|
281
|
+
runs in plain Node. [`@modyra/widgets`](packages/widgets) adds headless
|
|
282
|
+
widget controllers and the interaction/accessibility contract shared by
|
|
283
|
+
every renderer. Each adapter implements the same four-primitive reactive
|
|
284
|
+
contract on its framework's native reactivity: Angular signals,
|
|
285
|
+
`@vue/reactivity`, React's `useSyncExternalStore`, Lit's ReactiveController.
|
|
286
|
+
|
|
287
|
+
## Angular entry points
|
|
288
|
+
|
|
289
|
+
| Import | Contents | Extra peer deps |
|
|
290
|
+
| :------------------------ | :---------------------------------------- | :------------------------------- |
|
|
291
|
+
| `@modyra/angular` | Full bundle: adapter + UI + tools | — |
|
|
292
|
+
| `@modyra/angular/adapter` | Headless Angular adapter layer only | — |
|
|
293
|
+
| `@modyra/angular/ui` | UI primitives and built-in renderers only | — |
|
|
294
|
+
| `@modyra/angular/zod` | `mdyFormFromSchema()` | `@modyra/zod` + `zod` (optional) |
|
|
295
|
+
| `@modyra/angular/interop` | `mdyCva` for Reactive Forms | `@angular/forms` (optional) |
|
|
296
|
+
|
|
297
|
+
## Documentation
|
|
298
|
+
|
|
299
|
+
The full index lives in [docs/README.md](docs/README.md). The shortlist:
|
|
300
|
+
|
|
301
|
+
- [Mental model](docs/guides/mental-model.md) — the state graph, field lifecycle, operation semantics
|
|
302
|
+
- [Typed forms](docs/guides/typed-forms.md) — schema, handles, `patch`/`getChanges`, async validation, field arrays, undo/redo, **drafts (read the security note)**, wizard, Zod
|
|
303
|
+
- [Usage modes](docs/guides/usage-modes.md) — declarative, explicit adapter, headless, validation semantics
|
|
304
|
+
- [UI toolkit](docs/guides/ui-toolkit.md) — renderer catalog, enterprise select, dynamic forms, CSS tokens
|
|
305
|
+
- [DevTools](docs/guides/devtools.md) — hotkey overlay, masking, production notes
|
|
306
|
+
- [I18n](docs/guides/i18n.md) — UI strings (en/it/de/fr/es), date/time value models, localized parsing
|
|
307
|
+
- [Reactive Forms interop](docs/guides/interop.md) · [Comparison](docs/guides/comparison-reactive-forms.md) · [Troubleshooting](docs/guides/troubleshooting.md)
|
|
308
|
+
|
|
309
|
+
Project policies: [security](SECURITY.md) · [contributing](CONTRIBUTING.md) · [changelog](CHANGELOG.md)
|
|
310
|
+
|
|
311
|
+
## Compatibility and status
|
|
312
|
+
|
|
313
|
+
- **Angular 21+** — the engine relies on stable signal APIs (`linkedSignal`,
|
|
314
|
+
`effect` semantics, signal-based inputs/queries) shipped in recent majors;
|
|
315
|
+
older majors are not tested and not supported.
|
|
316
|
+
- React ≥18, `@vue/reactivity` ≥3.4, Lit ≥3, `zod` ≥3.25 — each only for its
|
|
317
|
+
own adapter package.
|
|
318
|
+
- TypeScript strict mode; the library compiles with `strict` and
|
|
319
|
+
`strictTemplates`.
|
|
320
|
+
- Status: young library, actively developed, single maintainer. Unit and
|
|
321
|
+
type tests cover the core engine, every adapter and the widget layer
|
|
322
|
+
(`npm test`, `npm run test:core`, `npm run test:adapters`,
|
|
323
|
+
`npm run test:widgets`), plus a tree-shaking bundle check
|
|
324
|
+
(`npm run test:bundle`); browser, axe and visual tests are planned.
|
|
325
|
+
Pin your version and read release notes.
|
|
326
|
+
|
|
327
|
+
## Examples
|
|
328
|
+
|
|
329
|
+
`examples/{react,vue,lit}` implement the **same signup form** (name +
|
|
330
|
+
email, shared validators, agnostic devtools panel) so the adapters can be
|
|
331
|
+
compared side by side, with a runtime switcher across the shipped themes
|
|
332
|
+
(default, Material, iOS, Ionic). They import the **built** `@modyra/*`
|
|
333
|
+
packages from `node_modules` — the same artifacts users install, never the
|
|
334
|
+
library sources.
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
npm run demo:angular # Angular demo over the packaged build
|
|
338
|
+
npm run demo:react # http://localhost:4301
|
|
339
|
+
npm run demo:vue # http://localhost:4302
|
|
340
|
+
npm run demo:lit # http://localhost:4303
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Local development
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
pnpm install # workspace deps use the workspace: protocol — use pnpm
|
|
347
|
+
npm run build:lib # core + widgets + zod + Angular library build
|
|
348
|
+
npm run build:packages # core + widgets + zod/vue/react/lit + styles
|
|
349
|
+
npm start # Angular demo app
|
|
350
|
+
npm test # Angular unit + type tests
|
|
351
|
+
npm run test:adapters # zod/vue/react/lit node tests
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Brand
|
|
355
|
+
|
|
356
|
+
Logo, palette and typography live in
|
|
357
|
+
[`docs/assets/brand`](docs/assets/brand) — three soft modules (the
|
|
358
|
+
adapters) around a shared negative space (the core).
|
|
359
|
+
|
|
360
|
+
<img src="docs/assets/brand/palette.png" alt="Modyra palette — Indigo #7067FF, Violet #A855F7, Coral #FF6577, Night #0E0F16, Cloud #F8FAFC, Slate #94A3B8" width="420">
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
MIT © [Lorenzo Muscherà](https://github.com/lorenzomusche)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { MDY_DATE_LOCALE, MDY_DECLARATIVE_REGISTRY, MDY_FLOATING_LABELS_DEFAULT, MDY_FLOATING_LABELS_DENSITY_DEFAULT, MDY_FORM_ADAPTER, MDY_I18N_MESSAGES, MDY_INLINE_ERRORS, MdyDeclarativeAdapter, MdyTypedForm, angularReactivity, array, field, group, mdyForm, provideModyraLocale } from '@modyra/angular';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Public API Surface of @modyra/angular/adapter.
|
|
5
|
+
*
|
|
6
|
+
* Headless Angular bindings over the framework-agnostic core engine:
|
|
7
|
+
* typed form model, declarative adapter, core form contracts and DI tokens.
|
|
8
|
+
* No renderer components and no theme assets.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: this secondary entrypoint re-exports from @modyra/angular
|
|
11
|
+
* intentionally. ng-packagr secondary builds enforce rootDir boundaries,
|
|
12
|
+
* so direct imports from ../../src/... are not viable here.
|
|
13
|
+
*/
|
|
14
|
+
// Typed form (mdyForm)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generated bundle index. Do not edit.
|
|
18
|
+
*/
|
|
19
|
+
//# sourceMappingURL=modyra-angular-adapter.mjs.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, Injector, HostAttributeToken, signal, effect, untracked, forwardRef, Directive } from '@angular/core';
|
|
3
|
+
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
4
|
+
import { MDY_FORM_ADAPTER, MdyDeclarativeAdapter, MDY_DECLARATIVE_REGISTRY } from '@modyra/angular/adapter';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bridges any modyra renderer into classic Reactive Forms /
|
|
8
|
+
* `ngModel` — incremental adoption in existing codebases without an
|
|
9
|
+
* `<mdy-form>`:
|
|
10
|
+
*
|
|
11
|
+
* ```html
|
|
12
|
+
* <form [formGroup]="group">
|
|
13
|
+
* <mdy-control-text mdyCva name="email" formControlName="email" label="Email" />
|
|
14
|
+
* </form>
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The directive provides a private single-field adapter on the element (the
|
|
18
|
+
* renderer resolves it instead of a surrounding `<mdy-form>`) and implements
|
|
19
|
+
* `ControlValueAccessor` on top of it: `writeValue` → field value,
|
|
20
|
+
* user edits → `onChange`, first blur → `onTouched`, `setDisabledState` →
|
|
21
|
+
* field `disabled`. Validation stays in Reactive Forms — bind your own
|
|
22
|
+
* error display to the `FormControl`, or mirror errors into the renderer
|
|
23
|
+
* with `aria-*` inputs.
|
|
24
|
+
*/
|
|
25
|
+
class MdyCvaDirective {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.adapter = inject(MDY_FORM_ADAPTER);
|
|
28
|
+
this.injector = inject(Injector);
|
|
29
|
+
this.fieldName = inject(new HostAttributeToken("name"), { optional: true }) ?? "value";
|
|
30
|
+
this._disabled = signal(false, ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
|
|
31
|
+
/** True while writeValue applies a model→view update (must not re-emit). */
|
|
32
|
+
this._writing = false;
|
|
33
|
+
this._onChange = () => undefined;
|
|
34
|
+
this._onTouched = () => undefined;
|
|
35
|
+
if (this.fieldName === "value" &&
|
|
36
|
+
typeof ngDevMode !== "undefined" &&
|
|
37
|
+
ngDevMode) {
|
|
38
|
+
console.warn('[modyra] mdyCva host has no "name" attribute — using the fallback field name "value".');
|
|
39
|
+
}
|
|
40
|
+
const state = this._state();
|
|
41
|
+
this.adapter.setDisabled(this.fieldName, this._disabled.asReadonly());
|
|
42
|
+
// View → model: propagate user edits, skipping writeValue echoes and
|
|
43
|
+
// the first run — emitting the initial value would mark the FormControl
|
|
44
|
+
// dirty at startup, violating the CVA contract (R21).
|
|
45
|
+
let first = true;
|
|
46
|
+
effect(() => {
|
|
47
|
+
const value = state.value();
|
|
48
|
+
untracked(() => {
|
|
49
|
+
if (first) {
|
|
50
|
+
first = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (this._writing)
|
|
54
|
+
return;
|
|
55
|
+
this._onChange(value);
|
|
56
|
+
});
|
|
57
|
+
}, { injector: this.injector });
|
|
58
|
+
// First touch propagates the blur to Reactive Forms.
|
|
59
|
+
effect(() => {
|
|
60
|
+
const touched = state.touched();
|
|
61
|
+
untracked(() => {
|
|
62
|
+
if (touched)
|
|
63
|
+
this._onTouched();
|
|
64
|
+
});
|
|
65
|
+
}, { injector: this.injector });
|
|
66
|
+
}
|
|
67
|
+
_state() {
|
|
68
|
+
const ref = this.adapter.getField(this.fieldName);
|
|
69
|
+
if (!ref) {
|
|
70
|
+
throw new Error(`[modyra] mdyCva could not create field "${this.fieldName}"`);
|
|
71
|
+
}
|
|
72
|
+
return ref();
|
|
73
|
+
}
|
|
74
|
+
writeValue(value) {
|
|
75
|
+
this._writing = true;
|
|
76
|
+
try {
|
|
77
|
+
this._state().value.set(value ?? null);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this._writing = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
registerOnChange(fn) {
|
|
84
|
+
this._onChange = fn;
|
|
85
|
+
}
|
|
86
|
+
registerOnTouched(fn) {
|
|
87
|
+
this._onTouched = fn;
|
|
88
|
+
}
|
|
89
|
+
setDisabledState(isDisabled) {
|
|
90
|
+
this._disabled.set(isDisabled);
|
|
91
|
+
}
|
|
92
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MdyCvaDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
93
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.18", type: MdyCvaDirective, isStandalone: true, selector: "[mdyCva]", providers: [
|
|
94
|
+
{
|
|
95
|
+
provide: MDY_FORM_ADAPTER,
|
|
96
|
+
useFactory: () => new MdyDeclarativeAdapter(signal(undefined), signal("manual")),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
provide: MDY_DECLARATIVE_REGISTRY,
|
|
100
|
+
useExisting: MDY_FORM_ADAPTER,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
provide: NG_VALUE_ACCESSOR,
|
|
104
|
+
useExisting: forwardRef(() => MdyCvaDirective),
|
|
105
|
+
multi: true,
|
|
106
|
+
},
|
|
107
|
+
], ngImport: i0 }); }
|
|
108
|
+
}
|
|
109
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: MdyCvaDirective, decorators: [{
|
|
110
|
+
type: Directive,
|
|
111
|
+
args: [{
|
|
112
|
+
selector: "[mdyCva]",
|
|
113
|
+
standalone: true,
|
|
114
|
+
providers: [
|
|
115
|
+
{
|
|
116
|
+
provide: MDY_FORM_ADAPTER,
|
|
117
|
+
useFactory: () => new MdyDeclarativeAdapter(signal(undefined), signal("manual")),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
provide: MDY_DECLARATIVE_REGISTRY,
|
|
121
|
+
useExisting: MDY_FORM_ADAPTER,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
provide: NG_VALUE_ACCESSOR,
|
|
125
|
+
useExisting: forwardRef(() => MdyCvaDirective),
|
|
126
|
+
multi: true,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
}]
|
|
130
|
+
}], ctorParameters: () => [] });
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* Public API Surface of @modyra/angular/interop
|
|
134
|
+
*
|
|
135
|
+
* Interop with classic @angular/forms: the `mdyCva` directive makes every
|
|
136
|
+
* modyra renderer usable inside Reactive Forms / ngModel for
|
|
137
|
+
* incremental adoption. Requires the optional `@angular/forms` peer.
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generated bundle index. Do not edit.
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
export { MdyCvaDirective };
|
|
145
|
+
//# sourceMappingURL=modyra-angular-interop.mjs.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { MDY_DEVTOOLS_HOTKEY, MDY_FLOATING_LABELS, MdyA11yAnnouncer, MdyBaseControl, MdyCalendarCellComponent, MdyCalendarComponent, MdyCalendarGridComponent, MdyCalendarHeaderComponent, MdyCheckboxComponent, MdyChipsDirective, MdyColorsComponent, MdyConditionalOptionsDirective, MdyControlComponent, MdyControlLabelComponent, MdyDatePickerComponent, MdyDateRangePickerComponent, MdyDevtoolsDirective, MdyDisabledDirective, MdyDynamicFormComponent, MdyEmailDirective, MdyErrorListComponent, MdyFileComponent, MdyFloatingLabelsDirective, MdyFormArrayComponent, MdyFormComponent, MdyFormWizardComponent, MdyFormsDevtoolsComponent, MdyFormsDevtoolsOverlayComponent, MdyFormsDevtoolsService, MdyGlassDirective, MdyIconComponent, MdyInlineErrorIconComponent, MdyInlineErrorsDirective, MdyLoadOptionsDirective, MdyMaxDirective, MdyMaxLengthDirective, MdyMinDirective, MdyMinLengthDirective, MdyMultiselectComponent, MdyNumberComponent, MdyOptionDirective, MdyOptionsAutoLoadingDirective, MdyOptionsOverlayControl, MdyOverlayControl, MdyPatternDirective, MdyPrefixDirective, MdyRadioGroupComponent, MdyRangeCalendarComponent, MdyRangeCalendarGridComponent, MdyRequiredDirective, MdySegmentedButtonComponent, MdySelectComponent, MdySliderComponent, MdySuffixDirective, MdySupportingTextDirective, MdyTextComponent, MdyTextareaComponent, MdyTimepickerComponent, MdyToggleComponent, MdyValidatorBaseDirective, MdyWizardStepComponent } from '@modyra/angular';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Public API Surface of @modyra/angular/ui.
|
|
5
|
+
*
|
|
6
|
+
* Angular UI primitives and built-in renderers for modyra forms.
|
|
7
|
+
* This entry point intentionally excludes headless adapter contracts.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: this secondary entrypoint re-exports from @modyra/angular
|
|
10
|
+
* intentionally. ng-packagr secondary builds enforce rootDir boundaries,
|
|
11
|
+
* so direct imports from ../../src/... are not viable here.
|
|
12
|
+
*/
|
|
13
|
+
// Form containers
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generated bundle index. Do not edit.
|
|
17
|
+
*/
|
|
18
|
+
//# sourceMappingURL=modyra-angular-ui.mjs.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mdyForm } from '@modyra/angular/adapter';
|
|
2
|
+
import { buildZodTree, buildZodRefinementValidator } from '@modyra/zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds an Angular typed form from a `z.object()` schema — one source of
|
|
6
|
+
* truth for TypeScript types, validators, messages and required flags.
|
|
7
|
+
* See `@modyra/zod` for the shared semantics; bind the result with
|
|
8
|
+
* `[form]` and `[field]` exactly like `mdyForm()`.
|
|
9
|
+
*/
|
|
10
|
+
function mdyFormFromSchema(schema, options) {
|
|
11
|
+
const tree = buildZodTree(schema);
|
|
12
|
+
const refinementValidator = buildZodRefinementValidator(schema);
|
|
13
|
+
return mdyForm(tree, {
|
|
14
|
+
...(options?.submitMode !== undefined && { submitMode: options.submitMode }),
|
|
15
|
+
...(options?.injector !== undefined && { injector: options.injector }),
|
|
16
|
+
validators: [refinementValidator, ...(options?.validators ?? [])],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Public API Surface of @modyra/angular/zod
|
|
22
|
+
*
|
|
23
|
+
* Optional Zod adapter: derive a fully typed MdyTypedForm from a z.object()
|
|
24
|
+
* schema — types, validators, required flags and cross-field refinements
|
|
25
|
+
* from a single source of truth. Requires the `zod` peer (>= 3.25).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generated bundle index. Do not edit.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export { mdyFormFromSchema };
|
|
33
|
+
//# sourceMappingURL=modyra-angular-zod.mjs.map
|