@ng-simplicity/forms-core 1.1.1 → 1.1.3

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,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
- import { UntypedFormGroup, FormControl, UntypedFormArray, FormsModule, ReactiveFormsModule } from '@angular/forms';
6
+ import { UntypedFormGroup, FormControl, FormGroup, FormArray, 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,15 +65,29 @@ 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;
60
- id = v4();
77
+ /**
78
+ * Element id for this component, handy for `id` and `for` attributes. The config's `id` when it sets one,
79
+ * otherwise a generated unique id.
80
+ */
81
+ id;
82
+ /** The item's configuration. */
61
83
  config;
84
+ /** The full form item: `uuid`, `type` and `config`. */
62
85
  itemData;
63
86
  constructor() {
64
87
  super();
65
88
  this.itemData = inject(NGS_FORMS_ITEM_DATA);
66
89
  this.config = this.itemData.config;
90
+ this.id = this.config.id || v4();
67
91
  }
68
92
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsBaseClassFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
69
93
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsBaseClassFormComponent, isStandalone: true, usesInheritance: true, ngImport: i0 });
@@ -73,11 +97,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
73
97
  args: [{}]
74
98
  }], ctorParameters: () => [] });
75
99
 
100
+ /**
101
+ * Maps registry keys (the `type` of an `NgsFormsFormItem`) to the Angular components that render them.
102
+ *
103
+ * @remarks The service is provided in the `platform` injector, so there is one registry per page. Registering a
104
+ * key again replaces the previous component, which is how a theme package or your own component can override a
105
+ * built-in one. Register components before the form renders, typically in the constructor of the component that
106
+ * hosts `<ngs-form>`.
107
+ * @example
108
+ * ```ts
109
+ * constructor(registry: NgsFormsComponentRegistryService) {
110
+ * NgsFormsBootstrapModule.registerAllBootStrapComponents(registry);
111
+ * registry.register(MyRatingComponent.key, MyRatingComponent);
112
+ * }
113
+ * ```
114
+ */
76
115
  class NgsFormsComponentRegistryService {
77
116
  itemComponentRegistry = {};
117
+ /** Returns the component registered for `key`, or `undefined` when nothing is registered. */
78
118
  getComponentTypeForKey(key) {
79
119
  return this.itemComponentRegistry[key];
80
120
  }
121
+ /** Registers `component` under `key`, replacing any component already registered for it. */
81
122
  register(key, component) {
82
123
  this.itemComponentRegistry[key] = component;
83
124
  }
@@ -89,24 +130,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
89
130
  args: [{ providedIn: 'platform' }]
90
131
  }] });
91
132
 
133
+ /**
134
+ * The public API of a form. Provide it on the component that hosts `<ngs-form>` so each form gets its own instance.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * @Component({
139
+ * selector: 'app-profile',
140
+ * imports: [NgsFormComponent],
141
+ * providers: [NgsFormsService],
142
+ * template: `<ngs-form />`,
143
+ * })
144
+ * export class ProfileComponent {
145
+ * private readonly forms = inject(NgsFormsService);
146
+ * readonly value = toSignal(this.forms.formValue$);
147
+ * }
148
+ * ```
149
+ */
92
150
  class NgsFormsService {
151
+ /**
152
+ * @internal
153
+ */
93
154
  attempt = 0;
155
+ /**
156
+ * Emits the root form group's value every time it changes. Combine with `toSignal()` to read it in a template.
157
+ *
158
+ * @remarks The value is keyed by the root group's `name`, for example `{ profile: { firstName: 'Ada' } }`.
159
+ */
94
160
  formValue$ = new BehaviorSubject({});
161
+ /** The latest value emitted by `formValue$`. */
95
162
  formValue = {};
96
163
  internalFormService;
97
164
  formGroupSubscriptions = [];
98
165
  formGroup = new UntypedFormGroup({});
166
+ /** `true` once the user has changed any control in the form. */
99
167
  get dirty() {
100
168
  return this.formGroup?.dirty ?? false;
101
169
  }
170
+ /** `true` when every rendered control passes its validators. Hidden items are not validated. */
102
171
  get isValid() {
103
172
  if (!this.internalServiceIsSet)
104
173
  return false;
105
174
  return this.internalFormService.checkIsValid();
106
175
  }
176
+ /** `true` once `<ngs-form>` has connected to this service. */
107
177
  get internalServiceIsSet() {
108
178
  return !!this.internalFormService;
109
179
  }
180
+ /**
181
+ * Renders a form from `formConfig`, replacing any form that is already shown and resetting the submitted state.
182
+ *
183
+ * @remarks It is safe to call before `<ngs-form>` has initialised (for example in `ngOnInit`): the call is retried
184
+ * every 25 ms for about half a second until the form component is available.
185
+ */
110
186
  setFormConfig(formConfig) {
111
187
  if (!this.internalServiceIsSet) {
112
188
  if (this.attempt > 20) {
@@ -119,10 +195,17 @@ class NgsFormsService {
119
195
  }
120
196
  this.internalFormService.setFormData(formConfig);
121
197
  }
198
+ /**
199
+ * @internal
200
+ */
122
201
  setInternalService(internalFormsService) {
123
202
  this.internalFormService = internalFormsService;
124
203
  this.bindFormGroup();
125
204
  }
205
+ /**
206
+ * Marks the form as submitted (or not). While submitted, every invalid control shows its error message even if
207
+ * the user never touched it.
208
+ */
126
209
  setIsSubmitted(isSubmitted) {
127
210
  if (!this.internalServiceIsSet)
128
211
  return;
@@ -138,15 +221,26 @@ class NgsFormsService {
138
221
  }));
139
222
  });
140
223
  }
224
+ /**
225
+ * Merges `state` into the state of one component, identified by its `uuid` or control `name`.
226
+ * Components read the merged result through `commonState`.
227
+ */
141
228
  updateComponentState(componentKey, state) {
142
229
  this.internalFormService?.updateComponentState(componentKey, state);
143
230
  }
231
+ /** Merges `state` into the state shared by every component in the form. */
144
232
  updateGlobalState(state) {
145
233
  this.internalFormService?.updateGlobalState(state);
146
234
  }
147
235
  ngOnDestroy() {
148
236
  this.formGroupSubscriptions.forEach((subscription) => subscription.unsubscribe());
149
237
  }
238
+ /**
239
+ * Patches values into the form, using the same shape as `formValue$` (including the root group name).
240
+ *
241
+ * @remarks Only controls that are currently rendered receive values. Patch again after a conditional item becomes
242
+ * visible, and make sure a form array has enough rows (see `initialItemCount`) before patching array values.
243
+ */
150
244
  patchValue(entityData) {
151
245
  this.formGroup.patchValue(entityData);
152
246
  }
@@ -157,6 +251,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
157
251
  type: Injectable
158
252
  }] });
159
253
 
254
+ /**
255
+ * Per form state holder that connects `<ngs-form>`, its items and `NgsFormsService`.
256
+ *
257
+ * @remarks Provided by `NgsFormComponent`. Application code should use `NgsFormsService` instead; this service is
258
+ * exported for authors of custom components and may change between minor versions.
259
+ */
160
260
  class NgsFormsInternalService {
161
261
  formGroup$ = new BehaviorSubject(new UntypedFormGroup({}));
162
262
  formConfig$ = new BehaviorSubject(undefined);
@@ -171,6 +271,9 @@ class NgsFormsInternalService {
171
271
  return this.formGroup$.value.valid;
172
272
  }
173
273
  setFormData(formConfig) {
274
+ const state = { ...(formConfig.initialState ?? {}) };
275
+ state['global'] = { ...(formConfig.globalState ?? {}) };
276
+ this.state.next(state);
174
277
  this.formGroup$.next(new UntypedFormGroup({}));
175
278
  this.formConfig$.next(formConfig);
176
279
  this.isSubmitted$.next(false);
@@ -181,13 +284,24 @@ class NgsFormsInternalService {
181
284
  this.state.value[key] = updated;
182
285
  this.state.next(this.state.value);
183
286
  }
287
+ /** Sets a component's state from its config's `initialState`, unless state for `uuid` already exists. */
288
+ seedComponentState(uuid, initialState) {
289
+ if (!uuid || !initialState || this.state.value[uuid])
290
+ return;
291
+ this.mergeState(uuid, initialState);
292
+ }
184
293
  updateGlobalState(state) {
185
294
  this.mergeState('global', state);
186
295
  }
187
296
  updateComponentState(componentKey, state) {
188
297
  this.mergeState(componentKey, state);
189
298
  }
190
- subscribeToState(uuid, name) {
299
+ /**
300
+ * Merged state for one component: its own (by `uuid`), then by `name`, then global.
301
+ * `initialState` seeds the component's own state unless state for `uuid` already exists.
302
+ */
303
+ subscribeToState(uuid, name, initialState) {
304
+ this.seedComponentState(uuid, initialState);
191
305
  let last = '';
192
306
  return this.state.pipe(map((allState) => {
193
307
  const stateByUuid = allState[uuid] || {};
@@ -208,21 +322,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
208
322
  type: Injectable
209
323
  }] });
210
324
 
325
+ /**
326
+ * Base class for items that own a form control. Creates the control (seeded with `value` when the config has one),
327
+ * applies `validators` / `validators$` and `disabled` / `disabled$`, registers it with the parent group under
328
+ * `config.name` and keeps `errorMessage` up to date.
329
+ */
211
330
  class NgsFormsFormItemWithVisibleAndValidatorsBase extends NgsFormsBaseClassFormComponent {
331
+ /** The item's form control. Bind it with `[formControl]`. */
212
332
  control;
333
+ /**
334
+ * Message for the control's first error, taken from `errorMessageMap`. Empty while the control is valid, or
335
+ * before it is touched, dirty or submitted.
336
+ */
213
337
  errorMessage = '';
214
338
  formAddRemoveFns = inject(NGS_FORMS_CONTROL_ADD_REMOVE_FN);
215
339
  privateService = inject(NgsFormsInternalService);
216
340
  changeDetectorRef = inject(ChangeDetectorRef);
341
+ /** Whether the form has been marked as submitted with `NgsFormsService.setIsSubmitted()`. */
217
342
  submitted = toSignal(this.privateService.isSubmitted$, { initialValue: false });
343
+ /** The latest value of `disabled` / `disabled$`, or `undefined` when the config sets neither. */
344
+ disabledFromConfig;
218
345
  ngOnInit() {
219
346
  if (!this.control) {
220
- this.control = new FormControl(undefined, { validators: [] });
347
+ const initialValue = this.config.value;
348
+ this.control = new FormControl(initialValue, { validators: [] });
221
349
  }
222
- //this.bindVisible();
223
350
  this.bindValidators();
224
- //this.bindControlValidityChange();
225
351
  this.formAddRemoveFns.add(this.control, this.config.name);
352
+ this.bindDisabled();
226
353
  // Listen to changes and update error message dynamically
227
354
  this.subscribe(merge(this.control.valueChanges, this.control.statusChanges, this.privateService.isSubmitted$), () => {
228
355
  this.updateErrorMessage();
@@ -263,6 +390,34 @@ class NgsFormsFormItemWithVisibleAndValidatorsBase extends NgsFormsBaseClassForm
263
390
  this.subscribe(this.config.validators$, (validators) => this.control.setValidators(validators));
264
391
  }
265
392
  }
393
+ bindDisabled() {
394
+ const disabled$ = this.config.disabled$ ?? (this.config.disabled !== undefined ? of(this.config.disabled) : undefined);
395
+ if (!disabled$)
396
+ return;
397
+ const control = this.control;
398
+ this.subscribe(disabled$, (disabled) => {
399
+ this.disabledFromConfig = !!disabled;
400
+ this.applyDisabled(control);
401
+ });
402
+ // Child controls are added to groups and arrays after the container has been disabled, and adding an enabled
403
+ // child re-enables its parent. Re-apply the configured state whenever a container's status changes.
404
+ if (control instanceof FormGroup || control instanceof FormArray) {
405
+ this.subscribe(control.statusChanges, () => {
406
+ if (this.disabledFromConfig && control.enabled) {
407
+ control.disable();
408
+ }
409
+ });
410
+ }
411
+ }
412
+ applyDisabled(control) {
413
+ if (this.disabledFromConfig && !control.disabled) {
414
+ control.disable();
415
+ }
416
+ else if (!this.disabledFromConfig && control.disabled) {
417
+ control.enable();
418
+ }
419
+ this.changeDetectorRef.markForCheck();
420
+ }
266
421
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsFormItemWithVisibleAndValidatorsBase, deps: null, target: i0.ɵɵFactoryTarget.Directive });
267
422
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsFormItemWithVisibleAndValidatorsBase, isStandalone: true, usesInheritance: true, ngImport: i0 });
268
423
  }
@@ -271,11 +426,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
271
426
  args: [{}]
272
427
  }] });
273
428
 
429
+ /**
430
+ * Base class for input components. Adds access to the form's `NgsFormsService` and the merged component state.
431
+ *
432
+ * @remarks This is the class to extend when you build your own input. See the Custom components guide.
433
+ */
274
434
  class NgsFormsBaseClassFormInputComponent extends NgsFormsFormItemWithVisibleAndValidatorsBase {
435
+ /** The `NgsFormsService` of the form this input belongs to. */
275
436
  myFormService = inject(NgsFormsService);
276
437
  internalService = inject(NgsFormsInternalService);
277
- commonState = toSignal(this.internalService.subscribeToState(this.itemData.uuid || '', this.config.name), { initialValue: {} });
438
+ /** Merged state for this component: its own state, then state by name, then global state. */
439
+ commonState = toSignal(this.internalService.subscribeToState(this.itemData.uuid || '', this.config.name, this.config.initialState), { initialValue: {} });
440
+ /**
441
+ * Switches this component between `input` and `summary` display by updating its own state.
442
+ * With no argument it flips the current mode.
443
+ */
278
444
  toggleDisplayMode(setTo) {
445
+ const current = this.commonState().displayMode ?? 'input';
446
+ const displayMode = setTo ?? (current === 'summary' ? 'input' : 'summary');
447
+ this.internalService.updateComponentState(this.itemData.uuid || this.config.name, { displayMode });
279
448
  }
280
449
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsBaseClassFormInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
281
450
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsBaseClassFormInputComponent, isStandalone: true, usesInheritance: true, ngImport: i0 });
@@ -285,6 +454,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
285
454
  args: [{}]
286
455
  }] });
287
456
 
457
+ /** Base class for container items that render `config.items` without owning a form control. */
288
458
  class NgsFormsBaseClassItemsContainerBase extends NgsFormsBaseClassFormComponent {
289
459
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsBaseClassItemsContainerBase, deps: null, target: i0.ɵɵFactoryTarget.Directive });
290
460
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsBaseClassItemsContainerBase, isStandalone: true, usesInheritance: true, ngImport: i0 });
@@ -294,7 +464,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
294
464
  args: [{}]
295
465
  }] });
296
466
 
467
+ /**
468
+ * Base class for inputs with options. Exposes `options` as a signal that follows `options$` when given and falls
469
+ * back to the static `options`.
470
+ */
297
471
  class NgsFormsBaseClassFormItemInputWithOptionsComponent extends NgsFormsBaseClassFormInputComponent {
472
+ /** The current options. */
298
473
  options = toSignal(this.config.options$ ?? of(this.config.options ?? []), { initialValue: this.config.options ?? [] });
299
474
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsBaseClassFormItemInputWithOptionsComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive });
300
475
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: NgsFormsBaseClassFormItemInputWithOptionsComponent, isStandalone: true, usesInheritance: true, ngImport: i0 });
@@ -328,11 +503,13 @@ class NgsFormItemController extends NgsSubscriber {
328
503
  super.ngOnDestroy();
329
504
  }
330
505
  bindVisibility() {
331
- if (!this.itemData.config.visible$) {
506
+ const config = this.itemData.config;
507
+ const visible$ = config.visible$ ?? (config.visible !== undefined ? of(config.visible) : undefined);
508
+ if (!visible$) {
332
509
  Promise.resolve().then(() => this.create());
333
510
  return;
334
511
  }
335
- this.subscribe(this.itemData.config.visible$, (visible) => {
512
+ this.subscribe(visible$, (visible) => {
336
513
  if (this.lastVisible === visible)
337
514
  return;
338
515
  this.lastVisible = visible;
@@ -372,10 +549,22 @@ class NgsFormItemController extends NgsSubscriber {
372
549
  }
373
550
  }
374
551
 
552
+ /**
553
+ * Structural building block that looks up a form item's component in the registry and renders it.
554
+ *
555
+ * @remarks Use it inside custom container components to render child items:
556
+ * `<ng-container [ngs-form-item]="item" [ngs-form-group]="group"></ng-container>`.
557
+ */
375
558
  class NgsFormsFormItemDirective {
559
+ /** The item to render. */
376
560
  itemData = undefined;
561
+ /** Form group that the item's control is added to. */
377
562
  formGroup;
563
+ /** Form array that the item's control is added to. */
378
564
  formArray;
565
+ /**
566
+ * Index of the item inside a form array row, provided to children as `NGS_FORMS_ITEM_INDEX`.
567
+ */
379
568
  index;
380
569
  controller;
381
570
  viewContainerRef = inject(ViewContainerRef);
@@ -421,24 +610,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
421
610
  args: ['ngs-form-index']
422
611
  }] } });
423
612
 
613
+ /**
614
+ * Renders the form described by the `NgsFormsFormConfig` given to the nearest `NgsFormsService`.
615
+ *
616
+ * @remarks Place it in the template of the component that provides `NgsFormsService`. It takes no inputs.
617
+ * @example
618
+ * ```html
619
+ * <ngs-form></ngs-form>
620
+ * ```
621
+ */
424
622
  class NgsFormComponent {
425
623
  ngsInternalFormsService = inject(NgsFormsInternalService);
426
624
  ngsFormsService = inject(NgsFormsService);
427
625
  formGroup = toSignal(this.ngsInternalFormsService.formGroup$);
428
626
  formConfig = toSignal(this.ngsInternalFormsService.formConfig$);
627
+ /** The root item to render with the form group it belongs to (empty until a config is set). */
628
+ rendered = computed(() => {
629
+ const group = this.formGroup();
630
+ const root = this.formConfig()?.root;
631
+ return group && root ? [{ group, root }] : [];
632
+ }, ...(ngDevMode ? [{ debugName: "rendered" }] : []));
429
633
  constructor() {
430
634
  this.ngsFormsService.setInternalService(this.ngsInternalFormsService);
431
635
  }
432
636
  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 });
637
+ 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
638
  }
435
639
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormComponent, decorators: [{
436
640
  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" }]
641
+ 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
642
  }], ctorParameters: () => [] });
439
643
 
644
+ /**
645
+ * Renders its items one after another without any wrapper markup. Useful as a grouping node inside a row.
646
+ *
647
+ * @remarks Registry key: `column`. Does not create a form control.
648
+ */
440
649
  class NgsFormsColumnComponent extends NgsFormsBaseClassItemsContainerBase {
441
650
  static key = 'column';
651
+ /** Creates a `column` item. */
442
652
  static create(config) {
443
653
  return {
444
654
  uuid: config.uuid || v4(),
@@ -454,8 +664,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
454
664
  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
665
  }] });
456
666
 
457
- // --
458
-
667
+ /**
668
+ * Lays out items side by side. Each item is wrapped in a div whose class comes from `columnClass` (same for every
669
+ * item) or `columnClasses` (one per item), so it works with any CSS grid system.
670
+ *
671
+ * @remarks Registry key: `form-row`. Does not create a form control.
672
+ * @example
673
+ * ```ts
674
+ * NgsFormsRowComponent.create({
675
+ * containerClass: 'row g-3',
676
+ * columnClasses: ['col-md-6', 'col-md-6'],
677
+ * items: [firstName, lastName],
678
+ * })
679
+ * ```
680
+ */
459
681
  class NgsFormsRowComponent extends NgsFormsBaseClassFormComponent {
460
682
  static key = 'form-row';
461
683
  constructor() {
@@ -470,6 +692,7 @@ class NgsFormsRowComponent extends NgsFormsBaseClassFormComponent {
470
692
  }
471
693
  return this.config.columnClasses[index];
472
694
  }
695
+ /** Creates a `form-row` item. */
473
696
  static create(config) {
474
697
  return {
475
698
  uuid: config.uuid || v4(),
@@ -485,8 +708,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
485
708
  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
709
  }], ctorParameters: () => [] });
487
710
 
711
+ /**
712
+ * A named group of items backed by an `UntypedFormGroup`. Its value is nested under `name` in the form value.
713
+ *
714
+ * @remarks Registry key: `form-group`. Usually the `root` of a form, and also useful for nesting related fields
715
+ * (an address, for example) or for hiding a whole group with `visible$`.
716
+ * @example
717
+ * ```ts
718
+ * NgsFormsFormGroupComponent.create({
719
+ * name: 'address',
720
+ * items: [addressLine1, city, postcode],
721
+ * })
722
+ * ```
723
+ */
488
724
  class NgsFormsFormGroupComponent extends NgsFormsFormItemWithVisibleAndValidatorsBase {
489
725
  static key = 'form-group';
726
+ /** Creates a `form-group` item. */
490
727
  static create(config) {
491
728
  return {
492
729
  uuid: config.uuid || v4(),
@@ -509,9 +746,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
509
746
  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
747
  }], ctorParameters: () => [] });
511
748
 
749
+ /**
750
+ * Holds the `UntypedFormArray` of one `NgsFormsFormArrayContainerComponent` and is shared with its list and buttons.
751
+ *
752
+ * @remarks Inject it in a custom component placed inside a form array to add or remove rows programmatically.
753
+ */
512
754
  class NgsFormsFormArrayInternalService {
513
755
  config = inject(NGS_FORMS_ITEM_DATA).config;
514
756
  formArray = new UntypedFormArray([]);
757
+ /** Emits after a row is added or removed. */
515
758
  itemsChanged = new Subject();
516
759
  constructor() {
517
760
  if (this.config.initialItemCount) {
@@ -522,6 +765,7 @@ class NgsFormsFormArrayInternalService {
522
765
  }
523
766
  ngOnDestroy() {
524
767
  }
768
+ /** Removes the row at `index`, unless that would go below `minItems`. */
525
769
  removeAt(index) {
526
770
  if (this.config?.minItems && this.formArray.controls.length <= this.config.minItems) {
527
771
  return;
@@ -532,6 +776,7 @@ class NgsFormsFormArrayInternalService {
532
776
  this.formArray.removeAt(index);
533
777
  this.itemsChanged.next();
534
778
  }
779
+ /** Appends an empty row, unless the array already has `maxItems` rows. */
535
780
  addItem() {
536
781
  if (this.config?.maxItems && this.formArray.controls.length >= this.config.maxItems) {
537
782
  return;
@@ -546,9 +791,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
546
791
  type: Injectable
547
792
  }], ctorParameters: () => [] });
548
793
 
794
+ /**
795
+ * A button that appends a row to the enclosing form array, up to `maxItems`.
796
+ *
797
+ * @remarks Registry key: `form-array-add-item`. Must be placed inside an `NgsFormsFormArrayContainerComponent`.
798
+ */
549
799
  class NgsFormsFormArrayAddItemComponent extends NgsFormsBaseClassFormComponent {
550
800
  static key = 'form-array-add-item';
551
801
  internalArrayService = inject(NgsFormsFormArrayInternalService);
802
+ /** Creates a `form-array-add-item` item. */
552
803
  static create(config) {
553
804
  return {
554
805
  uuid: config.uuid || v4(),
@@ -567,6 +818,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
567
818
  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
819
  }] });
569
820
 
821
+ /**
822
+ * A repeatable list of rows backed by an `UntypedFormArray`. Its value is an array under `name`.
823
+ *
824
+ * @remarks Registry key: `form-array`. Put an `NgsFormsFormArrayListComponent` inside `items` to render the rows,
825
+ * and `NgsFormsFormArrayAddItemComponent` / `NgsFormsFormArrayRemoveItemComponent` to let users change them.
826
+ */
570
827
  class NgsFormsFormArrayContainerComponent extends NgsFormsFormItemWithVisibleAndValidatorsBase {
571
828
  formArrayService;
572
829
  static key = 'form-array';
@@ -575,6 +832,7 @@ class NgsFormsFormArrayContainerComponent extends NgsFormsFormItemWithVisibleAnd
575
832
  this.formArrayService = formArrayService;
576
833
  this.control = this.formArrayService.formArray;
577
834
  }
835
+ /** Creates a `form-array` item. */
578
836
  static create(config) {
579
837
  return {
580
838
  uuid: config.uuid || v4(),
@@ -590,6 +848,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
590
848
  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
849
  }], ctorParameters: () => [{ type: NgsFormsFormArrayInternalService }] });
592
850
 
851
+ /**
852
+ * Renders one copy of `templateItem` for every row of the enclosing form array. Each row gets its own form group,
853
+ * so the controls in the template become the properties of each array entry.
854
+ *
855
+ * @remarks Registry key: `form-array-list`. Must be placed inside an `NgsFormsFormArrayContainerComponent`.
856
+ */
593
857
  class NgsFormsFormArrayListComponent extends NgsFormsBaseClassFormComponent {
594
858
  static key = 'form-array-list';
595
859
  internalArrayService = inject(NgsFormsFormArrayInternalService);
@@ -600,6 +864,7 @@ class NgsFormsFormArrayListComponent extends NgsFormsBaseClassFormComponent {
600
864
  this.changeDetectorRef.markForCheck();
601
865
  });
602
866
  }
867
+ /** Creates a `form-array-list` item. */
603
868
  static create(config) {
604
869
  return {
605
870
  uuid: config.uuid || v4(),
@@ -618,11 +883,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
618
883
  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
884
  }] });
620
885
 
886
+ /**
887
+ * A button that removes its own row from the enclosing form array, down to `minItems`.
888
+ *
889
+ * @remarks Registry key: `form-array-remove-item`. Place it inside the `templateItem` of an
890
+ * `NgsFormsFormArrayListComponent` so each row gets one.
891
+ */
621
892
  class NgsFormsFormArrayRemoveItemComponent extends NgsFormsBaseClassFormComponent {
622
893
  static key = 'form-array-remove-item';
623
894
  internalArrayService = inject(NgsFormsFormArrayInternalService);
624
895
  elementRef = inject(ElementRef);
625
896
  indexFromDi = inject(NGS_FORMS_ITEM_INDEX, { optional: true });
897
+ /** Index of the row this button belongs to. */
626
898
  get myIndex() {
627
899
  if (this.indexFromDi !== null && this.indexFromDi !== undefined) {
628
900
  return this.indexFromDi;
@@ -640,6 +912,7 @@ class NgsFormsFormArrayRemoveItemComponent extends NgsFormsBaseClassFormComponen
640
912
  constructor() {
641
913
  super();
642
914
  }
915
+ /** Creates a `form-array-remove-item` item. */
643
916
  static create(config) {
644
917
  return {
645
918
  uuid: config.uuid || v4(),
@@ -651,15 +924,21 @@ class NgsFormsFormArrayRemoveItemComponent extends NgsFormsBaseClassFormComponen
651
924
  this.internalArrayService.removeAt(this.myIndex);
652
925
  }
653
926
  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 });
927
+ 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
928
  }
656
929
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsFormArrayRemoveItemComponent, decorators: [{
657
930
  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" }]
931
+ 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
932
  }], ctorParameters: () => [] });
660
933
 
934
+ /**
935
+ * Renders a line of static text, such as a heading or a hint, inside the form.
936
+ *
937
+ * @remarks Registry key: `text-div`. Config: `text` and optional `classes`.
938
+ */
661
939
  class NgsFormsTextDivComponent extends NgsFormsBaseClassFormComponent {
662
940
  static key = 'text-div';
941
+ /** Creates a `text-div` item. */
663
942
  static create(config) {
664
943
  return {
665
944
  uuid: config.uuid || v4(),
@@ -667,6 +946,9 @@ class NgsFormsTextDivComponent extends NgsFormsBaseClassFormComponent {
667
946
  config: config,
668
947
  };
669
948
  }
949
+ /**
950
+ * Shortcut for a heading: creates a `text-div` with `text` and the given class (default `h2`).
951
+ */
670
952
  static title(title, divClass = 'h2') {
671
953
  return NgsFormsTextDivComponent.create({
672
954
  text: title,
@@ -674,13 +956,19 @@ class NgsFormsTextDivComponent extends NgsFormsBaseClassFormComponent {
674
956
  });
675
957
  }
676
958
  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 });
959
+ 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
960
  }
679
961
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsTextDivComponent, decorators: [{
680
962
  type: Component,
681
- args: [{ selector: 'ngs-form-component-text-div', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"{{config.classes\">{{ config.text }}</div>\n" }]
963
+ args: [{ selector: 'ngs-form-component-text-div', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"{{ config.classes }}\">{{ config.text }}</div>\n" }]
682
964
  }] });
683
965
 
966
+ /**
967
+ * Renders an HTML string inside the form. The HTML is passed through Angular's sanitizer.
968
+ *
969
+ * @remarks Registry key: `content-html`. Registered by `registerCoreNgsFormComponents()`, which the theme packages'
970
+ * `registerAll…` helpers call for you.
971
+ */
684
972
  class NgsFormsHtmlContentComponent extends NgsFormsBaseClassFormComponent {
685
973
  static key = 'content-html';
686
974
  contentConfig = this.config;
@@ -692,6 +980,7 @@ class NgsFormsHtmlContentComponent extends NgsFormsBaseClassFormComponent {
692
980
  sanitizer.sanitize(SecurityContext.HTML, this.contentConfig.html) ||
693
981
  '';
694
982
  }
983
+ /** Creates a `content-html` item. */
695
984
  static create(config) {
696
985
  return {
697
986
  uuid: config.uuid || v4(),
@@ -712,8 +1001,16 @@ const sectionConfigDefaults = {
712
1001
  subtitleClass: 'lead',
713
1002
  };
714
1003
 
1004
+ /**
1005
+ * Groups items under a title and optional subtitle. Does not create a form control, so child values stay in the
1006
+ * parent group.
1007
+ *
1008
+ * @remarks Registry key: `section`. Config: `title`, `subtitle`, `titleClass` (default `h3`),
1009
+ * `subtitleClass` (default `lead`) and `items`.
1010
+ */
715
1011
  class NgsFormsFormSectionComponent extends NgsFormsBaseClassItemsContainerBase {
716
1012
  static key = 'section';
1013
+ /** Creates a `section` item. */
717
1014
  static create(config) {
718
1015
  return {
719
1016
  uuid: config.uuid || v4(),
@@ -732,7 +1029,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
732
1029
  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
1030
  }] });
734
1031
 
1032
+ // --
1033
+
1034
+ /** Registration helpers for the structural components in `@ng-simplicity/forms-core`. */
735
1035
  class NgsFormsCoreModule {
1036
+ /**
1037
+ * Registers the structural components: `form-group`, `form-row`, `column`, `form-array`, `form-array-list`,
1038
+ * `form-array-add-item`, `form-array-remove-item`, `text-div`, `content-html` and `section`.
1039
+ *
1040
+ * @remarks The theme packages call this for you from their own `registerAll…` helpers.
1041
+ */
736
1042
  static registerCoreNgsFormComponents(registryService) {
737
1043
  registryService.register(NgsFormsFormGroupComponent.key, NgsFormsFormGroupComponent);
738
1044
  registryService.register(NgsFormsRowComponent.key, NgsFormsRowComponent);
@@ -742,7 +1048,7 @@ class NgsFormsCoreModule {
742
1048
  registryService.register(NgsFormsFormArrayRemoveItemComponent.key, NgsFormsFormArrayRemoveItemComponent);
743
1049
  registryService.register(NgsFormsFormArrayListComponent.key, NgsFormsFormArrayListComponent);
744
1050
  registryService.register(NgsFormsTextDivComponent.key, NgsFormsTextDivComponent);
745
- //registryService.register('content-html', NgsFormsHtmlContentComponent);
1051
+ registryService.register(NgsFormsHtmlContentComponent.key, NgsFormsHtmlContentComponent);
746
1052
  registryService.register(NgsFormsFormSectionComponent.key, NgsFormsFormSectionComponent);
747
1053
  }
748
1054
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NgsFormsCoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });