@stemy/ngx-dynamic-form 19.2.13 → 19.3.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,6 +1,6 @@
1
1
  import * as i2 from '@stemy/ngx-utils';
2
- import { cachedFactory, ReflectUtils, ObjectUtils, LANGUAGE_SERVICE, API_SERVICE, StringUtils, TOASTER_SERVICE, EventsService, NgxUtilsModule } from '@stemy/ngx-utils';
3
- import { Subject, BehaviorSubject, map, startWith, distinctUntilChanged, switchMap, first, from, isObservable, firstValueFrom } from 'rxjs';
2
+ import { cachedFactory, ReflectUtils, ObjectUtils, LANGUAGE_SERVICE, ForbiddenZone, API_SERVICE, StringUtils, TOASTER_SERVICE, EventsService, NgxUtilsModule } from '@stemy/ngx-utils';
3
+ import { Subject, BehaviorSubject, startWith, distinctUntilChanged, switchMap, from, isObservable, firstValueFrom, first } from 'rxjs';
4
4
  import * as i0 from '@angular/core';
5
5
  import { Inject, Injectable, input, output, inject, Renderer2, ElementRef, computed, signal, effect, HostListener, HostBinding, Directive, Injector, ChangeDetectionStrategy, ViewEncapsulation, Component, makeEnvironmentProviders, NgModule } from '@angular/core';
6
6
  import * as i1 from '@angular/forms';
@@ -17,18 +17,18 @@ const FORM_ROOT_KEY = "__root";
17
17
 
18
18
  function customizeFormField(...providers) {
19
19
  const factory = cachedFactory(providers);
20
- return async (property, schema, config, options, injector) => {
20
+ return async (field, options, injector, property, schema) => {
21
21
  const customizers = factory(injector);
22
- const configs = [config];
22
+ const fields = [field];
23
23
  for (const customizer of customizers) {
24
- const index = configs.findIndex(m => customizer.acceptField(m));
24
+ const index = fields.findIndex(m => customizer.acceptField(m, property, schema));
25
25
  if (index >= 0) {
26
- const custom = await customizer.customizeField(configs[index], property, schema, options);
26
+ const custom = await customizer.customizeField(fields[index], options, property, schema);
27
27
  const result = Array.isArray(custom) ? custom : [custom];
28
- configs.splice(index, 1, ...result);
28
+ fields.splice(index, 1, ...result);
29
29
  }
30
30
  }
31
- return configs;
31
+ return fields;
32
32
  };
33
33
  }
34
34
 
@@ -176,32 +176,9 @@ function maxValueValidation(max, each) {
176
176
  return validateEach(each, v => typeof v == "number" && v <= max, "maxValue");
177
177
  }
178
178
 
179
- function isStringWithVal(val) {
180
- return typeof val == "string" && val.length > 0;
181
- }
182
- function findRefs(property) {
183
- const refs = Array.isArray(property.allOf)
184
- ? property.allOf.map(o => o.$ref).filter(isStringWithVal)
185
- : [property.items?.$ref, property.$ref].filter(isStringWithVal);
186
- return refs.map(t => t.split("/").pop());
187
- }
188
179
  function replaceSpecialChars(str, to = "-") {
189
180
  return `${str}`.replace(/[&\/\\#, +()$~%.@'":*?<>{}]/g, to);
190
181
  }
191
- function mergeFormFields(formFields) {
192
- const res = [];
193
- for (const formModel of formFields) {
194
- for (const subModel of formModel) {
195
- const index = res.findIndex(t => t.key == subModel.key);
196
- if (index >= 0) {
197
- res[index] = subModel;
198
- continue;
199
- }
200
- res.push(subModel);
201
- }
202
- }
203
- return res;
204
- }
205
182
  function getFieldByPath(field, path) {
206
183
  if (field.path === path) {
207
184
  return field;
@@ -215,6 +192,18 @@ function getFieldByPath(field, path) {
215
192
  }
216
193
  return null;
217
194
  }
195
+ function getFieldsByKey(field, key) {
196
+ if (field.key === key) {
197
+ return [field];
198
+ }
199
+ if (!field.fieldGroup)
200
+ return [];
201
+ const results = [];
202
+ for (const sf of field.fieldGroup) {
203
+ results.push(...getFieldsByKey(sf, key));
204
+ }
205
+ return results;
206
+ }
218
207
  function setFieldHidden(field, hidden = true) {
219
208
  const hide = field.expressions?.hide;
220
209
  if (hide) {
@@ -245,6 +234,113 @@ const MIN_INPUT_NUM = -999999999;
245
234
  const MAX_INPUT_NUM = 999999999;
246
235
  const EDITOR_FORMATS = ["php", "json", "html", "css", "scss"];
247
236
 
237
+ class ConfigForSchemaWrap {
238
+ opts;
239
+ mode;
240
+ injector;
241
+ schema;
242
+ get labelPrefix() {
243
+ return this.opts.labelPrefix;
244
+ }
245
+ get labelCustomizer() {
246
+ return this.opts.labelCustomizer;
247
+ }
248
+ get testId() {
249
+ return this.opts.testId;
250
+ }
251
+ fieldCustomizer;
252
+ constructor(opts, mode, injector, schema) {
253
+ this.opts = opts;
254
+ this.mode = mode;
255
+ this.injector = injector;
256
+ this.schema = schema;
257
+ this.fieldCustomizer = this.mode !== "wrap" || !ObjectUtils.isFunction(this.opts.fieldCustomizer)
258
+ ? field => field
259
+ : this.opts.fieldCustomizer;
260
+ }
261
+ async customize(field, property, schema) {
262
+ field.defaultValue = `${field.props?.type}`.startsWith("date")
263
+ ? convertToDate(property.default) : property.default;
264
+ const res = await ForbiddenZone.run("customizer", () => this.fieldCustomizer(field, this.forCustomizer(), this.injector, property, schema));
265
+ return !res ? [field] : handleConfigs(res);
266
+ }
267
+ forCustomizer() {
268
+ return new ConfigForSchemaWrap(this.opts, "customizer", this.injector, this.schema);
269
+ }
270
+ forSchema(schema) {
271
+ return new ConfigForSchemaWrap(this.opts, this.mode, this.injector, schema);
272
+ }
273
+ }
274
+ async function toWrapOptions(customizeOrOptions, injector, schema, errorMsg) {
275
+ if (errorMsg && ForbiddenZone.isForbidden("customizer")) {
276
+ throw new Error(errorMsg);
277
+ }
278
+ if (customizeOrOptions instanceof ConfigForSchemaWrap) {
279
+ return customizeOrOptions;
280
+ }
281
+ let schemaOptions = customizeOrOptions;
282
+ if (!ObjectUtils.isObject(schemaOptions)) {
283
+ schemaOptions = {
284
+ fieldCustomizer: customizeOrOptions
285
+ };
286
+ }
287
+ return new ConfigForSchemaWrap(schemaOptions, "wrap", injector, schema);
288
+ }
289
+ function convertToDate(value) {
290
+ if (ObjectUtils.isNullOrUndefined(value))
291
+ return null;
292
+ const date = ObjectUtils.isDate(value)
293
+ ? value
294
+ : new Date(value);
295
+ return isNaN(date) ? new Date() : date;
296
+ }
297
+ function handleConfigs(configs) {
298
+ return Array.isArray(configs) ? configs : [configs];
299
+ }
300
+ function isStringWithVal(val) {
301
+ return typeof val == "string" && val.length > 0;
302
+ }
303
+ function findRefs(property) {
304
+ const refs = Array.isArray(property.allOf)
305
+ ? property.allOf.map(o => o.$ref).filter(isStringWithVal)
306
+ : [property.items?.$ref, property.$ref].filter(isStringWithVal);
307
+ return refs.map(t => t.split("/").pop());
308
+ }
309
+ function mergeFormFields(formFields) {
310
+ const res = [];
311
+ for (const formModel of formFields) {
312
+ for (const subModel of formModel) {
313
+ const index = res.findIndex(t => t.key == subModel.key);
314
+ if (index >= 0) {
315
+ res[index] = subModel;
316
+ continue;
317
+ }
318
+ res.push(subModel);
319
+ }
320
+ }
321
+ return res;
322
+ }
323
+ function getFormValidationErrors(controls, parentPath = "") {
324
+ const errors = [];
325
+ Object.entries(controls).forEach(([name, control], ix) => {
326
+ const path = !parentPath ? name : `${parentPath}.${name}`;
327
+ if (control instanceof FormGroup$1) {
328
+ getFormValidationErrors(control.controls, path).forEach(error => errors.push(error));
329
+ return;
330
+ }
331
+ if (control instanceof FormArray$1) {
332
+ control.controls.forEach((control, ix) => {
333
+ getFormValidationErrors(control.controls, `${path}.${ix}`).forEach(error => errors.push(error));
334
+ });
335
+ return;
336
+ }
337
+ Object.entries(control.errors || {}).forEach(([errorKey, errorValue]) => {
338
+ errors.push({ control, path, errorKey, errorValue });
339
+ });
340
+ });
341
+ return errors;
342
+ }
343
+
248
344
  class DynamicFormBuilderService {
249
345
  injector;
250
346
  api;
@@ -254,60 +350,6 @@ class DynamicFormBuilderService {
254
350
  this.api = api;
255
351
  this.language = language;
256
352
  }
257
- getLabel(key, label, parent, options) {
258
- const labelPrefix = !ObjectUtils.isString(options.labelPrefix) ? `` : options.labelPrefix;
259
- const pathPrefix = `${parent?.props?.label || labelPrefix}`;
260
- const labelItems = ObjectUtils.isString(label)
261
- ? (!label ? [] : [labelPrefix, label])
262
- : [pathPrefix, `${key || ""}`];
263
- return labelItems.filter(l => l.length > 0).join(".");
264
- }
265
- createFormField(key, type, data, props, parent, options) {
266
- const validators = Array.isArray(data.validators)
267
- ? data.validators.reduce((res, validator, ix) => {
268
- res[validator.validatorName || `validator_${ix}`] = validator;
269
- return res;
270
- }, {})
271
- : data.validators || {};
272
- const hide = new BehaviorSubject(data.hidden === true);
273
- const additional = new BehaviorSubject({});
274
- const field = {
275
- key,
276
- type,
277
- validators,
278
- parent,
279
- path: !parent?.path ? key : `${parent.path}.${key}`,
280
- fieldSet: String(data.fieldSet || ""),
281
- resetOnHide: false,
282
- validation: {
283
- messages: Object.keys(validators).reduce((res, key) => {
284
- res[key] = validationMessage(this.injector, key, options.labelPrefix);
285
- return res;
286
- }, {})
287
- },
288
- props: {
289
- ...props,
290
- disabled: data.disabled === true,
291
- formCheck: "nolabel",
292
- required: !!validators.required,
293
- label: this.getLabel(key, data.label, parent, options),
294
- },
295
- modelOptions: {
296
- updateOn: "change"
297
- },
298
- fieldGroupClassName: "field-container",
299
- expressions: {
300
- hide,
301
- additional
302
- }
303
- };
304
- field.expressions.className = hide.pipe(map(hidden => {
305
- const classes = [`dynamic-form-field`, `dynamic-form-field-${field.key}`, `dynamic-form-${field.type || "group"}`].concat(Array.isArray(data.classes) ? data.classes : [data.classes || ""]);
306
- return hidden ? `` : classes.filter(c => c?.length > 0).join(" ");
307
- }));
308
- field.expressions.path = () => field.path;
309
- return field;
310
- }
311
353
  resolveFormFields(target, parent, options) {
312
354
  const prototype = target?.prototype || {};
313
355
  const fields = ReflectUtils.getMetadata("dynamicFormFields", target?.prototype || {}) || new Set();
@@ -347,7 +389,7 @@ class DynamicFormBuilderService {
347
389
  }
348
390
  // Create a field-set wrapper for each group and concat the other fields to the end
349
391
  return Object.keys(groups).map(group => {
350
- return {
392
+ const fieldSet = {
351
393
  fieldGroup: groups[group],
352
394
  wrappers: ["form-fieldset"],
353
395
  className: `dynamic-form-fieldset dynamic-form-fieldset-${group}`,
@@ -357,6 +399,8 @@ class DynamicFormBuilderService {
357
399
  hidden: false
358
400
  }
359
401
  };
402
+ this.setExpressions(fieldSet, options);
403
+ return fieldSet;
360
404
  }).concat(others);
361
405
  }
362
406
  createFormInput(key, data, parent, options) {
@@ -375,6 +419,8 @@ class DynamicFormBuilderService {
375
419
  minLength: isNaN(data.minLength) ? 0 : data.minLength,
376
420
  maxLength: isNaN(data.maxLength) ? MAX_INPUT_NUM : data.maxLength,
377
421
  placeholder: data.placeholder || "",
422
+ indeterminate: data.indeterminate || false,
423
+ suffix: data.suffix || "",
378
424
  attributes: {
379
425
  autocomplete
380
426
  },
@@ -497,6 +543,81 @@ class DynamicFormBuilderService {
497
543
  control.setValue(options[0].value);
498
544
  return options;
499
545
  }
546
+ getLabel(key, label, parent, options) {
547
+ const labelPrefix = !ObjectUtils.isString(options.labelPrefix) ? `` : options.labelPrefix;
548
+ const pathPrefix = `${parent?.props?.label || labelPrefix}`;
549
+ const labelItems = ObjectUtils.isString(label)
550
+ ? (!label ? [] : [labelPrefix, label])
551
+ : [pathPrefix, `${key || ""}`];
552
+ return options.labelCustomizer?.(key, label, parent, options.labelPrefix)
553
+ ?? labelItems.filter(l => l.length > 0).join(".");
554
+ }
555
+ createFormField(key, type, data, props, parent, options) {
556
+ const validators = Array.isArray(data.validators)
557
+ ? data.validators.reduce((res, validator, ix) => {
558
+ res[validator.validatorName || `validator_${ix}`] = validator;
559
+ return res;
560
+ }, {})
561
+ : data.validators || {};
562
+ const hide = new BehaviorSubject(data.hidden === true);
563
+ const additional = new BehaviorSubject({});
564
+ const field = {
565
+ key,
566
+ type,
567
+ validators,
568
+ parent,
569
+ fieldSet: String(data.fieldSet || ""),
570
+ resetOnHide: false,
571
+ validation: {
572
+ messages: Object.keys(validators).reduce((res, key) => {
573
+ res[key] = validationMessage(this.injector, key, options.labelPrefix);
574
+ return res;
575
+ }, {})
576
+ },
577
+ props: {
578
+ ...props,
579
+ disabled: data.disabled === true,
580
+ formCheck: "nolabel",
581
+ required: !!validators.required,
582
+ label: this.getLabel(key, data.label, parent, options),
583
+ },
584
+ modelOptions: {
585
+ updateOn: "change"
586
+ },
587
+ fieldGroupClassName: "field-container",
588
+ expressions: {
589
+ hide,
590
+ additional,
591
+ className: (target) => {
592
+ return target.hide ? `` : [`dynamic-form-field`, `dynamic-form-field-${target.key}`, `dynamic-form-${target.type || "group"}`].concat(Array.isArray(data.classes) ? data.classes : [data.classes || ""]).filter(c => c?.length > 0).join(" ");
593
+ }
594
+ }
595
+ };
596
+ this.setExpressions(field, options);
597
+ return field;
598
+ }
599
+ setExpressions(field, options) {
600
+ const expressions = {
601
+ path: target => {
602
+ const tp = target.parent;
603
+ const key = !target.key ? `` : `.${target.key}`;
604
+ return !tp?.path ? `${target.key || ""}` : `${tp.path}.${key}`;
605
+ },
606
+ testId: target => {
607
+ const tp = target.parent;
608
+ const prefix = !options.testId ? `` : `${options.testId}-`;
609
+ const key = !target.key ? `` : `-${target.key}`;
610
+ return !tp?.testId ? `${prefix}${target.key || key}` : `${tp.testId}${key}`;
611
+ }
612
+ };
613
+ Object.entries(expressions).forEach(([key, expression]) => {
614
+ field.expressions = field.expressions ?? {};
615
+ field.expressions[key] = expression;
616
+ if (ObjectUtils.isFunction(expression)) {
617
+ field[key] = expression(field);
618
+ }
619
+ });
620
+ }
500
621
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormBuilderService, deps: [{ token: i0.Injector }, { token: API_SERVICE }, { token: LANGUAGE_SERVICE }], target: i0.ɵɵFactoryTarget.Injectable });
501
622
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormBuilderService });
502
623
  }
@@ -510,28 +631,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
510
631
  args: [LANGUAGE_SERVICE]
511
632
  }] }] });
512
633
 
513
- function getFormValidationErrors(controls, parentPath = "") {
514
- const errors = [];
515
- Object.entries(controls).forEach(([name, control], ix) => {
516
- const path = !parentPath ? name : `${parentPath}.${name}`;
517
- if (control instanceof FormGroup$1) {
518
- getFormValidationErrors(control.controls, path).forEach(error => errors.push(error));
519
- return;
520
- }
521
- if (control instanceof FormArray$1) {
522
- control.controls.forEach((control, ix) => {
523
- getFormValidationErrors(control.controls, `${path}.${ix}`).forEach(error => errors.push(error));
524
- });
525
- return;
526
- }
527
- Object.entries(control.errors || {}).forEach(([errorKey, errorValue]) => {
528
- errors.push({ control, path, errorKey, errorValue });
529
- });
530
- });
531
- return errors;
532
- }
533
-
534
- class DynamicFormService {
634
+ class DynamicFormSchemaService {
535
635
  openApi;
536
636
  injector;
537
637
  builder;
@@ -541,149 +641,32 @@ class DynamicFormService {
541
641
  get language() {
542
642
  return this.api.language;
543
643
  }
544
- schemas;
545
644
  constructor(openApi, injector, builder) {
546
645
  this.openApi = openApi;
547
646
  this.injector = injector;
548
647
  this.builder = builder;
549
648
  }
550
- async validateForm(form, showErrors = true) {
551
- const group = form.group();
552
- if (!group)
553
- return Promise.resolve();
554
- return new Promise((resolve, reject) => {
555
- group.statusChanges
556
- .pipe(first(status => status == "VALID" || status == "INVALID"))
557
- .subscribe(status => {
558
- if (showErrors) {
559
- this.showErrorsForGroup(group);
560
- }
561
- if (status == "VALID") {
562
- resolve(null);
563
- return;
564
- }
565
- reject(getFormValidationErrors(group.controls));
566
- });
567
- group.updateValueAndValidity();
568
- });
569
- }
570
- async serializeForm(form, validate = true) {
571
- const fields = form.config();
572
- if (!fields)
573
- return null;
574
- if (validate) {
575
- await this.validateForm(form);
576
- }
577
- return this.serialize(fields);
649
+ async getSchema(name) {
650
+ return this.openApi.getSchema(name);
578
651
  }
579
- async serialize(fields) {
580
- const result = {};
581
- if (!fields)
582
- return result;
583
- for (const field of fields) {
584
- const serializer = field.serializer;
585
- const key = `${field.key}`;
586
- if (ObjectUtils.isFunction(serializer)) {
587
- result[key] = await serializer(field, this.injector);
588
- continue;
589
- }
590
- if (field.hide && !field.serialize) {
591
- continue;
592
- }
593
- const control = field.formControl;
594
- if (field.fieldGroup) {
595
- const group = await this.serialize(field.fieldGroup);
596
- if (field.key) {
597
- result[key] = !field.fieldArray ? group : Object.values(group);
598
- continue;
599
- }
600
- Object.assign(result, group);
601
- continue;
602
- }
603
- result[key] = control.value;
604
- }
605
- return result;
606
- }
607
- async getFormFieldsForSchema(name, customizeOrOptions) {
608
- const group = await this.getFormFieldGroupForSchema(name, customizeOrOptions);
609
- return group.fieldGroup;
610
- }
611
- async getFormFieldGroupForSchema(name, customizeOrOptions) {
612
- this.schemas = await this.openApi.getSchemas();
613
- const schemaOptions = ObjectUtils.isObject(customizeOrOptions) ? customizeOrOptions : {};
614
- const customizeConfig = ObjectUtils.isFunction(customizeOrOptions) ? customizeOrOptions : schemaOptions.customizer;
615
- const schema = this.schemas[name];
616
- const wrapOptions = {
617
- ...schemaOptions,
618
- schema,
619
- injector: this.injector,
620
- customizer: async (property, options, config) => {
621
- config.defaultValue = `${config.type}`.startsWith("date")
622
- ? this.convertToDate(property.default) : property.default;
623
- if (!ObjectUtils.isFunction(customizeConfig))
624
- return [config];
625
- let res = customizeConfig(property, schema, config, options, this.injector);
626
- if (!res)
627
- return [config];
628
- if (res instanceof Promise) {
629
- res = await res;
630
- }
631
- return Array.isArray(res) ? res : [res];
632
- }
633
- };
634
- const config = {
635
- key: FORM_ROOT_KEY,
636
- path: "",
637
- wrappers: ["form-group"]
638
- };
639
- const fields = await this.getFormFieldsForSchemaDef(schema, config, wrapOptions);
640
- const fieldGroup = [...fields];
641
- // Add id fields if necessary
642
- if (fields.length > 0) {
643
- const idFields = [
644
- { key: "id", props: { hidden: true } },
645
- { key: "_id", props: { hidden: true } },
646
- ];
647
- fieldGroup.unshift(...idFields
648
- .filter(t => !fields.some(c => c.key == t.key)));
649
- }
650
- config.fieldGroup = fieldGroup;
651
- const root = await wrapOptions.customizer({
652
- id: FORM_ROOT_KEY,
653
- type: "object",
654
- properties: schema?.properties || {}
655
- }, wrapOptions, config);
656
- // Check if the customized root wrapper returned an array
657
- fields.length = 0;
658
- for (const model of root) {
659
- if (model.key === FORM_ROOT_KEY) {
660
- return model;
661
- }
662
- else {
663
- fields.push(model);
664
- }
665
- }
666
- return {
667
- ...config,
668
- fieldGroup: fields
669
- };
670
- }
671
- async getFormFieldsForSchemaDef(schema, parent, options) {
652
+ async getFormFieldsForSchema(name, parent, customizeOrOptions) {
653
+ const schema = await this.getSchema(name);
672
654
  if (!schema)
673
655
  return [];
656
+ const options = await toWrapOptions(customizeOrOptions, this.injector, schema);
674
657
  const keys = Object.keys(schema.properties || {});
675
658
  const fields = [];
676
659
  // Collect all properties of this schema def
677
660
  for (const key of keys) {
678
661
  const property = schema.properties[key];
679
- const propFields = await this.getFormFieldsForProp(property, options, parent);
662
+ const propFields = await this.getFormFieldsForProp(property, schema, options, parent);
680
663
  fields.push(...propFields);
681
664
  }
682
665
  return this.builder.createFieldSets(fields.filter(f => null !== f), parent, options);
683
666
  }
684
- async getFormFieldsForProp(property, options, parent) {
667
+ async getFormFieldsForProp(property, schema, options, parent) {
685
668
  const field = await this.getFormFieldForProp(property, options, parent);
686
- return !field ? [] : options.customizer(property, options, field);
669
+ return !field ? [] : options.customize(field, property, schema);
687
670
  }
688
671
  async getFormFieldForProp(property, options, parent) {
689
672
  const $enum = property.items?.enum || property.enum;
@@ -737,9 +720,9 @@ class DynamicFormService {
737
720
  }
738
721
  async getFormArrayConfig(property, options, parent) {
739
722
  return this.builder.createFormArray(property.id, async (sp) => {
740
- const subSchemas = findRefs(property).map(ref => this.schemas[ref]);
723
+ const subSchemas = findRefs(property);
741
724
  if (subSchemas.length > 0) {
742
- const subModels = await Promise.all(subSchemas.map(s => this.getFormFieldsForSchemaDef(s, sp, options)));
725
+ const subModels = await Promise.all(subSchemas.map(s => this.getFormFieldsForSchema(s, sp, options)));
743
726
  return mergeFormFields(ObjectUtils.copy(subModels));
744
727
  }
745
728
  return this.getFormFieldForProp(property.items, options, null);
@@ -758,9 +741,9 @@ class DynamicFormService {
758
741
  }, parent, options);
759
742
  }
760
743
  async getFormGroupConfig(property, options, parent) {
761
- const subSchemas = findRefs(property).map(ref => this.schemas[ref]);
762
744
  return this.builder.createFormGroup(property.id, async (sp) => {
763
- const subModels = await Promise.all(subSchemas.map(s => this.getFormFieldsForSchemaDef(s, sp, options)));
745
+ const subSchemas = findRefs(property);
746
+ const subModels = await Promise.all(subSchemas.map(s => this.getFormFieldsForSchema(s, sp, options)));
764
747
  return mergeFormFields(subModels);
765
748
  }, {
766
749
  ...this.getFormFieldData(property, options),
@@ -793,7 +776,9 @@ class DynamicFormService {
793
776
  max: sub.maximum,
794
777
  minLength: sub.minLength,
795
778
  maxLength: sub.maxLength,
796
- placeholder: property.placeholder
779
+ placeholder: property.placeholder,
780
+ indeterminate: property.indeterminate,
781
+ suffix: property.suffix
797
782
  }, parent, options);
798
783
  }
799
784
  getFormTextareaConfig(property, options, parent) {
@@ -808,7 +793,7 @@ class DynamicFormService {
808
793
  placeholder: property.placeholder || ""
809
794
  }, parent, options);
810
795
  }
811
- // getFormEditorConfig(property: IOpenApiSchemaProperty, options: ConfigForSchemaWrapOptions): DynamicEditorModelConfig {
796
+ // getFormEditorConfig(property: IOpenApiSchemaProperty, options: ConfigForSchemaWrap): DynamicEditorModelConfig {
812
797
  // const sub = property.type == "array" ? property.items || property : property;
813
798
  // return Object.assign(
814
799
  // this.getFormControlConfig(property, options),
@@ -832,8 +817,8 @@ class DynamicFormService {
832
817
  ...this.getFormFieldData(property, options),
833
818
  type: property.format == "date-time" ? "datetime-local" : "date",
834
819
  // format: property.dateFormat || "dd.MM.yyyy",
835
- min: this.convertToDate(property.min),
836
- max: this.convertToDate(property.max),
820
+ min: convertToDate(property.min),
821
+ max: convertToDate(property.max),
837
822
  }, parent, options);
838
823
  }
839
824
  getFormSelectConfig(property, options, parent) {
@@ -956,14 +941,6 @@ class DynamicFormService {
956
941
  }
957
942
  });
958
943
  }
959
- convertToDate(value) {
960
- if (ObjectUtils.isNullOrUndefined(value))
961
- return null;
962
- const date = ObjectUtils.isDate(value)
963
- ? value
964
- : new Date(value);
965
- return isNaN(date) ? new Date() : date;
966
- }
967
944
  addPropertyValidators(validators, property) {
968
945
  if (!property)
969
946
  return;
@@ -1004,12 +981,155 @@ class DynamicFormService {
1004
981
  validators.itemsMaxValue = maxValueValidation(items.maximum, true);
1005
982
  }
1006
983
  }
1007
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormService, deps: [{ token: i2.OpenApiService }, { token: i0.Injector }, { token: DynamicFormBuilderService }], target: i0.ɵɵFactoryTarget.Injectable });
984
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormSchemaService, deps: [{ token: i2.OpenApiService }, { token: i0.Injector }, { token: DynamicFormBuilderService }], target: i0.ɵɵFactoryTarget.Injectable });
985
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormSchemaService });
986
+ }
987
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormSchemaService, decorators: [{
988
+ type: Injectable
989
+ }], ctorParameters: () => [{ type: i2.OpenApiService }, { type: i0.Injector }, { type: DynamicFormBuilderService }] });
990
+
991
+ class DynamicFormService {
992
+ fs;
993
+ injector;
994
+ api;
995
+ constructor(fs, injector, api) {
996
+ this.fs = fs;
997
+ this.injector = injector;
998
+ this.api = api;
999
+ }
1000
+ async getFormFieldsForSchema(name, customizeOrOptions) {
1001
+ const group = await this.getFormFieldGroupBySchemaName(name, customizeOrOptions, "getFormFieldsForSchema");
1002
+ return group.fieldGroup;
1003
+ }
1004
+ async getFormFieldGroupForSchema(name, customizeOrOptions) {
1005
+ return this.getFormFieldGroupBySchemaName(name, customizeOrOptions, "getFormFieldsForSchema");
1006
+ }
1007
+ async getFormFieldGroupBySchemaName(name, customizeOrOptions, restrictedMethod) {
1008
+ const config = {
1009
+ key: FORM_ROOT_KEY,
1010
+ path: "",
1011
+ wrappers: ["form-group"]
1012
+ };
1013
+ const schema = await this.fs.getSchema(name);
1014
+ const wrapOptions = await toWrapOptions(customizeOrOptions, this.injector, schema, `"DynamicFormService.${restrictedMethod}" is called from a customizer, which is not allowed. Please use DynamicFormSchemaService instead!`);
1015
+ const fields = await this.fs.getFormFieldsForSchema(name, config, wrapOptions);
1016
+ const fieldGroup = [...fields];
1017
+ config.fieldGroup = fieldGroup;
1018
+ // There are no actual fields in the schema, or no schema exists
1019
+ if (fields.length === 0)
1020
+ return config;
1021
+ // Add id fields if necessary
1022
+ const idFields = [
1023
+ { key: "id", props: { hidden: true } },
1024
+ { key: "_id", props: { hidden: true } },
1025
+ ];
1026
+ fieldGroup.unshift(...idFields
1027
+ .filter(t => !fields.some(c => c.key == t.key)));
1028
+ const root = await wrapOptions.customize(config, {
1029
+ id: FORM_ROOT_KEY,
1030
+ type: "object",
1031
+ properties: schema?.properties || {}
1032
+ }, schema);
1033
+ // Check if the customized root wrapper returned an array
1034
+ fields.length = 0;
1035
+ for (const model of root) {
1036
+ if (model.key === FORM_ROOT_KEY) {
1037
+ return model;
1038
+ }
1039
+ else {
1040
+ fields.push(model);
1041
+ }
1042
+ }
1043
+ return {
1044
+ ...config,
1045
+ fieldGroup: fields
1046
+ };
1047
+ }
1048
+ async validateForm(form, showErrors = true) {
1049
+ const group = form.group();
1050
+ if (!group)
1051
+ return Promise.resolve();
1052
+ return new Promise((resolve, reject) => {
1053
+ group.statusChanges
1054
+ .pipe(first(status => status == "VALID" || status == "INVALID"))
1055
+ .subscribe(status => {
1056
+ if (showErrors) {
1057
+ this.showErrorsForGroup(group);
1058
+ }
1059
+ if (status == "VALID") {
1060
+ resolve(null);
1061
+ return;
1062
+ }
1063
+ reject(getFormValidationErrors(group.controls));
1064
+ });
1065
+ group.updateValueAndValidity();
1066
+ });
1067
+ }
1068
+ async serializeForm(form, validate = true) {
1069
+ const fields = form.config();
1070
+ if (!fields)
1071
+ return null;
1072
+ if (validate) {
1073
+ await this.validateForm(form);
1074
+ }
1075
+ return this.serialize(fields);
1076
+ }
1077
+ async serialize(fields) {
1078
+ const result = {};
1079
+ if (!fields)
1080
+ return result;
1081
+ for (const field of fields) {
1082
+ const serializer = field.serializer;
1083
+ const key = `${field.key}`;
1084
+ if (ObjectUtils.isFunction(serializer)) {
1085
+ result[key] = await serializer(field, this.injector);
1086
+ continue;
1087
+ }
1088
+ if (field.hide && !field.serialize) {
1089
+ continue;
1090
+ }
1091
+ const control = field.formControl;
1092
+ if (field.fieldGroup) {
1093
+ const group = await this.serialize(field.fieldGroup);
1094
+ if (field.key) {
1095
+ result[key] = !field.fieldArray ? group : Object.values(group);
1096
+ continue;
1097
+ }
1098
+ Object.assign(result, group);
1099
+ continue;
1100
+ }
1101
+ result[key] = control.value;
1102
+ }
1103
+ return result;
1104
+ }
1105
+ showErrorsForGroup(formGroup) {
1106
+ if (!formGroup)
1107
+ return;
1108
+ formGroup.markAsTouched({ onlySelf: true });
1109
+ const controls = Object.keys(formGroup.controls).map(id => formGroup.controls[id]);
1110
+ this.showErrorsForControls(controls);
1111
+ }
1112
+ showErrorsForControls(controls) {
1113
+ controls.forEach(control => {
1114
+ if (control instanceof FormGroup$1) {
1115
+ this.showErrorsForGroup(control);
1116
+ return;
1117
+ }
1118
+ control.markAsTouched({ onlySelf: true });
1119
+ if (control instanceof FormArray$1) {
1120
+ this.showErrorsForControls(control.controls);
1121
+ }
1122
+ });
1123
+ }
1124
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormService, deps: [{ token: DynamicFormSchemaService }, { token: i0.Injector }, { token: API_SERVICE }], target: i0.ɵɵFactoryTarget.Injectable });
1008
1125
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormService });
1009
1126
  }
1010
1127
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormService, decorators: [{
1011
1128
  type: Injectable
1012
- }], ctorParameters: () => [{ type: i2.OpenApiService }, { type: i0.Injector }, { type: DynamicFormBuilderService }] });
1129
+ }], ctorParameters: () => [{ type: DynamicFormSchemaService }, { type: i0.Injector }, { type: undefined, decorators: [{
1130
+ type: Inject,
1131
+ args: [API_SERVICE]
1132
+ }] }] });
1013
1133
 
1014
1134
  class AsyncSubmitDirective {
1015
1135
  method = input(null, { alias: "async-submit" });
@@ -1113,6 +1233,8 @@ class DynamicFormComponent {
1113
1233
  events = inject(EventsService);
1114
1234
  languages = inject(LANGUAGE_SERVICE);
1115
1235
  labelPrefix = input("label");
1236
+ labelCustomizer = input(null);
1237
+ testId = input("");
1116
1238
  data = input({});
1117
1239
  fields = input(null);
1118
1240
  fieldChanges = new Subject();
@@ -1121,7 +1243,9 @@ class DynamicFormComponent {
1121
1243
  });
1122
1244
  config = computed(() => {
1123
1245
  return this.fields() || this.builder.resolveFormFields(this.data()?.constructor, null, {
1124
- labelPrefix: this.labelPrefix()
1246
+ labelPrefix: this.labelPrefix(),
1247
+ labelCustomizer: this.labelCustomizer(),
1248
+ testId: this.testId(),
1125
1249
  });
1126
1250
  });
1127
1251
  group = computed(() => {
@@ -1159,7 +1283,7 @@ class DynamicFormComponent {
1159
1283
  this.options?.resetModel?.();
1160
1284
  }
1161
1285
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1162
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: DynamicFormComponent, isStandalone: false, selector: "dynamic-form", inputs: { labelPrefix: { classPropertyName: "labelPrefix", publicName: "labelPrefix", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSubmit: "onSubmit" }, ngImport: i0, template: "@if (config() && group()) {\n <form [formGroup]=\"group()\" (ngSubmit)=\"submit()\" autocomplete=\"off\" role=\"presentation\">\n <input type=\"submit\" [hidden]=\"true\" />\n <formly-form [model]=\"data()\"\n [fields]=\"config()\"\n [form]=\"group()\"\n [options]=\"options\"></formly-form>\n <ng-content></ng-content>\n </form>\n}\n", dependencies: [{ kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: i3.FormlyForm, selector: "formly-form", inputs: ["form", "model", "fields", "options"], outputs: ["modelChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1286
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: DynamicFormComponent, isStandalone: false, selector: "dynamic-form", inputs: { labelPrefix: { classPropertyName: "labelPrefix", publicName: "labelPrefix", isSignal: true, isRequired: false, transformFunction: null }, labelCustomizer: { classPropertyName: "labelCustomizer", publicName: "labelCustomizer", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSubmit: "onSubmit" }, ngImport: i0, template: "@if (config() && group()) {\n <form [formGroup]=\"group()\" (ngSubmit)=\"submit()\" autocomplete=\"off\" role=\"presentation\">\n <input type=\"submit\" [hidden]=\"true\" />\n <formly-form [model]=\"data()\"\n [fields]=\"config()\"\n [form]=\"group()\"\n [options]=\"options\"></formly-form>\n <ng-content></ng-content>\n </form>\n}\n", dependencies: [{ kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: i3.FormlyForm, selector: "formly-form", inputs: ["form", "model", "fields", "options"], outputs: ["modelChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1163
1287
  }
1164
1288
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormComponent, decorators: [{
1165
1289
  type: Component,
@@ -1193,11 +1317,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
1193
1317
 
1194
1318
  class DynamicFormFieldComponent extends FieldWrapper {
1195
1319
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormFieldComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1196
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: DynamicFormFieldComponent, isStandalone: false, selector: "dynamic-form-field", usesInheritance: true, ngImport: i0, template: "<label class=\"field-label\" *ngIf=\"props.label\">\n {{ props.label | translate }}\n <p class=\"field-description\" *ngIf=\"props.description\">{{ props.description | translate }}</p>\n <span class=\"field-required\" *ngIf=\"props.required && props.hideRequiredMarker !== true\" aria-hidden=\"true\">*</span>\n</label>\n<div class=\"field-container\">\n <ng-container #fieldComponent></ng-container>\n <div *ngIf=\"showError\" class=\"field-errors invalid-feedback\">\n <formly-validation-message\n id=\"{{ id }}-formly-validation-error\"\n [field]=\"field\"\n role=\"alert\"\n ></formly-validation-message>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.ɵFormlyValidationMessage, selector: "formly-validation-message", inputs: ["field"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], encapsulation: i0.ViewEncapsulation.None });
1320
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: DynamicFormFieldComponent, isStandalone: false, selector: "dynamic-form-field", usesInheritance: true, ngImport: i0, template: "<label class=\"field-label\" [for]=\"id\" *ngIf=\"props.label\">\n {{ props.label | translate }}\n <p class=\"field-description\" *ngIf=\"props.description\">{{ props.description | translate }}</p>\n <span class=\"field-required\" *ngIf=\"props.required && props.hideRequiredMarker !== true\" aria-hidden=\"true\">*</span>\n</label>\n<div class=\"field-container\">\n <ng-container #fieldComponent></ng-container>\n <div *ngIf=\"showError\" class=\"field-errors invalid-feedback\">\n <formly-validation-message\n id=\"{{ id }}-formly-validation-error\"\n [field]=\"field\"\n role=\"alert\"\n ></formly-validation-message>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3.ɵFormlyValidationMessage, selector: "formly-validation-message", inputs: ["field"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], encapsulation: i0.ViewEncapsulation.None });
1197
1321
  }
1198
1322
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicFormFieldComponent, decorators: [{
1199
1323
  type: Component,
1200
- args: [{ standalone: false, selector: "dynamic-form-field", encapsulation: ViewEncapsulation.None, template: "<label class=\"field-label\" *ngIf=\"props.label\">\n {{ props.label | translate }}\n <p class=\"field-description\" *ngIf=\"props.description\">{{ props.description | translate }}</p>\n <span class=\"field-required\" *ngIf=\"props.required && props.hideRequiredMarker !== true\" aria-hidden=\"true\">*</span>\n</label>\n<div class=\"field-container\">\n <ng-container #fieldComponent></ng-container>\n <div *ngIf=\"showError\" class=\"field-errors invalid-feedback\">\n <formly-validation-message\n id=\"{{ id }}-formly-validation-error\"\n [field]=\"field\"\n role=\"alert\"\n ></formly-validation-message>\n </div>\n</div>\n" }]
1324
+ args: [{ standalone: false, selector: "dynamic-form-field", encapsulation: ViewEncapsulation.None, template: "<label class=\"field-label\" [for]=\"id\" *ngIf=\"props.label\">\n {{ props.label | translate }}\n <p class=\"field-description\" *ngIf=\"props.description\">{{ props.description | translate }}</p>\n <span class=\"field-required\" *ngIf=\"props.required && props.hideRequiredMarker !== true\" aria-hidden=\"true\">*</span>\n</label>\n<div class=\"field-container\">\n <ng-container #fieldComponent></ng-container>\n <div *ngIf=\"showError\" class=\"field-errors invalid-feedback\">\n <formly-validation-message\n id=\"{{ id }}-formly-validation-error\"\n [field]=\"field\"\n role=\"alert\"\n ></formly-validation-message>\n </div>\n</div>\n" }]
1201
1325
  }] });
1202
1326
 
1203
1327
  class DynamicFormFieldsetComponent extends FieldWrapper {
@@ -1273,7 +1397,8 @@ class NgxDynamicFormModule {
1273
1397
  return [
1274
1398
  ...providers,
1275
1399
  DynamicFormService,
1276
- DynamicFormBuilderService
1400
+ DynamicFormBuilderService,
1401
+ DynamicFormSchemaService
1277
1402
  ];
1278
1403
  }
1279
1404
  static forRoot(config) {
@@ -1339,5 +1464,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
1339
1464
  * Generated bundle index. Do not edit.
1340
1465
  */
1341
1466
 
1342
- export { AsyncSubmitDirective, DynamicFormArrayComponent, DynamicFormBuilderService, DynamicFormChipsComponent, DynamicFormComponent, DynamicFormFieldComponent, DynamicFormFieldsetComponent, DynamicFormGroupComponent, DynamicFormService, DynamicFormUploadComponent, EDITOR_FORMATS, FORM_ROOT_KEY, FormFile, FormGroup, FormInput, FormModel, FormSelect, FormSerializable, FormUpload, MAX_INPUT_NUM, MIN_INPUT_NUM, NgxDynamicFormModule, additionalFieldValues, customizeFormField, emailValidation, getFieldByPath, jsonValidation, maxLengthValidation, maxValueValidation, mergeFormFields, minLengthValidation, minValueValidation, phoneValidation, replaceSpecialChars, requiredValidation, setFieldDisabled, setFieldHidden, translationValidation, validationMessage };
1467
+ export { AsyncSubmitDirective, DynamicFormArrayComponent, DynamicFormBuilderService, DynamicFormChipsComponent, DynamicFormComponent, DynamicFormFieldComponent, DynamicFormFieldsetComponent, DynamicFormGroupComponent, DynamicFormSchemaService, DynamicFormService, DynamicFormUploadComponent, EDITOR_FORMATS, FORM_ROOT_KEY, FormFile, FormGroup, FormInput, FormModel, FormSelect, FormSerializable, FormUpload, MAX_INPUT_NUM, MIN_INPUT_NUM, NgxDynamicFormModule, additionalFieldValues, customizeFormField, emailValidation, getFieldByPath, getFieldsByKey, jsonValidation, maxLengthValidation, maxValueValidation, minLengthValidation, minValueValidation, phoneValidation, replaceSpecialChars, requiredValidation, setFieldDisabled, setFieldHidden, translationValidation, validationMessage };
1343
1468
  //# sourceMappingURL=stemy-ngx-dynamic-form.mjs.map