@formancy/angular 0.1.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.
@@ -0,0 +1,1047 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, signal, DestroyRef, computed, input, ChangeDetectionStrategy, Component, ViewContainerRef, Directive, Injector, runInInjectionContext, effect, output, viewChild, afterRenderEffect } from '@angular/core';
3
+ import { parsePath } from '@formancy/core';
4
+
5
+ /**
6
+ * The engine reaches components through DI, never through inputs drilling: a
7
+ * form is one engine instance, and every inject* helper resolves against it.
8
+ */
9
+ const FORMANCY_ENGINE = new InjectionToken('formancy.engine');
10
+ function provideFormancy(engine) {
11
+ return [{ provide: FORMANCY_ENGINE, useValue: engine }];
12
+ }
13
+ function injectEngine() {
14
+ const engine = inject(FORMANCY_ENGINE, { optional: true });
15
+ if (engine === null) {
16
+ throw new Error('formancy needs provideFormancy(engine) in the injector tree above this component');
17
+ }
18
+ return engine;
19
+ }
20
+
21
+ /**
22
+ * One field's live state as a signal — the Angular half of the same protocol
23
+ * React binds with useSyncExternalStore. The engine's snapshots are
24
+ * identity-stable, so `set` is a real change exactly when the field changed,
25
+ * which is what zoneless OnPush change detection needs: no zone, no
26
+ * ChangeDetectorRef, no whole-form re-render.
27
+ *
28
+ * Call from an injection context (field member initialiser or constructor).
29
+ */
30
+ function injectField(path) {
31
+ const engine = injectEngine();
32
+ const parsed = typeof path === 'string' ? parsePath(path) : path;
33
+ const snapshot = signal(engine.getFieldSnapshot(parsed), /* @ts-ignore */
34
+ ...(ngDevMode ? [{ debugName: "snapshot" }] : /* istanbul ignore next */ []));
35
+ const unsubscribe = engine.subscribeField(parsed, () => {
36
+ snapshot.set(engine.getFieldSnapshot(parsed));
37
+ });
38
+ inject(DestroyRef).onDestroy(unsubscribe);
39
+ return {
40
+ snapshot: snapshot.asReadonly(),
41
+ setValue: (value) => engine.setValue(parsed, value),
42
+ touch: () => engine.touch(parsed),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * A repeater's live row count as a signal — the Angular half of React's
48
+ * useRepeater. Reading rowCount eagerly doubles as validation: the engine
49
+ * throws on a non-repeater path here, at wiring time, not at first render.
50
+ *
51
+ * Call from an injection context (field member initialiser or constructor).
52
+ */
53
+ function injectRepeater(path) {
54
+ const engine = injectEngine();
55
+ const parsed = typeof path === 'string' ? parsePath(path) : path;
56
+ const rowCount = signal(engine.rowCount(parsed), /* @ts-ignore */
57
+ ...(ngDevMode ? [{ debugName: "rowCount" }] : /* istanbul ignore next */ []));
58
+ const unsubscribe = engine.subscribeField(parsed, () => {
59
+ rowCount.set(engine.rowCount(parsed));
60
+ });
61
+ inject(DestroyRef).onDestroy(unsubscribe);
62
+ return {
63
+ rowCount: rowCount.asReadonly(),
64
+ // Computed from rowCount: the engine mints an id when a row is created, so
65
+ // the count changing is exactly when the ids change.
66
+ rowIds: computed(() => Array.from({ length: rowCount() }, (_, index) => engine.rowId(parsed, index))),
67
+ addRow: () => engine.addRow(parsed),
68
+ removeRow: (index) => engine.removeRow(parsed, index),
69
+ };
70
+ }
71
+
72
+ /**
73
+ * The wizard's live position as a signal. Throws on an unpaged form rather
74
+ * than degrading to a one-page pseudo-wizard: a stepper rendered over a flat
75
+ * form is a bug in the caller, and a loud error at wiring beats a mute
76
+ * component. Same words as React's useWizard, because it is the same policy.
77
+ *
78
+ * Call from an injection context (field member initialiser or constructor).
79
+ */
80
+ function injectWizard() {
81
+ const engine = injectEngine();
82
+ const wizard = engine.wizard();
83
+ if (wizard === undefined) {
84
+ throw new Error('injectWizard needs a schema with pages; this form has none');
85
+ }
86
+ const page = signal(wizard.page(), /* @ts-ignore */
87
+ ...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
88
+ const unsubscribe = wizard.subscribe(() => page.set(wizard.page()));
89
+ inject(DestroyRef).onDestroy(unsubscribe);
90
+ return {
91
+ page: page.asReadonly(),
92
+ pageCount: wizard.pageCount,
93
+ next: () => wizard.next(),
94
+ back: () => wizard.back(),
95
+ goTo: (pageIndex) => wizard.goTo(pageIndex),
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Submit the form: touch everything, validate everything, and on failure move
101
+ * focus to the first invalid control — the WCAG-required behaviour that every
102
+ * consumer would otherwise have to hand-roll, and most would forget.
103
+ *
104
+ * Focus lives in the binding, not the engine, because it needs the DOM after
105
+ * the commit; the engine only names the field (firstInvalid) and mints the id.
106
+ * Same policy, word for word, as React's useSubmit — the fixtures hold both
107
+ * renderers to it.
108
+ */
109
+ function injectSubmit() {
110
+ const engine = injectEngine();
111
+ return () => {
112
+ const outcome = engine.submit();
113
+ if (!outcome.ok && typeof document !== 'undefined') {
114
+ // An ErrorSummary takes precedence: it lists every problem, and focusing
115
+ // it announces the situation once. It focuses ITSELF when errors appear,
116
+ // so nothing to do here. Without one, fall back to the first control.
117
+ const summary = document.querySelector('[data-formancy-part="error-summary"]');
118
+ if (summary === null) {
119
+ const firstInvalid = engine.firstInvalid();
120
+ if (firstInvalid !== null) {
121
+ const ids = engine.getFieldSnapshot(parsePath(firstInvalid)).ids;
122
+ document.getElementById(ids.control)?.focus();
123
+ }
124
+ }
125
+ }
126
+ return outcome;
127
+ };
128
+ }
129
+
130
+ const FORMANCY_FIELD_CONTEXT = new InjectionToken('formancy.field-context');
131
+ function injectFieldContext() {
132
+ const context = inject(FORMANCY_FIELD_CONTEXT, { optional: true });
133
+ if (context === null) {
134
+ throw new Error('formancy field components render inside a formancy-field slot, which provides FORMANCY_FIELD_CONTEXT for the path and label');
135
+ }
136
+ return context;
137
+ }
138
+ const FORMANCY_REGISTRY = new InjectionToken('formancy.registry');
139
+ function provideFormancyRegistry(registry) {
140
+ return [{ provide: FORMANCY_REGISTRY, useValue: registry }];
141
+ }
142
+
143
+ /**
144
+ * The built-in unstyled field components — the Angular rendering of the same
145
+ * decisions React's defaults made. Zero CSS; `data-formancy-part` is the
146
+ * styling hook; every id and ARIA attribute comes from the engine's prop
147
+ * getters so the wiring is byte-identical across renderers.
148
+ *
149
+ * The interpolations that become accessible text (labels, error codes) sit on
150
+ * one template line on purpose: element-internal whitespace would leak into
151
+ * textContent, and the conformance driver reads error codes verbatim.
152
+ */
153
+ /** Shared unstyled shell: real label, projected control, error text as the
154
+ * describedby target. */
155
+ class FormancyFieldShell {
156
+ field = input.required(/* @ts-ignore */
157
+ ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
158
+ label = input.required(/* @ts-ignore */
159
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
160
+ showError = computed(() => {
161
+ const snapshot = this.field().snapshot();
162
+ return snapshot.touched && snapshot.errors.length > 0;
163
+ }, /* @ts-ignore */
164
+ ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
165
+ errorText = computed(() => this.field().snapshot().errors.join(', '), /* @ts-ignore */
166
+ ...(ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []));
167
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyFieldShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
168
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: FormancyFieldShell, isStandalone: true, selector: "formancy-field-shell", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
169
+ <div data-formancy-part="field" [attr.data-state]="showError() ? 'invalid' : 'valid'">
170
+ <label data-formancy-part="label" [id]="field().snapshot().props.label.id" [attr.for]="field().snapshot().props.label.for">{{ label() }}</label>
171
+ <ng-content />
172
+ @if (showError()) {
173
+ <p data-formancy-part="error" [id]="field().snapshot().props.error.id">{{ errorText() }}</p>
174
+ }
175
+ </div>
176
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
177
+ }
178
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyFieldShell, decorators: [{
179
+ type: Component,
180
+ args: [{
181
+ selector: 'formancy-field-shell',
182
+ changeDetection: ChangeDetectionStrategy.OnPush,
183
+ template: `
184
+ <div data-formancy-part="field" [attr.data-state]="showError() ? 'invalid' : 'valid'">
185
+ <label data-formancy-part="label" [id]="field().snapshot().props.label.id" [attr.for]="field().snapshot().props.label.for">{{ label() }}</label>
186
+ <ng-content />
187
+ @if (showError()) {
188
+ <p data-formancy-part="error" [id]="field().snapshot().props.error.id">{{ errorText() }}</p>
189
+ }
190
+ </div>
191
+ `,
192
+ }]
193
+ }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }] } });
194
+ /** The state every leaf control shares; components extend it so the templates
195
+ * stay the only per-type code. Context comes through DI (see registry.ts). */
196
+ class FieldComponentBase {
197
+ context = injectFieldContext();
198
+ field = injectField(this.context.path);
199
+ control = computed(() => this.field.snapshot().props.control, /* @ts-ignore */
200
+ ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
201
+ engine = injectEngine();
202
+ /**
203
+ * Option labels resolved to strings, since a label may be a reference into
204
+ * the message catalogue. Falling back to the stored value keeps an
205
+ * untranslated option selectable rather than blank.
206
+ */
207
+ options = computed(() => (this.field.snapshot().def.options ?? []).map((option) => ({
208
+ value: option.value,
209
+ label: this.engine.text(option.label) ?? option.value,
210
+ })), /* @ts-ignore */
211
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
212
+ }
213
+ class FormancyTextField extends FieldComponentBase {
214
+ text = computed(() => {
215
+ const value = this.field.snapshot().value;
216
+ return typeof value === 'string' ? value : '';
217
+ }, /* @ts-ignore */
218
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
219
+ onInput(event) {
220
+ this.field.setValue(event.target.value);
221
+ }
222
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyTextField, deps: null, target: i0.ɵɵFactoryTarget.Component });
223
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: FormancyTextField, isStandalone: true, selector: "formancy-text-field", usesInheritance: true, ngImport: i0, template: `
224
+ <formancy-field-shell [field]="field" [label]="context.label">
225
+ <input
226
+ type="text"
227
+ [id]="control().id"
228
+ [attr.name]="control().name"
229
+ [attr.aria-invalid]="control()['aria-invalid']"
230
+ [attr.aria-required]="control()['aria-required']"
231
+ [attr.aria-describedby]="control()['aria-describedby']"
232
+ [disabled]="control().disabled === true"
233
+ [value]="text()"
234
+ (input)="onInput($event)"
235
+ (blur)="field.touch()"
236
+ />
237
+ </formancy-field-shell>
238
+ `, isInline: true, dependencies: [{ kind: "component", type: FormancyFieldShell, selector: "formancy-field-shell", inputs: ["field", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
239
+ }
240
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyTextField, decorators: [{
241
+ type: Component,
242
+ args: [{
243
+ selector: 'formancy-text-field',
244
+ changeDetection: ChangeDetectionStrategy.OnPush,
245
+ imports: [FormancyFieldShell],
246
+ template: `
247
+ <formancy-field-shell [field]="field" [label]="context.label">
248
+ <input
249
+ type="text"
250
+ [id]="control().id"
251
+ [attr.name]="control().name"
252
+ [attr.aria-invalid]="control()['aria-invalid']"
253
+ [attr.aria-required]="control()['aria-required']"
254
+ [attr.aria-describedby]="control()['aria-describedby']"
255
+ [disabled]="control().disabled === true"
256
+ [value]="text()"
257
+ (input)="onInput($event)"
258
+ (blur)="field.touch()"
259
+ />
260
+ </formancy-field-shell>
261
+ `,
262
+ }]
263
+ }] });
264
+ class FormancyTextareaField extends FieldComponentBase {
265
+ text = computed(() => {
266
+ const value = this.field.snapshot().value;
267
+ return typeof value === 'string' ? value : '';
268
+ }, /* @ts-ignore */
269
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
270
+ onInput(event) {
271
+ this.field.setValue(event.target.value);
272
+ }
273
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyTextareaField, deps: null, target: i0.ɵɵFactoryTarget.Component });
274
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: FormancyTextareaField, isStandalone: true, selector: "formancy-textarea-field", usesInheritance: true, ngImport: i0, template: `
275
+ <formancy-field-shell [field]="field" [label]="context.label">
276
+ <textarea
277
+ [id]="control().id"
278
+ [attr.name]="control().name"
279
+ [attr.aria-invalid]="control()['aria-invalid']"
280
+ [attr.aria-required]="control()['aria-required']"
281
+ [attr.aria-describedby]="control()['aria-describedby']"
282
+ [disabled]="control().disabled === true"
283
+ [value]="text()"
284
+ (input)="onInput($event)"
285
+ (blur)="field.touch()"
286
+ ></textarea>
287
+ </formancy-field-shell>
288
+ `, isInline: true, dependencies: [{ kind: "component", type: FormancyFieldShell, selector: "formancy-field-shell", inputs: ["field", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
289
+ }
290
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyTextareaField, decorators: [{
291
+ type: Component,
292
+ args: [{
293
+ selector: 'formancy-textarea-field',
294
+ changeDetection: ChangeDetectionStrategy.OnPush,
295
+ imports: [FormancyFieldShell],
296
+ template: `
297
+ <formancy-field-shell [field]="field" [label]="context.label">
298
+ <textarea
299
+ [id]="control().id"
300
+ [attr.name]="control().name"
301
+ [attr.aria-invalid]="control()['aria-invalid']"
302
+ [attr.aria-required]="control()['aria-required']"
303
+ [attr.aria-describedby]="control()['aria-describedby']"
304
+ [disabled]="control().disabled === true"
305
+ [value]="text()"
306
+ (input)="onInput($event)"
307
+ (blur)="field.touch()"
308
+ ></textarea>
309
+ </formancy-field-shell>
310
+ `,
311
+ }]
312
+ }] });
313
+ class FormancyNumberField extends FieldComponentBase {
314
+ text = computed(() => {
315
+ const value = this.field.snapshot().value;
316
+ return typeof value === 'number' ? String(value) : '';
317
+ }, /* @ts-ignore */
318
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
319
+ onInput(event) {
320
+ const raw = event.target.value;
321
+ this.field.setValue(raw === '' ? null : Number(raw));
322
+ }
323
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyNumberField, deps: null, target: i0.ɵɵFactoryTarget.Component });
324
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: FormancyNumberField, isStandalone: true, selector: "formancy-number-field", usesInheritance: true, ngImport: i0, template: `
325
+ <formancy-field-shell [field]="field" [label]="context.label">
326
+ <input
327
+ type="number"
328
+ [id]="control().id"
329
+ [attr.name]="control().name"
330
+ [attr.aria-invalid]="control()['aria-invalid']"
331
+ [attr.aria-required]="control()['aria-required']"
332
+ [attr.aria-describedby]="control()['aria-describedby']"
333
+ [disabled]="control().disabled === true"
334
+ [value]="text()"
335
+ (input)="onInput($event)"
336
+ (blur)="field.touch()"
337
+ />
338
+ </formancy-field-shell>
339
+ `, isInline: true, dependencies: [{ kind: "component", type: FormancyFieldShell, selector: "formancy-field-shell", inputs: ["field", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
340
+ }
341
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyNumberField, decorators: [{
342
+ type: Component,
343
+ args: [{
344
+ selector: 'formancy-number-field',
345
+ changeDetection: ChangeDetectionStrategy.OnPush,
346
+ imports: [FormancyFieldShell],
347
+ template: `
348
+ <formancy-field-shell [field]="field" [label]="context.label">
349
+ <input
350
+ type="number"
351
+ [id]="control().id"
352
+ [attr.name]="control().name"
353
+ [attr.aria-invalid]="control()['aria-invalid']"
354
+ [attr.aria-required]="control()['aria-required']"
355
+ [attr.aria-describedby]="control()['aria-describedby']"
356
+ [disabled]="control().disabled === true"
357
+ [value]="text()"
358
+ (input)="onInput($event)"
359
+ (blur)="field.touch()"
360
+ />
361
+ </formancy-field-shell>
362
+ `,
363
+ }]
364
+ }] });
365
+ class FormancyCheckboxField extends FieldComponentBase {
366
+ checked = computed(() => this.field.snapshot().value === true, /* @ts-ignore */
367
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
368
+ onChange(event) {
369
+ this.field.setValue(event.target.checked);
370
+ }
371
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyCheckboxField, deps: null, target: i0.ɵɵFactoryTarget.Component });
372
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: FormancyCheckboxField, isStandalone: true, selector: "formancy-checkbox-field", usesInheritance: true, ngImport: i0, template: `
373
+ <formancy-field-shell [field]="field" [label]="context.label">
374
+ <input
375
+ type="checkbox"
376
+ [id]="control().id"
377
+ [attr.name]="control().name"
378
+ [attr.aria-invalid]="control()['aria-invalid']"
379
+ [attr.aria-required]="control()['aria-required']"
380
+ [attr.aria-describedby]="control()['aria-describedby']"
381
+ [disabled]="control().disabled === true"
382
+ [checked]="checked()"
383
+ (change)="onChange($event)"
384
+ (blur)="field.touch()"
385
+ />
386
+ </formancy-field-shell>
387
+ `, isInline: true, dependencies: [{ kind: "component", type: FormancyFieldShell, selector: "formancy-field-shell", inputs: ["field", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
388
+ }
389
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyCheckboxField, decorators: [{
390
+ type: Component,
391
+ args: [{
392
+ selector: 'formancy-checkbox-field',
393
+ changeDetection: ChangeDetectionStrategy.OnPush,
394
+ imports: [FormancyFieldShell],
395
+ template: `
396
+ <formancy-field-shell [field]="field" [label]="context.label">
397
+ <input
398
+ type="checkbox"
399
+ [id]="control().id"
400
+ [attr.name]="control().name"
401
+ [attr.aria-invalid]="control()['aria-invalid']"
402
+ [attr.aria-required]="control()['aria-required']"
403
+ [attr.aria-describedby]="control()['aria-describedby']"
404
+ [disabled]="control().disabled === true"
405
+ [checked]="checked()"
406
+ (change)="onChange($event)"
407
+ (blur)="field.touch()"
408
+ />
409
+ </formancy-field-shell>
410
+ `,
411
+ }]
412
+ }] });
413
+ class FormancyDateField extends FieldComponentBase {
414
+ text = computed(() => {
415
+ const value = this.field.snapshot().value;
416
+ return typeof value === 'string' ? value : '';
417
+ }, /* @ts-ignore */
418
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
419
+ onInput(event) {
420
+ const raw = event.target.value;
421
+ this.field.setValue(raw === '' ? null : raw);
422
+ }
423
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyDateField, deps: null, target: i0.ɵɵFactoryTarget.Component });
424
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.7", type: FormancyDateField, isStandalone: true, selector: "formancy-date-field", usesInheritance: true, ngImport: i0, template: `
425
+ <formancy-field-shell [field]="field" [label]="context.label">
426
+ <input
427
+ type="date"
428
+ [id]="control().id"
429
+ [attr.name]="control().name"
430
+ [attr.aria-invalid]="control()['aria-invalid']"
431
+ [attr.aria-required]="control()['aria-required']"
432
+ [attr.aria-describedby]="control()['aria-describedby']"
433
+ [disabled]="control().disabled === true"
434
+ [value]="text()"
435
+ (input)="onInput($event)"
436
+ (blur)="field.touch()"
437
+ />
438
+ </formancy-field-shell>
439
+ `, isInline: true, dependencies: [{ kind: "component", type: FormancyFieldShell, selector: "formancy-field-shell", inputs: ["field", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
440
+ }
441
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyDateField, decorators: [{
442
+ type: Component,
443
+ args: [{
444
+ selector: 'formancy-date-field',
445
+ changeDetection: ChangeDetectionStrategy.OnPush,
446
+ imports: [FormancyFieldShell],
447
+ template: `
448
+ <formancy-field-shell [field]="field" [label]="context.label">
449
+ <input
450
+ type="date"
451
+ [id]="control().id"
452
+ [attr.name]="control().name"
453
+ [attr.aria-invalid]="control()['aria-invalid']"
454
+ [attr.aria-required]="control()['aria-required']"
455
+ [attr.aria-describedby]="control()['aria-describedby']"
456
+ [disabled]="control().disabled === true"
457
+ [value]="text()"
458
+ (input)="onInput($event)"
459
+ (blur)="field.touch()"
460
+ />
461
+ </formancy-field-shell>
462
+ `,
463
+ }]
464
+ }] });
465
+ class FormancySelectField extends FieldComponentBase {
466
+ selected = computed(() => {
467
+ const value = this.field.snapshot().value;
468
+ return typeof value === 'string' ? value : '';
469
+ }, /* @ts-ignore */
470
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
471
+ onChange(event) {
472
+ const raw = event.target.value;
473
+ this.field.setValue(raw === '' ? null : raw);
474
+ }
475
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancySelectField, deps: null, target: i0.ɵɵFactoryTarget.Component });
476
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: FormancySelectField, isStandalone: true, selector: "formancy-select-field", usesInheritance: true, ngImport: i0, template: `
477
+ <formancy-field-shell [field]="field" [label]="context.label">
478
+ <select
479
+ [id]="control().id"
480
+ [attr.name]="control().name"
481
+ [attr.aria-invalid]="control()['aria-invalid']"
482
+ [attr.aria-required]="control()['aria-required']"
483
+ [attr.aria-describedby]="control()['aria-describedby']"
484
+ [disabled]="control().disabled === true"
485
+ (change)="onChange($event)"
486
+ (blur)="field.touch()"
487
+ >
488
+ <!-- The empty option is the unanswered state; without it the browser
489
+ silently pre-selects the first real option, which the engine never
490
+ heard about. Selectedness is bound per option because a select's
491
+ value property is only settable once its options exist. -->
492
+ <option value="" [selected]="selected() === ''"></option>
493
+ @for (option of options(); track option.value) {
494
+ <option [value]="option.value" [selected]="selected() === option.value">{{ option.label }}</option>
495
+ }
496
+ </select>
497
+ </formancy-field-shell>
498
+ `, isInline: true, dependencies: [{ kind: "component", type: FormancyFieldShell, selector: "formancy-field-shell", inputs: ["field", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
499
+ }
500
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancySelectField, decorators: [{
501
+ type: Component,
502
+ args: [{
503
+ selector: 'formancy-select-field',
504
+ changeDetection: ChangeDetectionStrategy.OnPush,
505
+ imports: [FormancyFieldShell],
506
+ template: `
507
+ <formancy-field-shell [field]="field" [label]="context.label">
508
+ <select
509
+ [id]="control().id"
510
+ [attr.name]="control().name"
511
+ [attr.aria-invalid]="control()['aria-invalid']"
512
+ [attr.aria-required]="control()['aria-required']"
513
+ [attr.aria-describedby]="control()['aria-describedby']"
514
+ [disabled]="control().disabled === true"
515
+ (change)="onChange($event)"
516
+ (blur)="field.touch()"
517
+ >
518
+ <!-- The empty option is the unanswered state; without it the browser
519
+ silently pre-selects the first real option, which the engine never
520
+ heard about. Selectedness is bound per option because a select's
521
+ value property is only settable once its options exist. -->
522
+ <option value="" [selected]="selected() === ''"></option>
523
+ @for (option of options(); track option.value) {
524
+ <option [value]="option.value" [selected]="selected() === option.value">{{ option.label }}</option>
525
+ }
526
+ </select>
527
+ </formancy-field-shell>
528
+ `,
529
+ }]
530
+ }] });
531
+ class FormancyRadioGroupField extends FieldComponentBase {
532
+ showError = computed(() => {
533
+ const snapshot = this.field.snapshot();
534
+ return snapshot.touched && snapshot.errors.length > 0;
535
+ }, /* @ts-ignore */
536
+ ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
537
+ errorText = computed(() => this.field.snapshot().errors.join(', '), /* @ts-ignore */
538
+ ...(ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []));
539
+ optionId(option) {
540
+ return `${this.field.snapshot().ids.control}:${option.value}`;
541
+ }
542
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyRadioGroupField, deps: null, target: i0.ɵɵFactoryTarget.Component });
543
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: FormancyRadioGroupField, isStandalone: true, selector: "formancy-radio-group-field", usesInheritance: true, ngImport: i0, template: `
544
+ <fieldset
545
+ data-formancy-part="field"
546
+ [attr.data-state]="showError() ? 'invalid' : 'valid'"
547
+ [attr.aria-describedby]="control()['aria-describedby']"
548
+ >
549
+ <legend data-formancy-part="label">{{ context.label }}</legend>
550
+ @for (option of options(); track option.value) {
551
+ <span data-formancy-part="radio-option">
552
+ <input
553
+ type="radio"
554
+ [id]="optionId(option)"
555
+ [attr.name]="control().name"
556
+ [value]="option.value"
557
+ [checked]="field.snapshot().value === option.value"
558
+ (change)="field.setValue(option.value)"
559
+ (blur)="field.touch()"
560
+ />
561
+ <label [attr.for]="optionId(option)">{{ option.label }}</label>
562
+ </span>
563
+ }
564
+ @if (showError()) {
565
+ <p data-formancy-part="error" [id]="field.snapshot().props.error.id">{{ errorText() }}</p>
566
+ }
567
+ </fieldset>
568
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
569
+ }
570
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyRadioGroupField, decorators: [{
571
+ type: Component,
572
+ args: [{
573
+ selector: 'formancy-radio-group-field',
574
+ changeDetection: ChangeDetectionStrategy.OnPush,
575
+ template: `
576
+ <fieldset
577
+ data-formancy-part="field"
578
+ [attr.data-state]="showError() ? 'invalid' : 'valid'"
579
+ [attr.aria-describedby]="control()['aria-describedby']"
580
+ >
581
+ <legend data-formancy-part="label">{{ context.label }}</legend>
582
+ @for (option of options(); track option.value) {
583
+ <span data-formancy-part="radio-option">
584
+ <input
585
+ type="radio"
586
+ [id]="optionId(option)"
587
+ [attr.name]="control().name"
588
+ [value]="option.value"
589
+ [checked]="field.snapshot().value === option.value"
590
+ (change)="field.setValue(option.value)"
591
+ (blur)="field.touch()"
592
+ />
593
+ <label [attr.for]="optionId(option)">{{ option.label }}</label>
594
+ </span>
595
+ }
596
+ @if (showError()) {
597
+ <p data-formancy-part="error" [id]="field.snapshot().props.error.id">{{ errorText() }}</p>
598
+ }
599
+ </fieldset>
600
+ `,
601
+ }]
602
+ }] });
603
+ /**
604
+ * The built-in unstyled components. `null` means the type renders nothing here:
605
+ * hidden and static are non-inputs, and the container types are laid out by
606
+ * their own machinery, not by a leaf slot.
607
+ */
608
+ const DEFAULT_FIELD_COMPONENTS = {
609
+ text: FormancyTextField,
610
+ textarea: FormancyTextareaField,
611
+ number: FormancyNumberField,
612
+ checkbox: FormancyCheckboxField,
613
+ date: FormancyDateField,
614
+ select: FormancySelectField,
615
+ radio: FormancyRadioGroupField,
616
+ hidden: null,
617
+ static: null,
618
+ group: null,
619
+ page: null,
620
+ repeater: null,
621
+ };
622
+
623
+ /**
624
+ * A minimal dynamic-component outlet: NgComponentOutlet's job without pulling
625
+ * @angular/common into the library's runtime dependencies for one directive.
626
+ * Creation happens in ngOnChanges — the framework-blessed moment for view
627
+ * manipulation — and the injector input is how the slot hands each component
628
+ * its FORMANCY_FIELD_CONTEXT.
629
+ */
630
+ class FormancyComponentOutlet {
631
+ container = inject(ViewContainerRef);
632
+ formancyOutlet = input.required(/* @ts-ignore */
633
+ ...(ngDevMode ? [{ debugName: "formancyOutlet" }] : /* istanbul ignore next */ []));
634
+ formancyOutletInjector = input.required(/* @ts-ignore */
635
+ ...(ngDevMode ? [{ debugName: "formancyOutletInjector" }] : /* istanbul ignore next */ []));
636
+ ref;
637
+ ngOnChanges() {
638
+ this.ref?.destroy();
639
+ this.ref = this.container.createComponent(this.formancyOutlet(), {
640
+ injector: this.formancyOutletInjector(),
641
+ });
642
+ }
643
+ ngOnDestroy() {
644
+ this.ref?.destroy();
645
+ }
646
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyComponentOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive });
647
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: FormancyComponentOutlet, isStandalone: true, selector: "[formancyOutlet]", inputs: { formancyOutlet: { classPropertyName: "formancyOutlet", publicName: "formancyOutlet", isSignal: true, isRequired: true, transformFunction: null }, formancyOutletInjector: { classPropertyName: "formancyOutletInjector", publicName: "formancyOutletInjector", isSignal: true, isRequired: true, transformFunction: null } }, usesOnChanges: true, ngImport: i0 });
648
+ }
649
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyComponentOutlet, decorators: [{
650
+ type: Directive,
651
+ args: [{ selector: '[formancyOutlet]' }]
652
+ }], propDecorators: { formancyOutlet: [{ type: i0.Input, args: [{ isSignal: true, alias: "formancyOutlet", required: true }] }], formancyOutletInjector: [{ type: i0.Input, args: [{ isSignal: true, alias: "formancyOutletInjector", required: true }] }] } });
653
+ /**
654
+ * One field's slot: resolve the component through the registry (per-path beats
655
+ * per-type beats the defaults) and mount it while the field is visible.
656
+ *
657
+ * A hidden field leaves the DOM entirely: display:none would still ship the
658
+ * markup, keep it in the accessibility tree's shadow, and leak its labels to
659
+ * screen-reader "read all" passes.
660
+ *
661
+ * The binding is wired in ngOnInit rather than a field initialiser because the
662
+ * path arrives as an input, which does not exist yet at construction time.
663
+ */
664
+ class FormancyFieldSlot {
665
+ registry = inject(FORMANCY_REGISTRY, { optional: true });
666
+ injector = inject(Injector);
667
+ path = input.required(/* @ts-ignore */
668
+ ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
669
+ fallbackLabel = input(/* @ts-ignore */
670
+ ...(ngDevMode ? [undefined, { debugName: "fallbackLabel" }] : /* istanbul ignore next */ []));
671
+ state;
672
+ ngOnInit() {
673
+ const path = this.path();
674
+ const binding = runInInjectionContext(this.injector, () => injectField(path));
675
+ const snapshot = binding.snapshot;
676
+ const component = this.registry?.byPath?.[path] ??
677
+ this.registry?.byType?.[snapshot().type] ??
678
+ DEFAULT_FIELD_COMPONENTS[snapshot().type];
679
+ // Labels ride on the model definition (version-0 presentation-lite); the
680
+ // labels input is the fallback, and the wire path is at least honest.
681
+ const label = snapshot().label ?? this.fallbackLabel() ?? path;
682
+ this.state = {
683
+ snapshot,
684
+ component,
685
+ injector: Injector.create({
686
+ providers: [{ provide: FORMANCY_FIELD_CONTEXT, useValue: { path, label } }],
687
+ parent: this.injector,
688
+ }),
689
+ };
690
+ }
691
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyFieldSlot, deps: [], target: i0.ɵɵFactoryTarget.Component });
692
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: FormancyFieldSlot, isStandalone: true, selector: "formancy-field", inputs: { path: { classPropertyName: "path", publicName: "path", isSignal: true, isRequired: true, transformFunction: null }, fallbackLabel: { classPropertyName: "fallbackLabel", publicName: "fallbackLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
693
+ @if (state; as s) {
694
+ @if (s.component !== null && s.snapshot().visible) {
695
+ <ng-container [formancyOutlet]="s.component" [formancyOutletInjector]="s.injector" />
696
+ }
697
+ }
698
+ `, isInline: true, dependencies: [{ kind: "directive", type: FormancyComponentOutlet, selector: "[formancyOutlet]", inputs: ["formancyOutlet", "formancyOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
699
+ }
700
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyFieldSlot, decorators: [{
701
+ type: Component,
702
+ args: [{
703
+ selector: 'formancy-field',
704
+ changeDetection: ChangeDetectionStrategy.OnPush,
705
+ imports: [FormancyComponentOutlet],
706
+ template: `
707
+ @if (state; as s) {
708
+ @if (s.component !== null && s.snapshot().visible) {
709
+ <ng-container [formancyOutlet]="s.component" [formancyOutletInjector]="s.injector" />
710
+ }
711
+ }
712
+ `,
713
+ }]
714
+ }], propDecorators: { path: [{ type: i0.Input, args: [{ isSignal: true, alias: "path", required: true }] }], fallbackLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackLabel", required: false }] }] } });
715
+ /**
716
+ * A repeater's own chrome: a named fieldset, one row div per item with the row
717
+ * fields and a remove control, and an add control. Row fields render here,
718
+ * never in the flat list — a row needs its remove button and its position
719
+ * context.
720
+ */
721
+ class FormancyRepeaterSection {
722
+ engine = injectEngine();
723
+ injector = inject(Injector);
724
+ wire = input.required(/* @ts-ignore */
725
+ ...(ngDevMode ? [{ debugName: "wire" }] : /* istanbul ignore next */ []));
726
+ labels = input(/* @ts-ignore */
727
+ ...(ngDevMode ? [undefined, { debugName: "labels" }] : /* istanbul ignore next */ []));
728
+ state;
729
+ ngOnInit() {
730
+ const wire = this.wire();
731
+ const def = this.engine.repeaters().find((candidate) => candidate.wire === wire)?.def;
732
+ const label = this.engine.text(def?.label) ?? this.labels()?.[wire] ?? wire;
733
+ const minItems = def?.minItems ?? 0;
734
+ const repeater = runInInjectionContext(this.injector, () => {
735
+ const binding = injectRepeater(wire);
736
+ // Seed to minItems: a repeater that promises one row must show one empty
737
+ // row, not an add button and a shrug. An effect rather than a one-shot,
738
+ // so dropping below the floor later re-seeds exactly as React's does.
739
+ effect(() => {
740
+ const shortfall = minItems - binding.rowCount();
741
+ for (let i = 0; i < shortfall; i++)
742
+ binding.addRow();
743
+ });
744
+ return binding;
745
+ });
746
+ this.state = {
747
+ repeater,
748
+ label,
749
+ addLabel: def?.addLabel ?? `Add ${label}`,
750
+ removeLabel: def?.removeLabel ?? `Remove ${label}`,
751
+ rows: computed(() => Array.from({ length: repeater.rowCount() }, (_, index) => ({
752
+ index,
753
+ // Identity, not position: removing a row renumbers everything after
754
+ // it, and tracking by index would make Angular reuse the wrong DOM
755
+ // nodes — moving focus and animating the wrong element.
756
+ id: repeater.rowIds()[index] ?? String(index),
757
+ wires: this.engine.fieldPaths().filter((candidate) => candidate.startsWith(`${wire}[${index}]`)),
758
+ }))),
759
+ };
760
+ }
761
+ fallbackFor(instanceWire) {
762
+ const template = instanceWire.replace(/\[\d+\]/, '[]');
763
+ return this.labels()?.[template] ?? this.labels()?.[instanceWire];
764
+ }
765
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyRepeaterSection, deps: [], target: i0.ɵɵFactoryTarget.Component });
766
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: FormancyRepeaterSection, isStandalone: true, selector: "formancy-repeater", inputs: { wire: { classPropertyName: "wire", publicName: "wire", isSignal: true, isRequired: true, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
767
+ @if (state; as s) {
768
+ <fieldset data-formancy-part="repeater">
769
+ <legend data-formancy-part="repeater-legend">{{ s.label }}</legend>
770
+ @for (row of s.rows(); track row.id) {
771
+ <div data-formancy-part="row">
772
+ @for (instanceWire of row.wires; track instanceWire) {
773
+ <formancy-field [path]="instanceWire" [fallbackLabel]="fallbackFor(instanceWire)" />
774
+ }
775
+ <!-- Position context in the NAME, so a screen-reader user knows
776
+ which row this button kills without walking the tree. -->
777
+ <button type="button" (click)="s.repeater.removeRow(row.index)">{{ s.removeLabel }} {{ row.index + 1 }} of {{ s.rows().length }}</button>
778
+ </div>
779
+ }
780
+ <button type="button" (click)="s.repeater.addRow()">{{ s.addLabel }}</button>
781
+ </fieldset>
782
+ }
783
+ `, isInline: true, dependencies: [{ kind: "component", type: FormancyFieldSlot, selector: "formancy-field", inputs: ["path", "fallbackLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
784
+ }
785
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyRepeaterSection, decorators: [{
786
+ type: Component,
787
+ args: [{
788
+ selector: 'formancy-repeater',
789
+ changeDetection: ChangeDetectionStrategy.OnPush,
790
+ imports: [FormancyFieldSlot],
791
+ template: `
792
+ @if (state; as s) {
793
+ <fieldset data-formancy-part="repeater">
794
+ <legend data-formancy-part="repeater-legend">{{ s.label }}</legend>
795
+ @for (row of s.rows(); track row.id) {
796
+ <div data-formancy-part="row">
797
+ @for (instanceWire of row.wires; track instanceWire) {
798
+ <formancy-field [path]="instanceWire" [fallbackLabel]="fallbackFor(instanceWire)" />
799
+ }
800
+ <!-- Position context in the NAME, so a screen-reader user knows
801
+ which row this button kills without walking the tree. -->
802
+ <button type="button" (click)="s.repeater.removeRow(row.index)">{{ s.removeLabel }} {{ row.index + 1 }} of {{ s.rows().length }}</button>
803
+ </div>
804
+ }
805
+ <button type="button" (click)="s.repeater.addRow()">{{ s.addLabel }}</button>
806
+ </fieldset>
807
+ }
808
+ `,
809
+ }]
810
+ }], propDecorators: { wire: [{ type: i0.Input, args: [{ isSignal: true, alias: "wire", required: true }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }] } });
811
+ /**
812
+ * Renders the whole form from the engine: one slot per field, resolved through
813
+ * the registry. Slots subscribe individually, so a keystroke re-renders one
814
+ * field and a visibility flip mounts or unmounts exactly the fields it hit.
815
+ * A paged schema renders a stepper, one page at a time, navigation, and the
816
+ * submit control on the last page — a failed submit navigates to the first
817
+ * page with a problem instead of leaving the user on a clean review page
818
+ * staring at a rejection.
819
+ */
820
+ class FormancyForm {
821
+ engine = injectEngine();
822
+ submit = injectSubmit();
823
+ wizard = this.engine.wizard() === undefined ? undefined : injectWizard();
824
+ /**
825
+ * Display text per wire path (row fields by their template wire,
826
+ * `items[].name`). A `label` on the model definition wins — that is how
827
+ * fixture schemas carry text until the spec's i18n section lands.
828
+ */
829
+ labels = input(/* @ts-ignore */
830
+ ...(ngDevMode ? [undefined, { debugName: "labels" }] : /* istanbul ignore next */ []));
831
+ submitLabel = input(/* @ts-ignore */
832
+ ...(ngDevMode ? [undefined, { debugName: "submitLabel" }] : /* istanbul ignore next */ []));
833
+ submitted = output();
834
+ pages = this.engine.pages();
835
+ repeaterWires = this.engine.repeaterPaths();
836
+ // Row fields render inside their repeater's own section, never in the flat
837
+ // list — a row needs its remove button and its position context. The static
838
+ // wires never change: visibility is per-slot metadata, not list membership.
839
+ staticWires = this.engine
840
+ .fieldPaths()
841
+ .filter((wire) => !this.repeaterWires.some((repeater) => wire.startsWith(`${repeater}[`)));
842
+ staticOnPage = computed(() => {
843
+ const wizard = this.wizard;
844
+ if (wizard === undefined)
845
+ return this.staticWires;
846
+ return this.staticWires.filter((wire) => this.engine.pageOf(parsePath(wire)) === wizard.page());
847
+ }, /* @ts-ignore */
848
+ ...(ngDevMode ? [{ debugName: "staticOnPage" }] : /* istanbul ignore next */ []));
849
+ repeatersOnPage = computed(() => {
850
+ const wizard = this.wizard;
851
+ if (wizard === undefined)
852
+ return this.repeaterWires;
853
+ return this.repeaterWires.filter((wire) => this.engine.pageOf(parsePath(wire)) === wizard.page());
854
+ }, /* @ts-ignore */
855
+ ...(ngDevMode ? [{ debugName: "repeatersOnPage" }] : /* istanbul ignore next */ []));
856
+ fallbackFor(wire) {
857
+ return this.labels()?.[wire];
858
+ }
859
+ pageLabel(page) {
860
+ return this.engine.text(page.def.label) ?? page.key;
861
+ }
862
+ onNext() {
863
+ void this.wizard?.next();
864
+ }
865
+ onSubmit() {
866
+ const outcome = this.submit();
867
+ if (!outcome.ok && this.wizard !== undefined) {
868
+ // Take the user TO the problem: a rejection on a clean review page
869
+ // explains nothing.
870
+ const firstInvalid = this.engine.firstInvalid();
871
+ if (firstInvalid !== null)
872
+ this.wizard.goTo(this.engine.pageOf(parsePath(firstInvalid)));
873
+ }
874
+ this.submitted.emit(outcome.ok
875
+ ? { ok: true, errors: outcome.errors, data: this.engine.value() }
876
+ : { ok: false, errors: outcome.errors });
877
+ }
878
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
879
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: FormancyForm, isStandalone: true, selector: "formancy-form", inputs: { labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, submitLabel: { classPropertyName: "submitLabel", publicName: "submitLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted" }, ngImport: i0, template: `
880
+ @if (wizard; as w) {
881
+ <nav data-formancy-part="stepper" aria-label="Progress">
882
+ <ol>
883
+ @for (page of pages; track page.key; let i = $index) {
884
+ <li [attr.aria-current]="i === w.page() ? 'step' : null">{{ pageLabel(page) }}</li>
885
+ }
886
+ </ol>
887
+ </nav>
888
+ @for (wire of staticOnPage(); track wire) {
889
+ <formancy-field [path]="wire" [fallbackLabel]="fallbackFor(wire)" />
890
+ }
891
+ @for (wire of repeatersOnPage(); track wire) {
892
+ <formancy-repeater [wire]="wire" [labels]="labels()" />
893
+ }
894
+ <div data-formancy-part="wizard-nav">
895
+ @if (w.page() > 0) {
896
+ <button type="button" (click)="w.back()">Back</button>
897
+ }
898
+ @if (w.page() < w.pageCount - 1) {
899
+ <button type="button" (click)="onNext()">Next</button>
900
+ } @else {
901
+ <button type="button" data-formancy-part="submit" (click)="onSubmit()">{{ submitLabel() ?? 'Submit' }}</button>
902
+ }
903
+ </div>
904
+ } @else {
905
+ @for (wire of staticWires; track wire) {
906
+ <formancy-field [path]="wire" [fallbackLabel]="fallbackFor(wire)" />
907
+ }
908
+ @for (wire of repeaterWires; track wire) {
909
+ <formancy-repeater [wire]="wire" [labels]="labels()" />
910
+ }
911
+ <button type="button" data-formancy-part="submit" (click)="onSubmit()">{{ submitLabel() ?? 'Submit' }}</button>
912
+ }
913
+ `, isInline: true, dependencies: [{ kind: "component", type: FormancyFieldSlot, selector: "formancy-field", inputs: ["path", "fallbackLabel"] }, { kind: "component", type: FormancyRepeaterSection, selector: "formancy-repeater", inputs: ["wire", "labels"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
914
+ }
915
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyForm, decorators: [{
916
+ type: Component,
917
+ args: [{
918
+ selector: 'formancy-form',
919
+ changeDetection: ChangeDetectionStrategy.OnPush,
920
+ imports: [FormancyFieldSlot, FormancyRepeaterSection],
921
+ template: `
922
+ @if (wizard; as w) {
923
+ <nav data-formancy-part="stepper" aria-label="Progress">
924
+ <ol>
925
+ @for (page of pages; track page.key; let i = $index) {
926
+ <li [attr.aria-current]="i === w.page() ? 'step' : null">{{ pageLabel(page) }}</li>
927
+ }
928
+ </ol>
929
+ </nav>
930
+ @for (wire of staticOnPage(); track wire) {
931
+ <formancy-field [path]="wire" [fallbackLabel]="fallbackFor(wire)" />
932
+ }
933
+ @for (wire of repeatersOnPage(); track wire) {
934
+ <formancy-repeater [wire]="wire" [labels]="labels()" />
935
+ }
936
+ <div data-formancy-part="wizard-nav">
937
+ @if (w.page() > 0) {
938
+ <button type="button" (click)="w.back()">Back</button>
939
+ }
940
+ @if (w.page() < w.pageCount - 1) {
941
+ <button type="button" (click)="onNext()">Next</button>
942
+ } @else {
943
+ <button type="button" data-formancy-part="submit" (click)="onSubmit()">{{ submitLabel() ?? 'Submit' }}</button>
944
+ }
945
+ </div>
946
+ } @else {
947
+ @for (wire of staticWires; track wire) {
948
+ <formancy-field [path]="wire" [fallbackLabel]="fallbackFor(wire)" />
949
+ }
950
+ @for (wire of repeaterWires; track wire) {
951
+ <formancy-repeater [wire]="wire" [labels]="labels()" />
952
+ }
953
+ <button type="button" data-formancy-part="submit" (click)="onSubmit()">{{ submitLabel() ?? 'Submit' }}</button>
954
+ }
955
+ `,
956
+ }]
957
+ }], propDecorators: { labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], submitLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitLabel", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }] } });
958
+
959
+ /**
960
+ * The error summary a failed submit focuses.
961
+ *
962
+ * The semantics are deliberate and easy to get wrong: the container takes
963
+ * focus via tabindex="-1" — focusing it already makes screen readers announce
964
+ * it, so role="alert" would announce it TWICE. Each entry is a real in-page
965
+ * link to the offending control, because links are what "take me to the
966
+ * problem" already means to assistive tech.
967
+ */
968
+ class FormancyErrorSummary {
969
+ labels = input(/* @ts-ignore */
970
+ ...(ngDevMode ? [undefined, { debugName: "labels" }] : /* istanbul ignore next */ []));
971
+ engine = injectEngine();
972
+ region = viewChild('region', /* @ts-ignore */
973
+ ...(ngDevMode ? [{ debugName: "region" }] : /* istanbul ignore next */ []));
974
+ errors;
975
+ constructor() {
976
+ const errors = signal(this.engine.visibleErrors(), /* @ts-ignore */
977
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
978
+ const unsubscribe = this.engine.subscribe(() => errors.set(this.engine.visibleErrors()));
979
+ inject(DestroyRef).onDestroy(unsubscribe);
980
+ this.errors = errors.asReadonly();
981
+ // Focus when errors APPEAR (a submit surfaced them), not on every
982
+ // keystroke that edits an already-broken field. afterRenderEffect, because
983
+ // the container only exists in the DOM once the render that shows it ran.
984
+ let previousCount = 0;
985
+ afterRenderEffect(() => {
986
+ const count = this.errors().length;
987
+ if (count > 0 && previousCount === 0)
988
+ this.region()?.nativeElement.focus();
989
+ previousCount = count;
990
+ });
991
+ }
992
+ heading = computed(() => {
993
+ const count = this.errors().length;
994
+ return count === 1 ? 'There is 1 problem to fix' : `There are ${count} problems to fix`;
995
+ }, /* @ts-ignore */
996
+ ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
997
+ labelFor(path) {
998
+ return this.labels()?.[path] ?? this.labels()?.[path.replace(/\[\d+\]/, '[]')] ?? path;
999
+ }
1000
+ controlIdOf(path) {
1001
+ return this.engine.getFieldSnapshot(parsePath(path)).ids.control;
1002
+ }
1003
+ focusControl(event, path) {
1004
+ // The hash alone scrolls but does not focus; do both.
1005
+ event.preventDefault();
1006
+ document.getElementById(this.controlIdOf(path))?.focus();
1007
+ }
1008
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyErrorSummary, deps: [], target: i0.ɵɵFactoryTarget.Component });
1009
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: FormancyErrorSummary, isStandalone: true, selector: "formancy-error-summary", inputs: { labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "region", first: true, predicate: ["region"], descendants: true, isSignal: true }], ngImport: i0, template: `
1010
+ @if (errors().length > 0) {
1011
+ <div data-formancy-part="error-summary" tabindex="-1" #region>
1012
+ <h2 data-formancy-part="error-summary-heading">{{ heading() }}</h2>
1013
+ <ul>
1014
+ @for (entry of errors(); track entry.path) {
1015
+ <li><a [attr.href]="'#' + controlIdOf(entry.path)" (click)="focusControl($event, entry.path)">{{ labelFor(entry.path) }}: {{ entry.codes.join(', ') }}</a></li>
1016
+ }
1017
+ </ul>
1018
+ </div>
1019
+ }
1020
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1021
+ }
1022
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: FormancyErrorSummary, decorators: [{
1023
+ type: Component,
1024
+ args: [{
1025
+ selector: 'formancy-error-summary',
1026
+ changeDetection: ChangeDetectionStrategy.OnPush,
1027
+ template: `
1028
+ @if (errors().length > 0) {
1029
+ <div data-formancy-part="error-summary" tabindex="-1" #region>
1030
+ <h2 data-formancy-part="error-summary-heading">{{ heading() }}</h2>
1031
+ <ul>
1032
+ @for (entry of errors(); track entry.path) {
1033
+ <li><a [attr.href]="'#' + controlIdOf(entry.path)" (click)="focusControl($event, entry.path)">{{ labelFor(entry.path) }}: {{ entry.codes.join(', ') }}</a></li>
1034
+ }
1035
+ </ul>
1036
+ </div>
1037
+ }
1038
+ `,
1039
+ }]
1040
+ }], ctorParameters: () => [], propDecorators: { labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], region: [{ type: i0.ViewChild, args: ['region', { isSignal: true }] }] } });
1041
+
1042
+ /**
1043
+ * Generated bundle index. Do not edit.
1044
+ */
1045
+
1046
+ export { DEFAULT_FIELD_COMPONENTS, FORMANCY_ENGINE, FORMANCY_FIELD_CONTEXT, FORMANCY_REGISTRY, FormancyCheckboxField, FormancyComponentOutlet, FormancyDateField, FormancyErrorSummary, FormancyFieldShell, FormancyFieldSlot, FormancyForm, FormancyNumberField, FormancyRadioGroupField, FormancyRepeaterSection, FormancySelectField, FormancyTextField, FormancyTextareaField, injectEngine, injectField, injectFieldContext, injectRepeater, injectSubmit, injectWizard, provideFormancy, provideFormancyRegistry };
1047
+ //# sourceMappingURL=formancy-angular.mjs.map