@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.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +50 -0
- package/fesm2022/formancy-angular.mjs +1047 -0
- package/fesm2022/formancy-angular.mjs.map +1 -0
- package/package.json +34 -0
- package/types/formancy-angular.d.ts +346 -0
|
@@ -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
|