@mosaicoo/form-angular 0.2.0 → 0.4.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.
@@ -1,7 +1,7 @@
1
1
  import { NgComponentOutlet, DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { InjectionToken, makeEnvironmentProviders, input, signal, inject, effect, untracked, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, output } from '@angular/core';
4
- import { importForm, createLegacyImporter, createFormEngine, collectInputs } from '@mosaicoo/form-core';
3
+ import { InjectionToken, makeEnvironmentProviders, input, signal, inject, ChangeDetectorRef, effect, untracked, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, output } from '@angular/core';
4
+ import { maskValue, importForm, createLegacyImporter, createFormEngine, collectInputs } from '@mosaicoo/form-core';
5
5
 
6
6
  /** Multi-provider token: maps merge, later providers win on conflicts. */
7
7
  const MFORM_FIELD_COMPONENTS = new InjectionToken('MFORM_FIELD_COMPONENTS');
@@ -11,6 +11,7 @@ const MFORM_FIELD_COMPONENTS = new InjectionToken('MFORM_FIELD_COMPONENTS');
11
11
  * base URLs, caching). Without it, a plain `fetch` is used.
12
12
  */
13
13
  const MFORM_REMOTE_FETCHER = new InjectionToken('MFORM_REMOTE_FETCHER');
14
+ const MFORM_UPLOAD = new InjectionToken('MFORM_UPLOAD');
14
15
  /**
15
16
  * Registers renderer extensions at any injector level:
16
17
  *
@@ -33,6 +34,9 @@ function provideMosaicooForm(config) {
33
34
  if (config.remoteFetcher) {
34
35
  providers.push({ provide: MFORM_REMOTE_FETCHER, useValue: config.remoteFetcher });
35
36
  }
37
+ if (config.uploadFiles) {
38
+ providers.push({ provide: MFORM_UPLOAD, useValue: config.uploadFiles });
39
+ }
36
40
  return makeEnvironmentProviders(providers);
37
41
  }
38
42
 
@@ -53,11 +57,36 @@ class FormNodeComponent {
53
57
  bare = input(false, ...(ngDevMode ? [{ debugName: "bare" }] : /* istanbul ignore next */ []));
54
58
  /** Host-registered providers for `optionsSource: provider` fields. */
55
59
  providers = input({}, ...(ngDevMode ? [{ debugName: "providers" }] : /* istanbul ignore next */ []));
60
+ /** Rendering mode inherited from the renderer. */
61
+ mode = input('edit', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
62
+ /** Renderer-supplied refresh hook for async completions (zoneless-safe). */
63
+ notify = input(() => { }, ...(ngDevMode ? [{ debugName: "notify" }] : /* istanbul ignore next */ []));
56
64
  activeTab = signal(0, ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
57
65
  /** DI-registered custom field components (maps merge; later wins). */
58
66
  componentMaps = inject(MFORM_FIELD_COMPONENTS, { optional: true });
59
67
  /** Host transport for remote options; plain fetch when absent. */
60
68
  remoteFetcher = inject(MFORM_REMOTE_FETCHER, { optional: true });
69
+ /** Host-owned upload for `file` fields. */
70
+ uploader = inject(MFORM_UPLOAD, { optional: true });
71
+ /**
72
+ * Async resolutions (debounce timers, provider promises) finish outside
73
+ * any template event — explicitly schedule the refresh for zoneless apps.
74
+ */
75
+ changeDetector = inject(ChangeDetectorRef);
76
+ uploading = signal(false, ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
77
+ /** Effective disabled state: mode, schema flag or calculated field. */
78
+ controlDisabled() {
79
+ return (this.mode() !== 'edit' ||
80
+ this.field().disabled === true ||
81
+ this.field().calculate !== undefined);
82
+ }
83
+ interactive() {
84
+ return this.mode() === 'edit';
85
+ }
86
+ pending() {
87
+ this.tick();
88
+ return this.engine().isPending(this.path());
89
+ }
61
90
  /** Custom component registered for this field type, if any. */
62
91
  customComponent() {
63
92
  const node = this.node();
@@ -119,7 +148,11 @@ class FormNodeComponent {
119
148
  if (!provider)
120
149
  return; // no provider: static options/fallback stand
121
150
  Promise.resolve(provider({ field: node, path, data: engine.getData(), args: source.args }))
122
- .then(apply)
151
+ .then((options) => {
152
+ apply(options);
153
+ this.changeDetector.markForCheck();
154
+ this.notify()();
155
+ })
123
156
  .catch(() => { }); // provider failure keeps the static fallback
124
157
  }
125
158
  else {
@@ -133,6 +166,7 @@ class FormNodeComponent {
133
166
  label: String(item[source.labelKey ?? 'label'] ?? item[source.valueKey ?? 'value'] ?? ''),
134
167
  value: (item[source.valueKey ?? 'value'] ?? ''),
135
168
  }))))
169
+ .then(() => this.changeDetector.markForCheck())
136
170
  .catch(() => { });
137
171
  }
138
172
  });
@@ -217,7 +251,12 @@ class FormNodeComponent {
217
251
  }
218
252
  stringValue() {
219
253
  const value = this.value();
220
- return value === undefined || value === null ? '' : String(value);
254
+ if (value === undefined || value === null)
255
+ return '';
256
+ const mask = this.field().format?.mask;
257
+ if (mask && typeof value === 'string')
258
+ return maskValue(value, mask).masked;
259
+ return String(value);
221
260
  }
222
261
  mapValue() {
223
262
  const value = this.value();
@@ -275,7 +314,15 @@ class FormNodeComponent {
275
314
  this.engine().setValue(this.path(), value);
276
315
  }
277
316
  setString(event) {
278
- const raw = event.target.value;
317
+ const target = event.target;
318
+ const raw = target.value;
319
+ const mask = this.field().format?.mask;
320
+ if (mask) {
321
+ const { masked, raw: stripped } = maskValue(raw, mask);
322
+ target.value = masked; // the control always shows the masked shape
323
+ this.setValue(this.field().format?.keepMask ? masked : stripped);
324
+ return;
325
+ }
279
326
  if (this.field().dataType === 'number') {
280
327
  const parsed = raw === '' ? undefined : Number(raw);
281
328
  this.setValue(parsed !== undefined && Number.isFinite(parsed) ? parsed : undefined);
@@ -296,11 +343,124 @@ class FormNodeComponent {
296
343
  }
297
344
  setFiles(event) {
298
345
  const files = event.target.files;
299
- this.setValue(files ? Array.from(files).map((f) => f.name) : []);
346
+ const list = files ? Array.from(files) : [];
347
+ if (!this.uploader || list.length === 0) {
348
+ // no host uploader: only file names are stored
349
+ this.setValue(list.map((f) => f.name));
350
+ return;
351
+ }
352
+ const field = this.field();
353
+ const path = this.path();
354
+ this.uploading.set(true);
355
+ Promise.all(list.map((file) => this.uploader(file, { field, path })))
356
+ .then((refs) => this.setValue(refs))
357
+ .catch(() => { }) // host upload failure keeps the previous value
358
+ .finally(() => {
359
+ this.uploading.set(false);
360
+ this.changeDetector.markForCheck();
361
+ this.notify()();
362
+ });
363
+ }
364
+ /** Names shown under a `file` control (refs or plain names). */
365
+ fileNames() {
366
+ const value = this.value();
367
+ if (!Array.isArray(value))
368
+ return [];
369
+ return value.map((item) => typeof item === 'object' && item !== null
370
+ ? String(item.name ?? '')
371
+ : String(item));
372
+ }
373
+ prefix() {
374
+ const value = this.node().props?.['prefix'];
375
+ return typeof value === 'string' && value ? value : null;
376
+ }
377
+ suffix() {
378
+ const value = this.node().props?.['suffix'];
379
+ return typeof value === 'string' && value ? value : null;
380
+ }
381
+ tagValues() {
382
+ const value = this.value();
383
+ return Array.isArray(value) ? value.map(String) : [];
384
+ }
385
+ addTag(event) {
386
+ event.preventDefault();
387
+ const input = event.target;
388
+ const tag = input.value.trim();
389
+ if (!tag)
390
+ return;
391
+ this.setValue([...this.tagValues(), tag]);
392
+ input.value = '';
393
+ }
394
+ removeTag(index) {
395
+ const next = [...this.tagValues()];
396
+ next.splice(index, 1);
397
+ this.setValue(next);
300
398
  }
301
399
  touch() {
302
400
  this.engine().markTouched(this.path());
303
- this.engine().validateField(this.path());
401
+ // Async-aware: resolves immediately for sync-only fields; pending events
402
+ // drive the busy indicator otherwise.
403
+ void this.engine().validateFieldAsync(this.path());
404
+ }
405
+ // -- reference (autocomplete) ---------------------------------------------
406
+ refOptions = signal([], ...(ngDevMode ? [{ debugName: "refOptions" }] : /* istanbul ignore next */ []));
407
+ refOpen = signal(false, ...(ngDevMode ? [{ debugName: "refOpen" }] : /* istanbul ignore next */ []));
408
+ refTimer = null;
409
+ /** Label of the chosen option (falls back to the raw value). */
410
+ refLabel = signal(null, ...(ngDevMode ? [{ debugName: "refLabel" }] : /* istanbul ignore next */ []));
411
+ refDisplay() {
412
+ return this.refLabel() ?? this.stringValue();
413
+ }
414
+ refSearch(event) {
415
+ const query = event.target.value;
416
+ this.refLabel.set(null);
417
+ if (this.refTimer)
418
+ clearTimeout(this.refTimer);
419
+ this.refTimer = setTimeout(() => this.refResolve(query), 250);
420
+ }
421
+ refResolve(query) {
422
+ const source = this.field().optionsSource;
423
+ if (!source || source.type !== 'provider') {
424
+ // static options filter locally
425
+ const filter = query.toLowerCase();
426
+ this.refOptions.set((this.field().options ?? []).filter((o) => o.label.toLowerCase().includes(filter)));
427
+ this.refOpen.set(true);
428
+ this.changeDetector.markForCheck();
429
+ this.notify()();
430
+ return;
431
+ }
432
+ const provider = this.providers()[source.name];
433
+ if (!provider)
434
+ return;
435
+ Promise.resolve(provider({
436
+ field: this.field(),
437
+ path: this.path(),
438
+ data: this.engine().getData(),
439
+ args: source.args,
440
+ query,
441
+ }))
442
+ .then((options) => {
443
+ this.refOptions.set(options);
444
+ this.refOpen.set(true);
445
+ this.changeDetector.markForCheck();
446
+ this.notify()();
447
+ })
448
+ .catch(() => { });
449
+ }
450
+ refPick(option) {
451
+ this.setValue(option.value);
452
+ this.refLabel.set(option.label);
453
+ this.refOpen.set(false);
454
+ this.touch();
455
+ }
456
+ refBlur() {
457
+ // let a click on an option land before closing
458
+ setTimeout(() => {
459
+ this.refOpen.set(false);
460
+ this.changeDetector.markForCheck();
461
+ this.notify()();
462
+ }, 150);
463
+ this.touch();
304
464
  }
305
465
  addRow() {
306
466
  this.engine().addRow(this.path());
@@ -309,18 +469,20 @@ class FormNodeComponent {
309
469
  this.engine().removeRow(this.path(), index);
310
470
  }
311
471
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
312
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormNodeComponent, isStandalone: true, selector: "mform-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, tick: { classPropertyName: "tick", publicName: "tick", isSignal: true, isRequired: true, transformFunction: null }, scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: false, transformFunction: null }, bare: { classPropertyName: "bare", publicName: "bare", isSignal: true, isRequired: false, transformFunction: null }, providers: { classPropertyName: "providers", publicName: "providers", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "mform-node" }, ngImport: i0, template: `
472
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormNodeComponent, isStandalone: true, selector: "mform-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, engine: { classPropertyName: "engine", publicName: "engine", isSignal: true, isRequired: true, transformFunction: null }, tick: { classPropertyName: "tick", publicName: "tick", isSignal: true, isRequired: true, transformFunction: null }, scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: false, transformFunction: null }, bare: { classPropertyName: "bare", publicName: "bare", isSignal: true, isRequired: false, transformFunction: null }, providers: { classPropertyName: "providers", publicName: "providers", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, notify: { classPropertyName: "notify", publicName: "notify", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "mform-node" }, ngImport: i0, template: `
313
473
  @let n = node();
314
474
  @if (visible()) {
315
475
  @switch (n.kind) {
316
476
  @case ('static') {
317
477
  @if (n.type === 'button') {
318
- <button
319
- class="mform-btn"
320
- [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
321
- >
322
- {{ n.label || 'Submit' }}
323
- </button>
478
+ @if (interactive()) {
479
+ <button
480
+ class="mform-btn"
481
+ [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
482
+ >
483
+ {{ n.label || 'Submit' }}
484
+ </button>
485
+ }
324
486
  } @else if (n.type === 'content') {
325
487
  <div class="mform-content" [innerHTML]="content()"></div>
326
488
  } @else {
@@ -336,7 +498,7 @@ class FormNodeComponent {
336
498
  @for (column of columns(); track $index) {
337
499
  <div class="mform-column">
338
500
  @for (child of column; track child.key) {
339
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
501
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
340
502
  }
341
503
  </div>
342
504
  }
@@ -361,7 +523,7 @@ class FormNodeComponent {
361
523
  @for (tab of container().children; track tab.key; let i = $index) {
362
524
  @if (activeTab() === i) {
363
525
  <div class="mform-tabpanel" role="tabpanel">
364
- <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" [bare]="true" />
526
+ <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" [bare]="true" />
365
527
  </div>
366
528
  }
367
529
  }
@@ -377,20 +539,24 @@ class FormNodeComponent {
377
539
  <div class="mform-grid-row" role="group" [attr.aria-label]="(container().label || container().key) + ' ' + (i + 1)">
378
540
  <div class="mform-grid-fields">
379
541
  @for (child of container().children; track child.key) {
380
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="rowScope(i)" />
542
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="rowScope(i)" />
381
543
  }
382
544
  </div>
383
- <button
384
- type="button"
385
- class="mform-row-remove"
386
- (click)="removeRow(i)"
387
- [attr.aria-label]="'Remove row ' + (i + 1)"
388
- >
389
-
390
- </button>
545
+ @if (interactive()) {
546
+ <button
547
+ type="button"
548
+ class="mform-row-remove"
549
+ (click)="removeRow(i)"
550
+ [attr.aria-label]="'Remove row ' + (i + 1)"
551
+ >
552
+
553
+ </button>
554
+ }
391
555
  </div>
392
556
  }
393
- <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
557
+ @if (interactive()) {
558
+ <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
559
+ }
394
560
  </div>
395
561
  </section>
396
562
  }
@@ -400,14 +566,14 @@ class FormNodeComponent {
400
566
  <header class="mform-panel-head">{{ container().label }}</header>
401
567
  <div class="mform-panel-body">
402
568
  @for (child of container().children; track child.key) {
403
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
569
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
404
570
  }
405
571
  </div>
406
572
  </section>
407
573
  } @else {
408
574
  <div class="mform-group">
409
575
  @for (child of container().children; track child.key) {
410
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
576
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
411
577
  }
412
578
  </div>
413
579
  }
@@ -421,7 +587,7 @@ class FormNodeComponent {
421
587
  [ngComponentOutletInputs]="customInputs()"
422
588
  />
423
589
  } @else if (field().type !== 'hidden') {
424
- <div class="mform-field" [class.has-error]="errors().length > 0">
590
+ <div class="mform-field" [class.has-error]="errors().length > 0" [class.is-pending]="pending()">
425
591
  @if (field().type !== 'checkbox') {
426
592
  <label class="mform-label" [attr.for]="controlId()">
427
593
  {{ field().label || field().key }}
@@ -437,7 +603,7 @@ class FormNodeComponent {
437
603
  [id]="controlId()"
438
604
  [value]="stringValue()"
439
605
  [placeholder]="field().placeholder || ''"
440
- [disabled]="field().disabled === true"
606
+ [disabled]="controlDisabled()"
441
607
  [attr.rows]="rowsProp()"
442
608
  [attr.aria-required]="required() || null"
443
609
  [attr.aria-invalid]="errors().length > 0 || null"
@@ -450,7 +616,7 @@ class FormNodeComponent {
450
616
  <select
451
617
  class="mform-control"
452
618
  [id]="controlId()"
453
- [disabled]="field().disabled === true"
619
+ [disabled]="controlDisabled()"
454
620
  [attr.aria-required]="required() || null"
455
621
  [attr.aria-invalid]="errors().length > 0 || null"
456
622
  [attr.aria-describedby]="describedBy()"
@@ -476,7 +642,7 @@ class FormNodeComponent {
476
642
  type="radio"
477
643
  [name]="controlId()"
478
644
  [checked]="stringValue() === '' + option.value"
479
- [disabled]="field().disabled === true"
645
+ [disabled]="controlDisabled()"
480
646
  (change)="setValue(option.value)"
481
647
  (blur)="touch()"
482
648
  />
@@ -491,7 +657,7 @@ class FormNodeComponent {
491
657
  type="checkbox"
492
658
  [id]="controlId()"
493
659
  [checked]="value() === true"
494
- [disabled]="field().disabled === true"
660
+ [disabled]="controlDisabled()"
495
661
  [attr.aria-invalid]="errors().length > 0 || null"
496
662
  [attr.aria-describedby]="describedBy()"
497
663
  (change)="setChecked($event)"
@@ -512,7 +678,7 @@ class FormNodeComponent {
512
678
  <input
513
679
  type="checkbox"
514
680
  [checked]="mapValue()['' + option.value] === true"
515
- [disabled]="field().disabled === true"
681
+ [disabled]="controlDisabled()"
516
682
  (change)="toggleMap('' + option.value, $event)"
517
683
  (blur)="touch()"
518
684
  />
@@ -521,17 +687,75 @@ class FormNodeComponent {
521
687
  }
522
688
  </div>
523
689
  }
690
+ @case ('tags') {
691
+ <div class="mform-tags">
692
+ @for (tag of tagValues(); track $index; let i = $index) {
693
+ <span class="mform-tag">
694
+ {{ tag }}
695
+ <button
696
+ type="button"
697
+ (click)="removeTag(i)"
698
+ [attr.aria-label]="'Remove ' + tag"
699
+ >✕</button>
700
+ </span>
701
+ }
702
+ <input
703
+ class="mform-control mform-tag-input"
704
+ type="text"
705
+ [id]="controlId()"
706
+ [placeholder]="field().placeholder || ''"
707
+ [disabled]="controlDisabled()"
708
+ [attr.aria-describedby]="describedBy()"
709
+ (keydown.enter)="addTag($event)"
710
+ (blur)="touch()"
711
+ />
712
+ </div>
713
+ }
524
714
  @case ('file') {
525
715
  <input
526
716
  class="mform-control"
527
717
  type="file"
528
718
  multiple
529
719
  [id]="controlId()"
530
- [disabled]="field().disabled === true"
720
+ [disabled]="controlDisabled() || uploading()"
721
+ [attr.aria-busy]="uploading() || null"
531
722
  [attr.aria-describedby]="describedBy()"
532
723
  (change)="setFiles($event)"
533
724
  (blur)="touch()"
534
725
  />
726
+ @if (uploading()) {
727
+ <div class="mform-desc" role="status">Uploading…</div>
728
+ }
729
+ @for (name of fileNames(); track $index) {
730
+ <div class="mform-file-name">📎 {{ name }}</div>
731
+ }
732
+ }
733
+ @case ('reference') {
734
+ <div class="mform-reference">
735
+ <input
736
+ class="mform-control"
737
+ type="text"
738
+ role="combobox"
739
+ [id]="controlId()"
740
+ [value]="refDisplay()"
741
+ [placeholder]="field().placeholder || ''"
742
+ [disabled]="controlDisabled()"
743
+ [attr.aria-expanded]="refOpen()"
744
+ [attr.aria-invalid]="errors().length > 0 || null"
745
+ [attr.aria-describedby]="describedBy()"
746
+ (input)="refSearch($event)"
747
+ (blur)="refBlur()"
748
+ />
749
+ @if (refOpen() && refOptions().length > 0) {
750
+ <ul class="mform-ref-list" role="listbox">
751
+ @for (option of refOptions(); track option.value) {
752
+ <li role="option">
753
+ <button type="button" (click)="refPick(option)">{{ option.label }}</button>
754
+ </li>
755
+ }
756
+ </ul>
757
+ }
758
+ </div>
535
759
  }
536
760
  @case ('unsupported') {
537
761
  <input
@@ -543,19 +767,29 @@ class FormNodeComponent {
543
767
  />
544
768
  }
545
769
  @default {
546
- <input
547
- class="mform-control"
548
- [type]="inputType()"
549
- [id]="controlId()"
550
- [value]="stringValue()"
551
- [placeholder]="field().placeholder || ''"
552
- [disabled]="field().disabled === true"
553
- [attr.aria-required]="required() || null"
554
- [attr.aria-invalid]="errors().length > 0 || null"
555
- [attr.aria-describedby]="describedBy()"
556
- (input)="setString($event)"
557
- (blur)="touch()"
558
- />
770
+ <div class="mform-affix" [class.has-affix]="prefix() || suffix()">
771
+ @if (prefix(); as p) {
772
+ <span class="mform-prefix" aria-hidden="true">{{ p }}</span>
773
+ }
774
+ <input
775
+ class="mform-control"
776
+ [type]="inputType()"
777
+ [id]="controlId()"
778
+ [value]="stringValue()"
779
+ [placeholder]="field().placeholder || ''"
780
+ [disabled]="controlDisabled()"
781
+ [attr.step]="field().type === 'currency' ? '0.01' : null"
782
+ [attr.inputmode]="field().type === 'currency' ? 'decimal' : null"
783
+ [attr.aria-required]="required() || null"
784
+ [attr.aria-invalid]="errors().length > 0 || null"
785
+ [attr.aria-describedby]="describedBy()"
786
+ (input)="setString($event)"
787
+ (blur)="touch()"
788
+ />
789
+ @if (suffix(); as s) {
790
+ <span class="mform-suffix" aria-hidden="true">{{ s }}</span>
791
+ }
792
+ </div>
559
793
  }
560
794
  }
561
795
  @if (field().description) {
@@ -569,7 +803,7 @@ class FormNodeComponent {
569
803
  }
570
804
  }
571
805
  }
572
- `, isInline: true, dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
806
+ `, isInline: true, dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
573
807
  }
574
808
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormNodeComponent, decorators: [{
575
809
  type: Component,
@@ -585,12 +819,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
585
819
  @switch (n.kind) {
586
820
  @case ('static') {
587
821
  @if (n.type === 'button') {
588
- <button
589
- class="mform-btn"
590
- [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
591
- >
592
- {{ n.label || 'Submit' }}
593
- </button>
822
+ @if (interactive()) {
823
+ <button
824
+ class="mform-btn"
825
+ [type]="buttonAction() === 'submit' ? 'submit' : 'button'"
826
+ >
827
+ {{ n.label || 'Submit' }}
828
+ </button>
829
+ }
594
830
  } @else if (n.type === 'content') {
595
831
  <div class="mform-content" [innerHTML]="content()"></div>
596
832
  } @else {
@@ -606,7 +842,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
606
842
  @for (column of columns(); track $index) {
607
843
  <div class="mform-column">
608
844
  @for (child of column; track child.key) {
609
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
845
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
610
846
  }
611
847
  </div>
612
848
  }
@@ -631,7 +867,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
631
867
  @for (tab of container().children; track tab.key; let i = $index) {
632
868
  @if (activeTab() === i) {
633
869
  <div class="mform-tabpanel" role="tabpanel">
634
- <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" [bare]="true" />
870
+ <mform-node [node]="tab" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" [bare]="true" />
635
871
  </div>
636
872
  }
637
873
  }
@@ -647,20 +883,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
647
883
  <div class="mform-grid-row" role="group" [attr.aria-label]="(container().label || container().key) + ' ' + (i + 1)">
648
884
  <div class="mform-grid-fields">
649
885
  @for (child of container().children; track child.key) {
650
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="rowScope(i)" />
886
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="rowScope(i)" />
651
887
  }
652
888
  </div>
653
- <button
654
- type="button"
655
- class="mform-row-remove"
656
- (click)="removeRow(i)"
657
- [attr.aria-label]="'Remove row ' + (i + 1)"
658
- >
659
-
660
- </button>
889
+ @if (interactive()) {
890
+ <button
891
+ type="button"
892
+ class="mform-row-remove"
893
+ (click)="removeRow(i)"
894
+ [attr.aria-label]="'Remove row ' + (i + 1)"
895
+ >
896
+
897
+ </button>
898
+ }
661
899
  </div>
662
900
  }
663
- <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
901
+ @if (interactive()) {
902
+ <button type="button" class="mform-row-add" (click)="addRow()">+ Add</button>
903
+ }
664
904
  </div>
665
905
  </section>
666
906
  }
@@ -670,14 +910,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
670
910
  <header class="mform-panel-head">{{ container().label }}</header>
671
911
  <div class="mform-panel-body">
672
912
  @for (child of container().children; track child.key) {
673
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
913
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
674
914
  }
675
915
  </div>
676
916
  </section>
677
917
  } @else {
678
918
  <div class="mform-group">
679
919
  @for (child of container().children; track child.key) {
680
- <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [scope]="childScope()" />
920
+ <mform-node [node]="child" [engine]="engine()" [tick]="tick()" [providers]="providers()" [mode]="mode()" [notify]="notify()" [scope]="childScope()" />
681
921
  }
682
922
  </div>
683
923
  }
@@ -691,7 +931,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
691
931
  [ngComponentOutletInputs]="customInputs()"
692
932
  />
693
933
  } @else if (field().type !== 'hidden') {
694
- <div class="mform-field" [class.has-error]="errors().length > 0">
934
+ <div class="mform-field" [class.has-error]="errors().length > 0" [class.is-pending]="pending()">
695
935
  @if (field().type !== 'checkbox') {
696
936
  <label class="mform-label" [attr.for]="controlId()">
697
937
  {{ field().label || field().key }}
@@ -707,7 +947,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
707
947
  [id]="controlId()"
708
948
  [value]="stringValue()"
709
949
  [placeholder]="field().placeholder || ''"
710
- [disabled]="field().disabled === true"
950
+ [disabled]="controlDisabled()"
711
951
  [attr.rows]="rowsProp()"
712
952
  [attr.aria-required]="required() || null"
713
953
  [attr.aria-invalid]="errors().length > 0 || null"
@@ -720,7 +960,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
720
960
  <select
721
961
  class="mform-control"
722
962
  [id]="controlId()"
723
- [disabled]="field().disabled === true"
963
+ [disabled]="controlDisabled()"
724
964
  [attr.aria-required]="required() || null"
725
965
  [attr.aria-invalid]="errors().length > 0 || null"
726
966
  [attr.aria-describedby]="describedBy()"
@@ -746,7 +986,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
746
986
  type="radio"
747
987
  [name]="controlId()"
748
988
  [checked]="stringValue() === '' + option.value"
749
- [disabled]="field().disabled === true"
989
+ [disabled]="controlDisabled()"
750
990
  (change)="setValue(option.value)"
751
991
  (blur)="touch()"
752
992
  />
@@ -761,7 +1001,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
761
1001
  type="checkbox"
762
1002
  [id]="controlId()"
763
1003
  [checked]="value() === true"
764
- [disabled]="field().disabled === true"
1004
+ [disabled]="controlDisabled()"
765
1005
  [attr.aria-invalid]="errors().length > 0 || null"
766
1006
  [attr.aria-describedby]="describedBy()"
767
1007
  (change)="setChecked($event)"
@@ -782,7 +1022,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
782
1022
  <input
783
1023
  type="checkbox"
784
1024
  [checked]="mapValue()['' + option.value] === true"
785
- [disabled]="field().disabled === true"
1025
+ [disabled]="controlDisabled()"
786
1026
  (change)="toggleMap('' + option.value, $event)"
787
1027
  (blur)="touch()"
788
1028
  />
@@ -791,17 +1031,75 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
791
1031
  }
792
1032
  </div>
793
1033
  }
1034
+ @case ('tags') {
1035
+ <div class="mform-tags">
1036
+ @for (tag of tagValues(); track $index; let i = $index) {
1037
+ <span class="mform-tag">
1038
+ {{ tag }}
1039
+ <button
1040
+ type="button"
1041
+ (click)="removeTag(i)"
1042
+ [attr.aria-label]="'Remove ' + tag"
1043
+ >✕</button>
1044
+ </span>
1045
+ }
1046
+ <input
1047
+ class="mform-control mform-tag-input"
1048
+ type="text"
1049
+ [id]="controlId()"
1050
+ [placeholder]="field().placeholder || ''"
1051
+ [disabled]="controlDisabled()"
1052
+ [attr.aria-describedby]="describedBy()"
1053
+ (keydown.enter)="addTag($event)"
1054
+ (blur)="touch()"
1055
+ />
1056
+ </div>
1057
+ }
794
1058
  @case ('file') {
795
1059
  <input
796
1060
  class="mform-control"
797
1061
  type="file"
798
1062
  multiple
799
1063
  [id]="controlId()"
800
- [disabled]="field().disabled === true"
1064
+ [disabled]="controlDisabled() || uploading()"
1065
+ [attr.aria-busy]="uploading() || null"
801
1066
  [attr.aria-describedby]="describedBy()"
802
1067
  (change)="setFiles($event)"
803
1068
  (blur)="touch()"
804
1069
  />
1070
+ @if (uploading()) {
1071
+ <div class="mform-desc" role="status">Uploading…</div>
1072
+ }
1073
+ @for (name of fileNames(); track $index) {
1074
+ <div class="mform-file-name">📎 {{ name }}</div>
1075
+ }
1076
+ }
1077
+ @case ('reference') {
1078
+ <div class="mform-reference">
1079
+ <input
1080
+ class="mform-control"
1081
+ type="text"
1082
+ role="combobox"
1083
+ [id]="controlId()"
1084
+ [value]="refDisplay()"
1085
+ [placeholder]="field().placeholder || ''"
1086
+ [disabled]="controlDisabled()"
1087
+ [attr.aria-expanded]="refOpen()"
1088
+ [attr.aria-invalid]="errors().length > 0 || null"
1089
+ [attr.aria-describedby]="describedBy()"
1090
+ (input)="refSearch($event)"
1091
+ (blur)="refBlur()"
1092
+ />
1093
+ @if (refOpen() && refOptions().length > 0) {
1094
+ <ul class="mform-ref-list" role="listbox">
1095
+ @for (option of refOptions(); track option.value) {
1096
+ <li role="option">
1097
+ <button type="button" (click)="refPick(option)">{{ option.label }}</button>
1098
+ </li>
1099
+ }
1100
+ </ul>
1101
+ }
1102
+ </div>
805
1103
  }
806
1104
  @case ('unsupported') {
807
1105
  <input
@@ -813,19 +1111,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
813
1111
  />
814
1112
  }
815
1113
  @default {
816
- <input
817
- class="mform-control"
818
- [type]="inputType()"
819
- [id]="controlId()"
820
- [value]="stringValue()"
821
- [placeholder]="field().placeholder || ''"
822
- [disabled]="field().disabled === true"
823
- [attr.aria-required]="required() || null"
824
- [attr.aria-invalid]="errors().length > 0 || null"
825
- [attr.aria-describedby]="describedBy()"
826
- (input)="setString($event)"
827
- (blur)="touch()"
828
- />
1114
+ <div class="mform-affix" [class.has-affix]="prefix() || suffix()">
1115
+ @if (prefix(); as p) {
1116
+ <span class="mform-prefix" aria-hidden="true">{{ p }}</span>
1117
+ }
1118
+ <input
1119
+ class="mform-control"
1120
+ [type]="inputType()"
1121
+ [id]="controlId()"
1122
+ [value]="stringValue()"
1123
+ [placeholder]="field().placeholder || ''"
1124
+ [disabled]="controlDisabled()"
1125
+ [attr.step]="field().type === 'currency' ? '0.01' : null"
1126
+ [attr.inputmode]="field().type === 'currency' ? 'decimal' : null"
1127
+ [attr.aria-required]="required() || null"
1128
+ [attr.aria-invalid]="errors().length > 0 || null"
1129
+ [attr.aria-describedby]="describedBy()"
1130
+ (input)="setString($event)"
1131
+ (blur)="touch()"
1132
+ />
1133
+ @if (suffix(); as s) {
1134
+ <span class="mform-suffix" aria-hidden="true">{{ s }}</span>
1135
+ }
1136
+ </div>
829
1137
  }
830
1138
  }
831
1139
  @if (field().description) {
@@ -841,7 +1149,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
841
1149
  }
842
1150
  `,
843
1151
  }]
844
- }], ctorParameters: () => [], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], engine: [{ type: i0.Input, args: [{ isSignal: true, alias: "engine", required: true }] }], tick: [{ type: i0.Input, args: [{ isSignal: true, alias: "tick", required: true }] }], scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: false }] }], bare: [{ type: i0.Input, args: [{ isSignal: true, alias: "bare", required: false }] }], providers: [{ type: i0.Input, args: [{ isSignal: true, alias: "providers", required: false }] }] } });
1152
+ }], ctorParameters: () => [], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], engine: [{ type: i0.Input, args: [{ isSignal: true, alias: "engine", required: true }] }], tick: [{ type: i0.Input, args: [{ isSignal: true, alias: "tick", required: true }] }], scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: false }] }], bare: [{ type: i0.Input, args: [{ isSignal: true, alias: "bare", required: false }] }], providers: [{ type: i0.Input, args: [{ isSignal: true, alias: "providers", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], notify: [{ type: i0.Input, args: [{ isSignal: true, alias: "notify", required: false }] }] } });
845
1153
 
846
1154
  /**
847
1155
  * `<mform-renderer>` — renders a form at runtime from a `FormSchema` (or any
@@ -864,6 +1172,20 @@ class FormRendererComponent {
864
1172
  optionsProviders = input({}, ...(ngDevMode ? [{ debugName: "optionsProviders" }] : /* istanbul ignore next */ []));
865
1173
  /** Host-supplied message resolver (localization of validation messages). */
866
1174
  messages = input(null, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
1175
+ /** Named custom validators (sync or async — Promises get pending states). */
1176
+ validators = input(null, ...(ngDevMode ? [{ debugName: "validators" }] : /* istanbul ignore next */ []));
1177
+ /** `edit` (default), `readonly` (no actions) or `disabled`. */
1178
+ mode = input('edit', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
1179
+ /**
1180
+ * Optional host submission: called after a VALID submit with the result;
1181
+ * rejections surface as the submission error state. Persistence transport
1182
+ * stays entirely on the host side.
1183
+ */
1184
+ submitHandler = input(null, ...(ngDevMode ? [{ debugName: "submitHandler" }] : /* istanbul ignore next */ []));
1185
+ /** True while async validation or the submit handler is running. */
1186
+ submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []));
1187
+ /** Message when the host submission rejected. */
1188
+ submitError = signal(null, ...(ngDevMode ? [{ debugName: "submitError" }] : /* istanbul ignore next */ []));
867
1189
  /** Fixed renderer texts (override to localize). `{{count}}` in `summary`. */
868
1190
  labels = input({
869
1191
  previous: 'Previous',
@@ -886,6 +1208,8 @@ class FormRendererComponent {
886
1208
  valueChanged = output();
887
1209
  /** Bumped on every engine event; nodes read it to refresh. */
888
1210
  tick = signal(0, ...(ngDevMode ? [{ debugName: "tick" }] : /* istanbul ignore next */ []));
1211
+ /** Handed to nodes so async completions re-render (zoneless-safe). */
1212
+ bumpTick = () => this.tick.update((v) => v + 1);
889
1213
  /** Active wizard step index. */
890
1214
  currentStep = signal(0, ...(ngDevMode ? [{ debugName: "currentStep" }] : /* istanbul ignore next */ []));
891
1215
  resolved = computed(() => {
@@ -916,6 +1240,7 @@ class FormRendererComponent {
916
1240
  ? createFormEngine(schema, {
917
1241
  initialData: this.initialData() ?? undefined,
918
1242
  messages: this.messages() ?? undefined,
1243
+ validators: this.validators() ?? undefined,
919
1244
  })
920
1245
  : null;
921
1246
  }, ...(ngDevMode ? [{ debugName: "engine" }] : /* istanbul ignore next */ []));
@@ -966,22 +1291,37 @@ class FormRendererComponent {
966
1291
  this.currentStep.update((index) => Math.min(this.steps().length - 1, index + 1));
967
1292
  }
968
1293
  // -- submission -----------------------------------------------------------
969
- onSubmit(event) {
1294
+ async onSubmit(event) {
970
1295
  event.preventDefault();
971
1296
  const engine = this.engine();
972
- if (!engine)
1297
+ if (!engine || this.submitting() || this.mode() !== 'edit')
973
1298
  return;
974
- const result = engine.submit();
975
- this.errorSummary.set(result.errors);
976
- if (!result.ok) {
977
- if (this.isWizard()) {
978
- const stepIndex = this.stepIndexForPath(result.errors[0]?.path ?? '');
979
- if (stepIndex >= 0)
980
- this.currentStep.set(stepIndex);
1299
+ this.submitting.set(true);
1300
+ this.submitError.set(null);
1301
+ try {
1302
+ const result = await engine.submitAsync();
1303
+ this.errorSummary.set(result.errors);
1304
+ if (!result.ok) {
1305
+ if (this.isWizard()) {
1306
+ const stepIndex = this.stepIndexForPath(result.errors[0]?.path ?? '');
1307
+ if (stepIndex >= 0)
1308
+ this.currentStep.set(stepIndex);
1309
+ }
1310
+ this.focusFirstError(result.errors);
981
1311
  }
982
- this.focusFirstError(result.errors);
1312
+ else if (this.submitHandler()) {
1313
+ try {
1314
+ await this.submitHandler()(result);
1315
+ }
1316
+ catch (error) {
1317
+ this.submitError.set(String(error instanceof Error ? error.message : error));
1318
+ }
1319
+ }
1320
+ this.submitted.emit(result);
1321
+ }
1322
+ finally {
1323
+ this.submitting.set(false);
983
1324
  }
984
- this.submitted.emit(result);
985
1325
  }
986
1326
  /** Finds the wizard step that owns a (possibly row-indexed) data path. */
987
1327
  stepIndexForPath(path) {
@@ -1001,7 +1341,7 @@ class FormRendererComponent {
1001
1341
  queueMicrotask(() => this.documentRef.getElementById(id)?.focus());
1002
1342
  }
1003
1343
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1004
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormRendererComponent, isStandalone: true, selector: "mform-renderer", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, initialData: { classPropertyName: "initialData", publicName: "initialData", isSignal: true, isRequired: false, transformFunction: null }, optionsProviders: { classPropertyName: "optionsProviders", publicName: "optionsProviders", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted", valueChanged: "valueChanged" }, ngImport: i0, template: `
1344
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FormRendererComponent, isStandalone: true, selector: "mform-renderer", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, initialData: { classPropertyName: "initialData", publicName: "initialData", isSignal: true, isRequired: false, transformFunction: null }, optionsProviders: { classPropertyName: "optionsProviders", publicName: "optionsProviders", isSignal: true, isRequired: false, transformFunction: null }, messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, validators: { classPropertyName: "validators", publicName: "validators", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, submitHandler: { classPropertyName: "submitHandler", publicName: "submitHandler", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitted: "submitted", valueChanged: "valueChanged" }, ngImport: i0, template: `
1005
1345
  @if (engine(); as engine) {
1006
1346
  <form class="mform-root" novalidate (submit)="onSubmit($event)">
1007
1347
  @if (errorSummary().length > 0) {
@@ -1034,6 +1374,7 @@ class FormRendererComponent {
1034
1374
  [engine]="engine"
1035
1375
  [tick]="tick()"
1036
1376
  [providers]="optionsProviders()"
1377
+ [mode]="mode()" [notify]="bumpTick"
1037
1378
  [bare]="true"
1038
1379
  />
1039
1380
  }
@@ -1047,18 +1388,23 @@ class FormRendererComponent {
1047
1388
  <button type="button" class="mform-btn" (click)="nextStep()">
1048
1389
  {{ labels().next }}
1049
1390
  </button>
1050
- } @else {
1051
- <button type="submit" class="mform-btn">{{ labels().submit }}</button>
1391
+ } @else if (mode() === 'edit') {
1392
+ <button type="submit" class="mform-btn" [disabled]="submitting()">
1393
+ {{ submitting() ? '…' : labels().submit }}
1394
+ </button>
1052
1395
  }
1053
1396
  </div>
1054
1397
  } @else {
1055
1398
  @for (node of engine.schema.fields; track node.key) {
1056
- <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" />
1399
+ <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" [mode]="mode()" [notify]="bumpTick" />
1057
1400
  }
1058
1401
  }
1402
+ @if (submitError(); as error) {
1403
+ <div class="mform-summary" role="alert">{{ error }}</div>
1404
+ }
1059
1405
  </form>
1060
1406
  }
1061
- `, isInline: true, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"], dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1407
+ `, isInline: true, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}@media(max-width:640px){.mform-columns{grid-template-columns:1fr!important}}.mform-affix{display:flex;align-items:center}.mform-affix.has-affix{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff)}.mform-affix.has-affix .mform-control{border:none;background:none;flex:1}.mform-affix.has-affix:focus-within{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px}.mform-prefix,.mform-suffix{padding:0 10px;color:var(--mform-muted, #6b6580);font-size:13px;white-space:nowrap}.mform-tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mform-tag{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-radius:999px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:6px}.mform-tag button{border:none;background:none;color:inherit;cursor:pointer;font-size:12px;padding:0;line-height:1}.mform-tag-input{flex:1;min-width:140px;width:auto}.mform-field.is-pending .mform-control{background-image:linear-gradient(90deg,transparent 0%,rgb(109 75 208 / 12%) 50%,transparent 100%);background-size:200% 100%;animation:mform-pending 1.2s linear infinite}@keyframes mform-pending{0%{background-position:200% 0}to{background-position:-200% 0}}.mform-file-name{font-size:12px;color:var(--mform-muted, #6b6580);padding:2px 0}.mform-reference{position:relative}.mform-ref-list{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:10;margin:0;padding:4px;list-style:none;background:var(--mform-control-bg, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);box-shadow:0 8px 24px #1f1b2e1f;max-height:220px;overflow-y:auto}.mform-ref-list button{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:7px 10px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.mform-ref-list button:hover{background:var(--mform-bg, #f6f5fa)}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"], dependencies: [{ kind: "component", type: FormNodeComponent, selector: "mform-node", inputs: ["node", "engine", "tick", "scope", "bare", "providers", "mode", "notify"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1062
1408
  }
1063
1409
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FormRendererComponent, decorators: [{
1064
1410
  type: Component,
@@ -1095,6 +1441,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1095
1441
  [engine]="engine"
1096
1442
  [tick]="tick()"
1097
1443
  [providers]="optionsProviders()"
1444
+ [mode]="mode()" [notify]="bumpTick"
1098
1445
  [bare]="true"
1099
1446
  />
1100
1447
  }
@@ -1108,19 +1455,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1108
1455
  <button type="button" class="mform-btn" (click)="nextStep()">
1109
1456
  {{ labels().next }}
1110
1457
  </button>
1111
- } @else {
1112
- <button type="submit" class="mform-btn">{{ labels().submit }}</button>
1458
+ } @else if (mode() === 'edit') {
1459
+ <button type="submit" class="mform-btn" [disabled]="submitting()">
1460
+ {{ submitting() ? '…' : labels().submit }}
1461
+ </button>
1113
1462
  }
1114
1463
  </div>
1115
1464
  } @else {
1116
1465
  @for (node of engine.schema.fields; track node.key) {
1117
- <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" />
1466
+ <mform-node [node]="node" [engine]="engine" [tick]="tick()" [providers]="optionsProviders()" [mode]="mode()" [notify]="bumpTick" />
1118
1467
  }
1119
1468
  }
1469
+ @if (submitError(); as error) {
1470
+ <div class="mform-summary" role="alert">{{ error }}</div>
1471
+ }
1120
1472
  </form>
1121
1473
  }
1122
- `, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"] }]
1123
- }], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], initialData: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialData", required: false }] }], optionsProviders: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionsProviders", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }], valueChanged: [{ type: i0.Output, args: ["valueChanged"] }] } });
1474
+ `, styles: [".mform-root{display:flex;flex-direction:column;gap:var(--mform-gap, 16px);font-family:var(--mform-font, system-ui, \"Segoe UI\", sans-serif);color:var(--mform-text, #1f1b2e)}.mform-node{display:contents}.mform-field{display:flex;flex-direction:column;gap:6px}.mform-label{font-size:var(--mform-label-size, 13px);font-weight:var(--mform-label-weight, 600)}.mform-req{color:var(--mform-danger, #c62842);margin-left:2px}.mform-control{font:inherit;width:100%;padding:var(--mform-control-padding, 9px 12px);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff);color:inherit;box-sizing:border-box}textarea.mform-control{min-height:72px;resize:vertical}.mform-control:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px;border-color:var(--mform-focus, #8e6ff0)}.mform-field.has-error .mform-control{border-color:var(--mform-danger, #c62842)}.mform-desc{font-size:12px;color:var(--mform-muted, #6b6580)}.mform-error{font-size:12px;color:var(--mform-danger, #c62842)}.mform-choices{display:flex;flex-direction:column;gap:6px}.mform-check{display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer}.mform-check input{width:16px;height:16px;accent-color:var(--mform-primary, #6d4bd0)}.mform-panel{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);overflow:hidden}.mform-panel-head{background:var(--mform-panel-head-bg, linear-gradient(90deg, #efeaff, #f8f6ff));color:var(--mform-panel-head-text, inherit);padding:10px 16px;font-weight:600;font-size:14px;border-bottom:1px solid var(--mform-border, #d9d5e6)}.mform-panel-body,.mform-group,.mform-column,.mform-tabpanel{display:flex;flex-direction:column;gap:var(--mform-gap, 16px)}.mform-panel-body{padding:var(--mform-panel-padding, 16px)}.mform-columns{display:grid;gap:var(--mform-gap, 16px)}@media(max-width:640px){.mform-columns{grid-template-columns:1fr!important}}.mform-affix{display:flex;align-items:center}.mform-affix.has-affix{border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);background:var(--mform-control-bg, #fff)}.mform-affix.has-affix .mform-control{border:none;background:none;flex:1}.mform-affix.has-affix:focus-within{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:1px}.mform-prefix,.mform-suffix{padding:0 10px;color:var(--mform-muted, #6b6580);font-size:13px;white-space:nowrap}.mform-tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mform-tag{background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border-radius:999px;padding:3px 10px;font-size:12px;display:inline-flex;align-items:center;gap:6px}.mform-tag button{border:none;background:none;color:inherit;cursor:pointer;font-size:12px;padding:0;line-height:1}.mform-tag-input{flex:1;min-width:140px;width:auto}.mform-field.is-pending .mform-control{background-image:linear-gradient(90deg,transparent 0%,rgb(109 75 208 / 12%) 50%,transparent 100%);background-size:200% 100%;animation:mform-pending 1.2s linear infinite}@keyframes mform-pending{0%{background-position:200% 0}to{background-position:-200% 0}}.mform-file-name{font-size:12px;color:var(--mform-muted, #6b6580);padding:2px 0}.mform-reference{position:relative}.mform-ref-list{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:10;margin:0;padding:4px;list-style:none;background:var(--mform-control-bg, #fff);border:1px solid var(--mform-border, #d9d5e6);border-radius:var(--mform-control-radius, 8px);box-shadow:0 8px 24px #1f1b2e1f;max-height:220px;overflow-y:auto}.mform-ref-list button{display:block;width:100%;text-align:left;font:inherit;font-size:13px;padding:7px 10px;border:none;border-radius:6px;background:none;color:inherit;cursor:pointer}.mform-ref-list button:hover{background:var(--mform-bg, #f6f5fa)}.mform-tabbar{display:flex;gap:4px;border-bottom:2px solid var(--mform-border, #d9d5e6);margin-bottom:14px}.mform-tab{padding:8px 18px;font:inherit;font-size:14px;border:none;background:none;color:var(--mform-muted, #6b6580);cursor:pointer}.mform-tab.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600;border-bottom:2px solid var(--mform-primary, #6d4bd0);margin-bottom:-2px}.mform-grid-row{display:flex;align-items:flex-start;gap:8px;padding:10px 0;border-bottom:1px dashed var(--mform-border, #d9d5e6)}.mform-grid-fields{flex:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--mform-gap, 16px)}.mform-row-remove{border:none;background:none;color:var(--mform-muted, #6b6580);font:inherit;cursor:pointer;padding:6px 8px}.mform-row-remove:hover{color:var(--mform-danger, #c62842)}.mform-row-add{align-self:flex-start;background:none;border:1px dashed var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0);padding:7px 14px;border-radius:var(--mform-control-radius, 8px);font:inherit;font-size:13px;cursor:pointer}.mform-btn{align-self:flex-start;background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff);border:none;border-radius:var(--mform-control-radius, 8px);padding:11px 26px;font:inherit;font-weight:600;cursor:pointer}.mform-btn:hover{filter:brightness(1.08)}.mform-btn:focus-visible{outline:2px solid var(--mform-focus, #8e6ff0);outline-offset:2px}.mform-content{font-size:14px}.mform-unsupported{font-size:13px;color:var(--mform-muted, #6b6580);border:1px dashed var(--mform-border, #d9d5e6);border-radius:var(--mform-radius, 10px);padding:10px 14px}.mform-steps{display:flex;gap:18px;list-style:none;margin:0 0 4px;padding:0 0 12px;border-bottom:1px solid var(--mform-border, #d9d5e6);flex-wrap:wrap}.mform-step{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--mform-muted, #6b6580)}.mform-step-index{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--mform-border, #d9d5e6);font-size:11px;font-weight:600}.mform-step.is-active{color:var(--mform-primary, #6d4bd0);font-weight:600}.mform-step.is-active .mform-step-index{border-color:var(--mform-primary, #6d4bd0);background:var(--mform-primary, #6d4bd0);color:var(--mform-primary-contrast, #fff)}.mform-step.is-done .mform-step-index{border-color:var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-wizard-nav{display:flex;gap:10px;padding-top:4px}.mform-btn-secondary{background:none;border:1px solid var(--mform-primary, #6d4bd0);color:var(--mform-primary, #6d4bd0)}.mform-summary{border:1px solid var(--mform-danger, #c62842);background:var(--mform-danger-bg, #fdf0f2);color:var(--mform-danger, #c62842);border-radius:var(--mform-radius, 10px);padding:12px 16px;font-size:13px}.mform-summary ul{margin:6px 0 0;padding-left:18px}\n"] }]
1475
+ }], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], initialData: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialData", required: false }] }], optionsProviders: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionsProviders", required: false }] }], messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], validators: [{ type: i0.Input, args: [{ isSignal: true, alias: "validators", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], submitHandler: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitHandler", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], submitted: [{ type: i0.Output, args: ["submitted"] }], valueChanged: [{ type: i0.Output, args: ["valueChanged"] }] } });
1124
1476
 
1125
1477
  /*
1126
1478
  * Runtime entry point of @mosaicoo/form-angular.
@@ -1135,5 +1487,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
1135
1487
  * Generated bundle index. Do not edit.
1136
1488
  */
1137
1489
 
1138
- export { FormNodeComponent, FormRendererComponent, MFORM_FIELD_COMPONENTS, MFORM_REMOTE_FETCHER, provideMosaicooForm };
1490
+ export { FormNodeComponent, FormRendererComponent, MFORM_FIELD_COMPONENTS, MFORM_REMOTE_FETCHER, MFORM_UPLOAD, provideMosaicooForm };
1139
1491
  //# sourceMappingURL=mosaicoo-form-angular.mjs.map