@ng-simplicity/forms-core 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -1,11 +1,21 @@
1
1
  # @ng-simplicity/forms-core
2
2
 
3
+ [![Documentation](https://img.shields.io/badge/Docs-ng--simplicity--forms.bryangwalsh.com-4f46e5.svg)](https://ng-simplicity-forms.bryangwalsh.com)
4
+
5
+ 📖 **Documentation, tutorials and live demos:** [ng-simplicity-forms.bryangwalsh.com](https://ng-simplicity-forms.bryangwalsh.com) · [Core concepts](https://ng-simplicity-forms.bryangwalsh.com/guides/core-concepts) · [API reference](https://ng-simplicity-forms.bryangwalsh.com/api)
6
+
3
7
  The core engine package for **NG-Simplicity Forms**. This package manages form states, dynamic registry injection, component lifecycle bindings, and structural layout directives (Rows, Columns, Sections, Groups, Arrays). It enables you to construct highly dynamic, complex forms where control properties (such as visibility, enabled/disabled state, select options, and validation rules) reactively adapt in real-time based on the current values inside the form. The engine is highly customizable, allowing you to develop and add your own custom form controls to the registry to be used in the form rendering.
4
8
 
5
9
  This core package is extended by styling integration packages that provide prebuilt themed form components:
6
10
  - **[@ng-simplicity/forms-bootstrap](../forms-bootstrap/README.md)**: Dynamic forms pre-styled with Bootstrap form fields and layouts.
7
11
  - **[@ng-simplicity/forms-material](../forms-material/README.md)**: Dynamic forms pre-styled with Angular Material inputs and controls.
8
12
 
13
+ ## Compatibility
14
+
15
+ | Package | Package version | Angular support |
16
+ |---|---:|---|
17
+ | `@ng-simplicity/forms-core` | `1.1.2` | `17.x`–`22.x` |
18
+
9
19
  ---
10
20
 
11
21
  ## Installation
@@ -1,14 +1,16 @@
1
1
  import { Subject, takeUntil, BehaviorSubject, map, filter, merge, of } from 'rxjs';
2
2
  import * as i0 from '@angular/core';
3
- import { Directive, InjectionToken, inject, Injectable, ChangeDetectorRef, Injector, ViewContainerRef, Input, ChangeDetectionStrategy, Component, ElementRef, Sanitizer, SecurityContext, NgModule } from '@angular/core';
3
+ import { Directive, InjectionToken, inject, Injectable, ChangeDetectorRef, Injector, ViewContainerRef, Input, computed, ChangeDetectionStrategy, Component, ElementRef, Sanitizer, SecurityContext, NgModule } from '@angular/core';
4
4
  import { v4 } from 'uuid';
5
5
  import { toSignal } from '@angular/core/rxjs-interop';
6
6
  import { UntypedFormGroup, FormControl, UntypedFormArray, FormsModule, ReactiveFormsModule } from '@angular/forms';
7
7
  import { CommonModule } from '@angular/common';
8
8
 
9
+ /** Base class that ties RxJS subscriptions to the component's lifetime. */
9
10
  class NgsSubscriber {
10
11
  destroy$ = new Subject();
11
12
  subscriptions = [];
13
+ /** Subscribes to `subscribeTo` until the component is destroyed. */
12
14
  subscribe(subscribeTo, nextObserver) {
13
15
  subscribeTo.pipe(takeUntil(this.destroy$)).subscribe(nextObserver);
14
16
  }
@@ -24,9 +26,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
24
26
  type: Directive
25
27
  }] });
26
28
 
29
+ /** Injection token for the `NgsFormsFormItem` a component is rendering. */
27
30
  const NGS_FORMS_ITEM_DATA = new InjectionToken('ngs-forms-item-data');
31
+ /**
32
+ * @internal
33
+ */
28
34
  const NGS_FORMS_CONTROL_ADD_REMOVE_FN = new InjectionToken('ngs-form-control-remove-fn');
35
+ /** Injection token for the row index when a component is rendered inside a form array list. */
29
36
  const NGS_FORMS_ITEM_INDEX = new InjectionToken('ngs-forms-item-index');
37
+ /**
38
+ * @internal
39
+ */
30
40
  const NgsFormsFormControlAddRemoveFunctions = {
31
41
  formGroup: (formGroup) => ({
32
42
  add: (control, controlName) => formGroup.addControl(controlName, control),
@@ -55,10 +65,20 @@ function ngsDefaults(target, ...sources) {
55
65
  return result;
56
66
  }
57
67
 
68
+ /**
69
+ * Root base class for every form item component. Reads the item's configuration from `NGS_FORMS_ITEM_DATA`.
70
+ *
71
+ * @remarks Extend it for components that do not own a form control, such as static content or buttons. Declare a
72
+ * `static key` and a `static create(config)` factory so the component can be registered and used in configs.
73
+ */
58
74
  class NgsFormsBaseClassFormComponent extends NgsSubscriber {
75
+ /** Registry key the component is registered under. */
59
76
  static key;
77
+ /** Unique id for this component instance, handy for `id` and `for` attributes. */
60
78
  id = v4();
79
+ /** The item's configuration. */
61
80
  config;
81
+ /** The full form item: `uuid`, `type` and `config`. */
62
82
  itemData;
63
83
  constructor() {
64
84
  super();
@@ -73,11 +93,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
73
93
  args: [{}]
74
94
  }], ctorParameters: () => [] });
75
95
 
96
+ /**
97
+ * Maps registry keys (the `type` of an `NgsFormsFormItem`) to the Angular components that render them.
98
+ *
99
+ * @remarks The service is provided in the `platform` injector, so there is one registry per page. Registering a
100
+ * key again replaces the previous component, which is how a theme package or your own component can override a
101
+ * built-in one. Register components before the form renders, typically in the constructor of the component that
102
+ * hosts `<ngs-form>`.
103
+ * @example
104
+ * ```ts
105
+ * constructor(registry: NgsFormsComponentRegistryService) {
106
+ * NgsFormsBootstrapModule.registerAllBootStrapComponents(registry);
107
+ * registry.register(MyRatingComponent.key, MyRatingComponent);
108
+ * }
109
+ * ```
110
+ */
76
111
  class NgsFormsComponentRegistryService {
77
112
  itemComponentRegistry = {};
113
+ /** Returns the component registered for `key`, or `undefined` when nothing is registered. */
78
114
  getComponentTypeForKey(key) {
79
115
  return this.itemComponentRegistry[key];
80
116
  }
117
+ /** Registers `component` under `key`, replacing any component already registered for it. */
81
118
  register(key, component) {
82
119
  this.itemComponentRegistry[key] = component;
83
120
  }
@@ -89,24 +126,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
89
126
  args: [{ providedIn: 'platform' }]
90
127
  }] });
91
128
 
129
+ /**
130
+ * The public API of a form. Provide it on the component that hosts `<ngs-form>` so each form gets its own instance.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * @Component({
135
+ * selector: 'app-profile',
136
+ * imports: [NgsFormComponent],
137
+ * providers: [NgsFormsService],
138
+ * template: `<ngs-form />`,
139
+ * })
140
+ * export class ProfileComponent {
141
+ * private readonly forms = inject(NgsFormsService);
142
+ * readonly value = toSignal(this.forms.formValue$);
143
+ * }
144
+ * ```
145
+ */
92
146
  class NgsFormsService {
147
+ /**
148
+ * @internal
149
+ */
93
150
  attempt = 0;
151
+ /**
152
+ * Emits the root form group's value every time it changes. Combine with `toSignal()` to read it in a template.
153
+ *
154
+ * @remarks The value is keyed by the root group's `name`, for example `{ profile: { firstName: 'Ada' } }`.
155
+ */
94
156
  formValue$ = new BehaviorSubject({});
157
+ /** The latest value emitted by `formValue$`. */
95
158
  formValue = {};
96
159
  internalFormService;
97
160
  formGroupSubscriptions = [];
98
161
  formGroup = new UntypedFormGroup({});
162
+ /** `true` once the user has changed any control in the form. */
99
163
  get dirty() {
100
164
  return this.formGroup?.dirty ?? false;
101
165
  }
166
+ /** `true` when every rendered control passes its validators. Hidden items are not validated. */
102
167
  get isValid() {
103
168
  if (!this.internalServiceIsSet)
104
169
  return false;
105
170
  return this.internalFormService.checkIsValid();
106
171
  }
172
+ /** `true` once `<ngs-form>` has connected to this service. */
107
173
  get internalServiceIsSet() {
108
174
  return !!this.internalFormService;
109
175
  }
176
+ /**
177
+ * Renders a form from `formConfig`, replacing any form that is already shown and resetting the submitted state.
178
+ *
179
+ * @remarks It is safe to call before `<ngs-form>` has initialised (for example in `ngOnInit`): the call is retried
180
+ * every 25 ms for about half a second until the form component is available.
181
+ */
110
182
  setFormConfig(formConfig) {
111
183
  if (!this.internalServiceIsSet) {
112
184
  if (this.attempt > 20) {
@@ -119,10 +191,17 @@ class NgsFormsService {
119
191
  }
120
192
  this.internalFormService.setFormData(formConfig);
121
193
  }
194
+ /**
195
+ * @internal
196
+ */
122
197
  setInternalService(internalFormsService) {
123
198
  this.internalFormService = internalFormsService;
124
199
  this.bindFormGroup();
125
200
  }
201
+ /**
202
+ * Marks the form as submitted (or not). While submitted, every invalid control shows its error message even if
203
+ * the user never touched it.
204
+ */
126
205
  setIsSubmitted(isSubmitted) {
127
206
  if (!this.internalServiceIsSet)
128
207
  return;
@@ -138,15 +217,26 @@ class NgsFormsService {
138
217
  }));
139
218
  });
140
219
  }
220
+ /**
221
+ * Merges `state` into the state of one component, identified by its `uuid` or control `name`.
222
+ * Components read the merged result through `commonState`.
223
+ */
141
224
  updateComponentState(componentKey, state) {
142
225
  this.internalFormService?.updateComponentState(componentKey, state);
143
226
  }
227
+ /** Merges `state` into the state shared by every component in the form. */
144
228
  updateGlobalState(state) {
145
229
  this.internalFormService?.updateGlobalState(state);
146
230
  }
147
231
  ngOnDestroy() {
148
232
  this.formGroupSubscriptions.forEach((subscription) => subscription.unsubscribe());
149
233
  }
234
+ /**
235
+ * Patches values into the form, using the same shape as `formValue$` (including the root group name).
236
+ *
237
+ * @remarks Only controls that are currently rendered receive values. Patch again after a conditional item becomes
238
+ * visible, and make sure a form array has enough rows (see `initialItemCount`) before patching array values.
239
+ */
150
240
  patchValue(entityData) {
151
241
  this.formGroup.patchValue(entityData);
152
242
  }
@@ -157,6 +247,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
157
247
  type: Injectable
158
248
  }] });
159
249
 
250
+ /**
251
+ * Per form state holder that connects `<ngs-form>`, its items and `NgsFormsService`.
252
+ *
253
+ * @remarks Provided by `NgsFormComponent`. Application code should use `NgsFormsService` instead; this service is
254
+ * exported for authors of custom components and may change between minor versions.
255
+ */
160
256
  class NgsFormsInternalService {
161
257
  formGroup$ = new BehaviorSubject(new UntypedFormGroup({}));
162
258
  formConfig$ = new BehaviorSubject(undefined);
@@ -208,12 +304,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
208
304
  type: Injectable
209
305
  }] });
210
306
 
307
+ /**
308
+ * Base class for items that own a form control. Creates the control, applies `validators` / `validators$`,
309
+ * registers it with the parent group under `config.name` and keeps `errorMessage` up to date.
310
+ */
211
311
  class NgsFormsFormItemWithVisibleAndValidatorsBase extends NgsFormsBaseClassFormComponent {
312
+ /** The item's form control. Bind it with `[formControl]`. */
212
313
  control;
314
+ /**
315
+ * Message for the control's first error, taken from `errorMessageMap`. Empty while the control is valid, or
316
+ * before it is touched, dirty or submitted.
317
+ */
213
318
  errorMessage = '';
214
319
  formAddRemoveFns = inject(NGS_FORMS_CONTROL_ADD_REMOVE_FN);
215
320
  privateService = inject(NgsFormsInternalService);
216
321
  changeDetectorRef = inject(ChangeDetectorRef);
322
+ /** Whether the form has been marked as submitted with `NgsFormsService.setIsSubmitted()`. */
217
323
  submitted = toSignal(this.privateService.isSubmitted$, { initialValue: false });
218
324
  ngOnInit() {
219
325
  if (!this.control) {
@@ -271,10 +377,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
271
377
  args: [{}]
272
378
  }] });
273
379
 
380
+ /**
381
+ * Base class for input components. Adds access to the form's `NgsFormsService` and the merged component state.
382
+ *
383
+ * @remarks This is the class to extend when you build your own input. See the Custom components guide.
384
+ */
274
385
  class NgsFormsBaseClassFormInputComponent extends NgsFormsFormItemWithVisibleAndValidatorsBase {
386
+ /** The `NgsFormsService` of the form this input belongs to. */
275
387
  myFormService = inject(NgsFormsService);
276
388
  internalService = inject(NgsFormsInternalService);
389
+ /** Merged state for this component: its own state, then state by name, then global state. */
277
390
  commonState = toSignal(this.internalService.subscribeToState(this.itemData.uuid || '', this.config.name), { initialValue: {} });
391
+ /**
392
+ * Switches between input and summary display.
393
+ *
394
+ * @notImplemented
395
+ */
278
396
  toggleDisplayMode(setTo) {
279
397
  }
280
398
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsBaseClassFormInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
@@ -285,6 +403,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
285
403
  args: [{}]
286
404
  }] });
287
405
 
406
+ /** Base class for container items that render `config.items` without owning a form control. */
288
407
  class NgsFormsBaseClassItemsContainerBase extends NgsFormsBaseClassFormComponent {
289
408
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsBaseClassItemsContainerBase, deps: null, target: i0.ɵɵFactoryTarget.Directive });
290
409
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsBaseClassItemsContainerBase, isStandalone: true, usesInheritance: true, ngImport: i0 });
@@ -294,7 +413,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
294
413
  args: [{}]
295
414
  }] });
296
415
 
416
+ /**
417
+ * Base class for inputs with options. Exposes `options` as a signal that follows `options$` when given and falls
418
+ * back to the static `options`.
419
+ */
297
420
  class NgsFormsBaseClassFormItemInputWithOptionsComponent extends NgsFormsBaseClassFormInputComponent {
421
+ /** The current options. */
298
422
  options = toSignal(this.config.options$ ?? of(this.config.options ?? []), { initialValue: this.config.options ?? [] });
299
423
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsBaseClassFormItemInputWithOptionsComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
300
424
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsBaseClassFormItemInputWithOptionsComponent, isStandalone: true, usesInheritance: true, ngImport: i0 });
@@ -372,10 +496,22 @@ class NgsFormItemController extends NgsSubscriber {
372
496
  }
373
497
  }
374
498
 
499
+ /**
500
+ * Structural building block that looks up a form item's component in the registry and renders it.
501
+ *
502
+ * @remarks Use it inside custom container components to render child items:
503
+ * `<ng-container [ngs-form-item]="item" [ngs-form-group]="group"></ng-container>`.
504
+ */
375
505
  class NgsFormsFormItemDirective {
506
+ /** The item to render. */
376
507
  itemData = undefined;
508
+ /** Form group that the item's control is added to. */
377
509
  formGroup;
510
+ /** Form array that the item's control is added to. */
378
511
  formArray;
512
+ /**
513
+ * Index of the item inside a form array row, provided to children as `NGS_FORMS_ITEM_INDEX`.
514
+ */
379
515
  index;
380
516
  controller;
381
517
  viewContainerRef = inject(ViewContainerRef);
@@ -421,24 +557,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
421
557
  args: ['ngs-form-index']
422
558
  }] } });
423
559
 
560
+ /**
561
+ * Renders the form described by the `NgsFormsFormConfig` given to the nearest `NgsFormsService`.
562
+ *
563
+ * @remarks Place it in the template of the component that provides `NgsFormsService`. It takes no inputs.
564
+ * @example
565
+ * ```html
566
+ * <ngs-form></ngs-form>
567
+ * ```
568
+ */
424
569
  class NgsFormComponent {
425
570
  ngsInternalFormsService = inject(NgsFormsInternalService);
426
571
  ngsFormsService = inject(NgsFormsService);
427
572
  formGroup = toSignal(this.ngsInternalFormsService.formGroup$);
428
573
  formConfig = toSignal(this.ngsInternalFormsService.formConfig$);
574
+ /** The root item to render with the form group it belongs to (empty until a config is set). */
575
+ rendered = computed(() => {
576
+ const group = this.formGroup();
577
+ const root = this.formConfig()?.root;
578
+ return group && root ? [{ group, root }] : [];
579
+ }, ...(ngDevMode ? [{ debugName: "rendered" }] : []));
429
580
  constructor() {
430
581
  this.ngsFormsService.setInternalService(this.ngsInternalFormsService);
431
582
  }
432
583
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
433
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: NgsFormComponent, isStandalone: true, selector: "ngs-form", providers: [NgsFormsInternalService], ngImport: i0, template: "@if (formConfig()?.root && formGroup()) {\n <ng-container [ngs-form-group]=\"formGroup()!\" [ngs-form-item]=\"formConfig()!.root\"></ng-container>\n}\n\n", dependencies: [{ kind: "directive", type: NgsFormsFormItemDirective, selector: "[ngs-form-item]", inputs: ["ngs-form-item", "ngs-form-group", "ngs-form-array", "ngs-form-index"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
584
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: NgsFormComponent, isStandalone: true, selector: "ngs-form", providers: [NgsFormsInternalService], ngImport: i0, template: "<!-- Tracking by form group re-creates the tree whenever setFormConfig() is called again, since each call creates a new group. -->\n@for (entry of rendered(); track entry.group) {\n <ng-container [ngs-form-group]=\"entry.group\" [ngs-form-item]=\"entry.root\"></ng-container>\n}\n", dependencies: [{ kind: "directive", type: NgsFormsFormItemDirective, selector: "[ngs-form-item]", inputs: ["ngs-form-item", "ngs-form-group", "ngs-form-array", "ngs-form-index"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
434
585
  }
435
586
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormComponent, decorators: [{
436
587
  type: Component,
437
- args: [{ selector: 'ngs-form', providers: [NgsFormsInternalService], imports: [NgsFormsFormItemDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (formConfig()?.root && formGroup()) {\n <ng-container [ngs-form-group]=\"formGroup()!\" [ngs-form-item]=\"formConfig()!.root\"></ng-container>\n}\n\n" }]
588
+ args: [{ selector: 'ngs-form', providers: [NgsFormsInternalService], imports: [NgsFormsFormItemDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Tracking by form group re-creates the tree whenever setFormConfig() is called again, since each call creates a new group. -->\n@for (entry of rendered(); track entry.group) {\n <ng-container [ngs-form-group]=\"entry.group\" [ngs-form-item]=\"entry.root\"></ng-container>\n}\n" }]
438
589
  }], ctorParameters: () => [] });
439
590
 
591
+ /**
592
+ * Renders its items one after another without any wrapper markup. Useful as a grouping node inside a row.
593
+ *
594
+ * @remarks Registry key: `column`. Does not create a form control.
595
+ */
440
596
  class NgsFormsColumnComponent extends NgsFormsBaseClassItemsContainerBase {
441
597
  static key = 'column';
598
+ /** Creates a `column` item. */
442
599
  static create(config) {
443
600
  return {
444
601
  uuid: config.uuid || v4(),
@@ -454,8 +611,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
454
611
  args: [{ selector: 'ngs-form-component-column', imports: [NgsFormsFormItemDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "@for (item of config.items; track item.uuid) {\n <ng-container [ngs-form-item]=\"item\"></ng-container>\n}\n" }]
455
612
  }] });
456
613
 
457
- // --
458
-
614
+ /**
615
+ * Lays out items side by side. Each item is wrapped in a div whose class comes from `columnClass` (same for every
616
+ * item) or `columnClasses` (one per item), so it works with any CSS grid system.
617
+ *
618
+ * @remarks Registry key: `form-row`. Does not create a form control.
619
+ * @example
620
+ * ```ts
621
+ * NgsFormsRowComponent.create({
622
+ * containerClass: 'row g-3',
623
+ * columnClasses: ['col-md-6', 'col-md-6'],
624
+ * items: [firstName, lastName],
625
+ * })
626
+ * ```
627
+ */
459
628
  class NgsFormsRowComponent extends NgsFormsBaseClassFormComponent {
460
629
  static key = 'form-row';
461
630
  constructor() {
@@ -470,6 +639,7 @@ class NgsFormsRowComponent extends NgsFormsBaseClassFormComponent {
470
639
  }
471
640
  return this.config.columnClasses[index];
472
641
  }
642
+ /** Creates a `form-row` item. */
473
643
  static create(config) {
474
644
  return {
475
645
  uuid: config.uuid || v4(),
@@ -485,8 +655,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
485
655
  args: [{ selector: 'ngs-forms-row-component', imports: [NgsFormsFormItemDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"{{config.containerClass}}\">\n @for(item of config.items; track item; let i = $index){\n <div class=\"{{getIndividualRowDivClass(i)}}\">\n <ng-container [ngs-form-item]=\"item\"></ng-container>\n </div>\n }\n</div>\n" }]
486
656
  }], ctorParameters: () => [] });
487
657
 
658
+ /**
659
+ * A named group of items backed by an `UntypedFormGroup`. Its value is nested under `name` in the form value.
660
+ *
661
+ * @remarks Registry key: `form-group`. Usually the `root` of a form, and also useful for nesting related fields
662
+ * (an address, for example) or for hiding a whole group with `visible$`.
663
+ * @example
664
+ * ```ts
665
+ * NgsFormsFormGroupComponent.create({
666
+ * name: 'address',
667
+ * items: [addressLine1, city, postcode],
668
+ * })
669
+ * ```
670
+ */
488
671
  class NgsFormsFormGroupComponent extends NgsFormsFormItemWithVisibleAndValidatorsBase {
489
672
  static key = 'form-group';
673
+ /** Creates a `form-group` item. */
490
674
  static create(config) {
491
675
  return {
492
676
  uuid: config.uuid || v4(),
@@ -509,9 +693,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
509
693
  args: [{ selector: 'ngs-forms-form-group', imports: [NgsFormsFormItemDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (config.items) {\n @for (item of config.items; track item.uuid) {\n <ng-container [ngs-form-item]=\"item\" [ngs-form-group]=\"formGroupControl\"></ng-container>\n }\n}\n" }]
510
694
  }], ctorParameters: () => [] });
511
695
 
696
+ /**
697
+ * Holds the `UntypedFormArray` of one `NgsFormsFormArrayContainerComponent` and is shared with its list and buttons.
698
+ *
699
+ * @remarks Inject it in a custom component placed inside a form array to add or remove rows programmatically.
700
+ */
512
701
  class NgsFormsFormArrayInternalService {
513
702
  config = inject(NGS_FORMS_ITEM_DATA).config;
514
703
  formArray = new UntypedFormArray([]);
704
+ /** Emits after a row is added or removed. */
515
705
  itemsChanged = new Subject();
516
706
  constructor() {
517
707
  if (this.config.initialItemCount) {
@@ -522,6 +712,7 @@ class NgsFormsFormArrayInternalService {
522
712
  }
523
713
  ngOnDestroy() {
524
714
  }
715
+ /** Removes the row at `index`, unless that would go below `minItems`. */
525
716
  removeAt(index) {
526
717
  if (this.config?.minItems && this.formArray.controls.length <= this.config.minItems) {
527
718
  return;
@@ -532,6 +723,7 @@ class NgsFormsFormArrayInternalService {
532
723
  this.formArray.removeAt(index);
533
724
  this.itemsChanged.next();
534
725
  }
726
+ /** Appends an empty row, unless the array already has `maxItems` rows. */
535
727
  addItem() {
536
728
  if (this.config?.maxItems && this.formArray.controls.length >= this.config.maxItems) {
537
729
  return;
@@ -546,9 +738,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
546
738
  type: Injectable
547
739
  }], ctorParameters: () => [] });
548
740
 
741
+ /**
742
+ * A button that appends a row to the enclosing form array, up to `maxItems`.
743
+ *
744
+ * @remarks Registry key: `form-array-add-item`. Must be placed inside an `NgsFormsFormArrayContainerComponent`.
745
+ */
549
746
  class NgsFormsFormArrayAddItemComponent extends NgsFormsBaseClassFormComponent {
550
747
  static key = 'form-array-add-item';
551
748
  internalArrayService = inject(NgsFormsFormArrayInternalService);
749
+ /** Creates a `form-array-add-item` item. */
552
750
  static create(config) {
553
751
  return {
554
752
  uuid: config.uuid || v4(),
@@ -567,6 +765,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
567
765
  args: [{ selector: 'ngs-forms-form-array-add-item', changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n (click)=\"addItem()\"\n [class]=\"config.buttonClass || 'btn btn-outline-primary'\"\n type=\"button\">\n @if (config.buttonIcon) {\n <i [class]=\"config.buttonIcon\" aria-hidden=\"true\"></i>\n }\n {{ config.buttonText || 'Add Item' }}\n</button>\n" }]
568
766
  }] });
569
767
 
768
+ /**
769
+ * A repeatable list of rows backed by an `UntypedFormArray`. Its value is an array under `name`.
770
+ *
771
+ * @remarks Registry key: `form-array`. Put an `NgsFormsFormArrayListComponent` inside `items` to render the rows,
772
+ * and `NgsFormsFormArrayAddItemComponent` / `NgsFormsFormArrayRemoveItemComponent` to let users change them.
773
+ */
570
774
  class NgsFormsFormArrayContainerComponent extends NgsFormsFormItemWithVisibleAndValidatorsBase {
571
775
  formArrayService;
572
776
  static key = 'form-array';
@@ -575,6 +779,7 @@ class NgsFormsFormArrayContainerComponent extends NgsFormsFormItemWithVisibleAnd
575
779
  this.formArrayService = formArrayService;
576
780
  this.control = this.formArrayService.formArray;
577
781
  }
782
+ /** Creates a `form-array` item. */
578
783
  static create(config) {
579
784
  return {
580
785
  uuid: config.uuid || v4(),
@@ -590,6 +795,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
590
795
  args: [{ selector: 'ngs-forms-form-array', imports: [NgsFormsFormItemDirective], providers: [NgsFormsFormArrayInternalService], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"config.containerClass || ''\">\n @for (item of config.items; track item.uuid) {\n <ng-container [ngs-form-item]=\"item\"></ng-container>\n }\n</div>\n" }]
591
796
  }], ctorParameters: () => [{ type: NgsFormsFormArrayInternalService }] });
592
797
 
798
+ /**
799
+ * Renders one copy of `templateItem` for every row of the enclosing form array. Each row gets its own form group,
800
+ * so the controls in the template become the properties of each array entry.
801
+ *
802
+ * @remarks Registry key: `form-array-list`. Must be placed inside an `NgsFormsFormArrayContainerComponent`.
803
+ */
593
804
  class NgsFormsFormArrayListComponent extends NgsFormsBaseClassFormComponent {
594
805
  static key = 'form-array-list';
595
806
  internalArrayService = inject(NgsFormsFormArrayInternalService);
@@ -600,6 +811,7 @@ class NgsFormsFormArrayListComponent extends NgsFormsBaseClassFormComponent {
600
811
  this.changeDetectorRef.markForCheck();
601
812
  });
602
813
  }
814
+ /** Creates a `form-array-list` item. */
603
815
  static create(config) {
604
816
  return {
605
817
  uuid: config.uuid || v4(),
@@ -618,11 +830,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
618
830
  args: [{ selector: 'ngs-forms-form-array-list', imports: [NgsFormsFormItemDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "@for (formGroup of myFormArray.controls; track formGroup; let i = $index) {\n <div [attr.data-index]=\"$index\" [class]=\"config.containerClass || ''\">\n <ng-container [ngs-form-group]=\"castToFormGroup(formGroup)\" [ngs-form-index]=\"i\"\n [ngs-form-item]=\"config.templateItem\"></ng-container>\n </div>\n}\n" }]
619
831
  }] });
620
832
 
833
+ /**
834
+ * A button that removes its own row from the enclosing form array, down to `minItems`.
835
+ *
836
+ * @remarks Registry key: `form-array-remove-item`. Place it inside the `templateItem` of an
837
+ * `NgsFormsFormArrayListComponent` so each row gets one.
838
+ */
621
839
  class NgsFormsFormArrayRemoveItemComponent extends NgsFormsBaseClassFormComponent {
622
840
  static key = 'form-array-remove-item';
623
841
  internalArrayService = inject(NgsFormsFormArrayInternalService);
624
842
  elementRef = inject(ElementRef);
625
843
  indexFromDi = inject(NGS_FORMS_ITEM_INDEX, { optional: true });
844
+ /** Index of the row this button belongs to. */
626
845
  get myIndex() {
627
846
  if (this.indexFromDi !== null && this.indexFromDi !== undefined) {
628
847
  return this.indexFromDi;
@@ -640,6 +859,7 @@ class NgsFormsFormArrayRemoveItemComponent extends NgsFormsBaseClassFormComponen
640
859
  constructor() {
641
860
  super();
642
861
  }
862
+ /** Creates a `form-array-remove-item` item. */
643
863
  static create(config) {
644
864
  return {
645
865
  uuid: config.uuid || v4(),
@@ -651,15 +871,21 @@ class NgsFormsFormArrayRemoveItemComponent extends NgsFormsBaseClassFormComponen
651
871
  this.internalArrayService.removeAt(this.myIndex);
652
872
  }
653
873
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsFormArrayRemoveItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
654
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: NgsFormsFormArrayRemoveItemComponent, isStandalone: true, selector: "ngs-forms-form-array-remove-item", usesInheritance: true, ngImport: i0, template: "<button\n (click)=\"removeItem()\"\n [class]=\"config.buttonClass || 'btn btn-outline-danger btn-sm'\"\n type=\"button\">\n @if (config.buttonIcon) {\n <i [class]=\"config.buttonIcon\" aria-hidden=\"true\"></i>\n }\n {{ config.buttonText || 'Remove' }}\n index:{{ myIndex }}\n</button>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
874
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: NgsFormsFormArrayRemoveItemComponent, isStandalone: true, selector: "ngs-forms-form-array-remove-item", usesInheritance: true, ngImport: i0, template: "<button\n (click)=\"removeItem()\"\n [class]=\"config.buttonClass || 'btn btn-outline-danger btn-sm'\"\n type=\"button\">\n @if (config.buttonIcon) {\n <i [class]=\"config.buttonIcon\" aria-hidden=\"true\"></i>\n }\n {{ config.buttonText || 'Remove' }}\n</button>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
655
875
  }
656
876
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsFormArrayRemoveItemComponent, decorators: [{
657
877
  type: Component,
658
- args: [{ selector: 'ngs-forms-form-array-remove-item', changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n (click)=\"removeItem()\"\n [class]=\"config.buttonClass || 'btn btn-outline-danger btn-sm'\"\n type=\"button\">\n @if (config.buttonIcon) {\n <i [class]=\"config.buttonIcon\" aria-hidden=\"true\"></i>\n }\n {{ config.buttonText || 'Remove' }}\n index:{{ myIndex }}\n</button>\n" }]
878
+ args: [{ selector: 'ngs-forms-form-array-remove-item', changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n (click)=\"removeItem()\"\n [class]=\"config.buttonClass || 'btn btn-outline-danger btn-sm'\"\n type=\"button\">\n @if (config.buttonIcon) {\n <i [class]=\"config.buttonIcon\" aria-hidden=\"true\"></i>\n }\n {{ config.buttonText || 'Remove' }}\n</button>\n" }]
659
879
  }], ctorParameters: () => [] });
660
880
 
881
+ /**
882
+ * Renders a line of static text, such as a heading or a hint, inside the form.
883
+ *
884
+ * @remarks Registry key: `text-div`. Config: `text` and optional `classes`.
885
+ */
661
886
  class NgsFormsTextDivComponent extends NgsFormsBaseClassFormComponent {
662
887
  static key = 'text-div';
888
+ /** Creates a `text-div` item. */
663
889
  static create(config) {
664
890
  return {
665
891
  uuid: config.uuid || v4(),
@@ -667,6 +893,9 @@ class NgsFormsTextDivComponent extends NgsFormsBaseClassFormComponent {
667
893
  config: config,
668
894
  };
669
895
  }
896
+ /**
897
+ * Shortcut for a heading: creates a `text-div` with `text` and the given class (default `h2`).
898
+ */
670
899
  static title(title, divClass = 'h2') {
671
900
  return NgsFormsTextDivComponent.create({
672
901
  text: title,
@@ -674,13 +903,19 @@ class NgsFormsTextDivComponent extends NgsFormsBaseClassFormComponent {
674
903
  });
675
904
  }
676
905
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsTextDivComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
677
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsTextDivComponent, isStandalone: true, selector: "ngs-form-component-text-div", usesInheritance: true, ngImport: i0, template: "<div class=\"{{config.classes\">{{ config.text }}</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
906
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsTextDivComponent, isStandalone: true, selector: "ngs-form-component-text-div", usesInheritance: true, ngImport: i0, template: "<div class=\"{{ config.classes }}\">{{ config.text }}</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
678
907
  }
679
908
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsTextDivComponent, decorators: [{
680
909
  type: Component,
681
- args: [{ selector: 'ngs-form-component-text-div', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"{{config.classes\">{{ config.text }}</div>\n" }]
910
+ args: [{ selector: 'ngs-form-component-text-div', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"{{ config.classes }}\">{{ config.text }}</div>\n" }]
682
911
  }] });
683
912
 
913
+ /**
914
+ * Renders an HTML string inside the form. The HTML is passed through Angular's sanitizer.
915
+ *
916
+ * @remarks Registry key: `content-html`. Registered by `registerCoreNgsFormComponents()`, which the theme packages'
917
+ * `registerAll…` helpers call for you.
918
+ */
684
919
  class NgsFormsHtmlContentComponent extends NgsFormsBaseClassFormComponent {
685
920
  static key = 'content-html';
686
921
  contentConfig = this.config;
@@ -692,6 +927,7 @@ class NgsFormsHtmlContentComponent extends NgsFormsBaseClassFormComponent {
692
927
  sanitizer.sanitize(SecurityContext.HTML, this.contentConfig.html) ||
693
928
  '';
694
929
  }
930
+ /** Creates a `content-html` item. */
695
931
  static create(config) {
696
932
  return {
697
933
  uuid: config.uuid || v4(),
@@ -712,8 +948,16 @@ const sectionConfigDefaults = {
712
948
  subtitleClass: 'lead',
713
949
  };
714
950
 
951
+ /**
952
+ * Groups items under a title and optional subtitle. Does not create a form control, so child values stay in the
953
+ * parent group.
954
+ *
955
+ * @remarks Registry key: `section`. Config: `title`, `subtitle`, `titleClass` (default `h3`),
956
+ * `subtitleClass` (default `lead`) and `items`.
957
+ */
715
958
  class NgsFormsFormSectionComponent extends NgsFormsBaseClassItemsContainerBase {
716
959
  static key = 'section';
960
+ /** Creates a `section` item. */
717
961
  static create(config) {
718
962
  return {
719
963
  uuid: config.uuid || v4(),
@@ -732,7 +976,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
732
976
  args: [{ selector: 'ngs-forms-form-section', imports: [NgsFormsFormItemDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"form-section\">\n <div class=\"form-section-title {{config.titleClass}}\">{{ config.title }}</div>\n @if (config.subtitle) {\n <div class=\"form-section-subtitle {{config.subtitleClass}}\">{{ config.subtitle }}</div>\n }\n @if (config.items.length) {\n <div class=\"form-section-items\">\n @for (item of config.items; track item.uuid) {\n <ng-container [ngs-form-item]=\"item\"></ng-container>\n }\n </div>\n }\n</div>\n" }]
733
977
  }] });
734
978
 
979
+ // --
980
+
981
+ /** Registration helpers for the structural components in `@ng-simplicity/forms-core`. */
735
982
  class NgsFormsCoreModule {
983
+ /**
984
+ * Registers the structural components: `form-group`, `form-row`, `column`, `form-array`, `form-array-list`,
985
+ * `form-array-add-item`, `form-array-remove-item`, `text-div`, `content-html` and `section`.
986
+ *
987
+ * @remarks The theme packages call this for you from their own `registerAll…` helpers.
988
+ */
736
989
  static registerCoreNgsFormComponents(registryService) {
737
990
  registryService.register(NgsFormsFormGroupComponent.key, NgsFormsFormGroupComponent);
738
991
  registryService.register(NgsFormsRowComponent.key, NgsFormsRowComponent);
@@ -742,7 +995,7 @@ class NgsFormsCoreModule {
742
995
  registryService.register(NgsFormsFormArrayRemoveItemComponent.key, NgsFormsFormArrayRemoveItemComponent);
743
996
  registryService.register(NgsFormsFormArrayListComponent.key, NgsFormsFormArrayListComponent);
744
997
  registryService.register(NgsFormsTextDivComponent.key, NgsFormsTextDivComponent);
745
- //registryService.register('content-html', NgsFormsHtmlContentComponent);
998
+ registryService.register(NgsFormsHtmlContentComponent.key, NgsFormsHtmlContentComponent);
746
999
  registryService.register(NgsFormsFormSectionComponent.key, NgsFormsFormSectionComponent);
747
1000
  }
748
1001
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsCoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });