UNPKG

@fluent-form/core

Version:

An Angular dynamic forms library powered by Fluent API and JSON.

839 lines (819 loc) 110 kB
import * as i0 from '@angular/core'; import { inject, DestroyRef, Injectable, InjectionToken, EnvironmentInjector, createComponent, input, ElementRef, effect, untracked, isSignal, Directive, Input, Injector, ViewEncapsulation, Component, computed, NgModule, model, output, forwardRef, HostListener, ViewContainerRef, TemplateRef, ViewChild, ChangeDetectionStrategy, Pipe, runInInjectionContext, makeEnvironmentProviders } from '@angular/core'; import { signalSetFn, SIGNAL } from '@angular/core/primitives/signals'; import { outputToObservable, toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Validators, FormControl, FormGroup, FormArray } from '@angular/forms'; import { Subject, takeUntil, fromEvent, Observable, map } from 'rxjs'; import * as i1 from '@angular/common'; import { NgTemplateOutlet } from '@angular/common'; import { BreakpointObserver } from '@angular/cdk/layout'; class DestroyedSubject extends Subject { constructor() { super(); inject(DestroyRef).onDestroy(() => { this.next(); this.complete(); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DestroyedSubject, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DestroyedSubject }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DestroyedSubject, decorators: [{ type: Injectable }], ctorParameters: () => [] }); const RETURN_STR = 'return '; const STATIC_EXPRESSION_PATTERN = /^{{.+}}$/; const INTERPOLATION_PATTERN = /^{{|}}$/g; class CodeEvaluator { } function isStaticExpression(str) { return STATIC_EXPRESSION_PATTERN.test(str); } class DynamicCodeEvaluator { evaluate(code, context) { code = RETURN_STR + code.replace(INTERPOLATION_PATTERN, ''); // TODO: This doesn't work with `unsafe-eval` enabled in CSP. const fn = new Function('o', `with(o){${code}}`); const proxy = new Proxy(Object.freeze(context), { // Intercept all properties to prevent lookup beyond the `Proxy` object's scope chain. has() { return true; }, get(target, key, receiver) { if (key === Symbol.unscopables) { return undefined; } return Reflect.get(target, key, receiver); } }); return fn(proxy); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicCodeEvaluator, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicCodeEvaluator, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DynamicCodeEvaluator, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const isObject = (o) => typeof o === 'object'; const isNumber = (o) => typeof o === 'number'; const isString = (o) => typeof o === 'string'; const isFunction = (o) => typeof o === 'function'; const isBoolean = (o) => typeof o === 'boolean'; const isUndefined = (o) => o === undefined; const isArray = (o) => Array.isArray(o); /** * @internal */ class ValueTransformer { constructor() { this.evaluator = inject(CodeEvaluator, { optional: true }); } transform(value, context = {}) { if (isFunction(value)) { return value(context); } if (isString(value) && this.evaluator && isStaticExpression(value)) { return this.evaluator.evaluate(value, context); } return value; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ValueTransformer, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ValueTransformer, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ValueTransformer, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); function throwWidgetNotFoundError(name) { throw new Error(`The '${name}' widget was not found`); } function throwCustomTemplateNotFoundError(name) { throw new Error(`The custom '${name}' template was not found`); } const WIDGET_MAP = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'WIDGET_MAP' : ''); const SCHEMA_MAP = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'SCHEMA_MAP' : ''); const NAMED_TEMPLATES = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'NAMED_TEMPLATES' : ''); const FLUENT_FORM_CONTENT = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FLUENT_FORM_CONTENT' : ''); const FLUENT_FORM_FIELD_CONTENT = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FLUENT_FORM_FIELD_CONTENT' : ''); class WidgetTemplateRegistry extends Map { constructor() { super(...arguments); this.envInjector = inject(EnvironmentInjector); this.map = inject(WIDGET_MAP); } get(kind) { return super.get(kind) ?? this.register(kind); } register(kind) { const component = this.map.get(kind); if (typeof ngDevMode !== 'undefined' && ngDevMode && !component) { throwWidgetNotFoundError(kind); } const { instance } = createComponent(component, { environmentInjector: this.envInjector }); this.set(kind, instance.templateRef); return instance.templateRef; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetTemplateRegistry, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetTemplateRegistry, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetTemplateRegistry, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); function isHookHolder(value) { return 'hooks' in value; } function isListenerHolder(value) { return 'listeners' in value; } function isPropertyHolder(value) { return 'properties' in value; } function isObserverHolder(value) { return 'observers' in value; } /** * @internal */ class FluentBindingDirective { constructor() { this.destroyRef = inject(DestroyRef); this.fluentBindingComponent = input(); this.fluentBindingSchema = input.required(); this.fluentBindingControl = input.required(); this.fluentBindingModel = input.required(); const elementRef = inject(ElementRef); const destroyed = inject(DestroyedSubject); effect(() => { const component = this.fluentBindingComponent(); const schema = this.fluentBindingSchema(); const control = this.fluentBindingControl(); const host = component ?? elementRef.nativeElement; untracked(() => { const context = () => ({ control, schema, model: this.fluentBindingModel() }); destroyed.next(); if (isPropertyHolder(schema)) { for (const [property, value] of Object.entries(schema.properties)) { const prop = host[property]; if (isSignal(prop)) { signalSetFn(prop[SIGNAL], value); } else { host[property] = value; } } } if (isListenerHolder(schema)) { for (const [eventName, listener] of Object.entries(schema.listeners)) { if (eventName === 'valueChange') { control.valueChanges.pipe(takeUntil(destroyed)).subscribe(value => { listener(value, context()); }); } else if (eventName === 'statusChange') { control.statusChanges.pipe(takeUntil(destroyed)).subscribe(status => { listener(status, context()); }); } else if (host instanceof HTMLElement) { fromEvent(host, eventName).pipe(takeUntil(destroyed)).subscribe(event => { listener(event, context()); }); } else { const output = host[eventName]; const observable = output instanceof Observable ? output : outputToObservable(output); observable.pipe(takeUntil(destroyed)).subscribe(event => { listener(event, context()); }); } } } if (isObserverHolder(schema)) { for (const [eventName, observer] of Object.entries(schema.observers)) { if (eventName === 'valueChange') { control.valueChanges.pipe(map(event => ({ event, context: context() })), observer, takeUntil(destroyed)).subscribe(); } else if (eventName === 'statusChange') { control.statusChanges.pipe(map(event => ({ event, context: context() })), observer, takeUntil(destroyed)).subscribe(); } else if (host instanceof HTMLElement) { fromEvent(host, eventName).pipe(map(event => ({ event, context: context() })), observer, takeUntil(destroyed)).subscribe(); } else { const output = host[eventName]; const observable = output instanceof Observable ? output : outputToObservable(output); observable.pipe(map(event => ({ event, context: context() })), observer, takeUntil(destroyed)).subscribe(); } } } }); }); } ngOnInit() { const schema = this.fluentBindingSchema(); const control = this.fluentBindingControl(); const model = this.fluentBindingModel(); const context = { control, schema, model }; if (isHookHolder(schema)) { schema.hooks.onInit?.(context); this.destroyRef.onDestroy(() => schema.hooks.onDestroy?.(context)); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentBindingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.14", type: FluentBindingDirective, isStandalone: true, selector: "[fluentBindingSchema]", inputs: { fluentBindingComponent: { classPropertyName: "fluentBindingComponent", publicName: "fluentBindingComponent", isSignal: true, isRequired: false, transformFunction: null }, fluentBindingSchema: { classPropertyName: "fluentBindingSchema", publicName: "fluentBindingSchema", isSignal: true, isRequired: true, transformFunction: null }, fluentBindingControl: { classPropertyName: "fluentBindingControl", publicName: "fluentBindingControl", isSignal: true, isRequired: true, transformFunction: null }, fluentBindingModel: { classPropertyName: "fluentBindingModel", publicName: "fluentBindingModel", isSignal: true, isRequired: true, transformFunction: null } }, providers: [DestroyedSubject], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentBindingDirective, decorators: [{ type: Directive, args: [{ selector: '[fluentBindingSchema]', providers: [DestroyedSubject] }] }], ctorParameters: () => [] }); /** * @internal */ class FluentContextGuardDirective { /** * @internal */ static ngTemplateContextGuard(_, context) { return true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentContextGuardDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FluentContextGuardDirective, isStandalone: true, selector: "ng-template[fluentContextGuard]", inputs: { fluentContextGuard: "fluentContextGuard" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentContextGuardDirective, decorators: [{ type: Directive, args: [{ selector: 'ng-template[fluentContextGuard]' }] }], propDecorators: { fluentContextGuard: [{ type: Input }] } }); class FluentControlWrapperDirective { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentControlWrapperDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FluentControlWrapperDirective, isStandalone: true, selector: "[fluentControlWrapper]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentControlWrapperDirective, decorators: [{ type: Directive, args: [{ selector: '[fluentControlWrapper]' }] }] }); class FluentFormFieldOutletDirective { constructor() { const outlet = inject(NgTemplateOutlet); const injector = inject(Injector); const { templateRef } = createComponent(inject(FLUENT_FORM_FIELD_CONTENT), { environmentInjector: inject(EnvironmentInjector), elementInjector: injector }).instance; outlet.ngTemplateOutlet = templateRef; outlet.ngTemplateOutletInjector = Injector.create({ providers: [], parent: injector }); outlet.ngOnChanges({ ngTemplateOutlet: {} }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentFormFieldOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: FluentFormFieldOutletDirective, isStandalone: true, selector: "[fluentFormFieldOutlet]", hostDirectives: [{ directive: i1.NgTemplateOutlet, inputs: ["ngTemplateOutletContext", "fluentFormFieldOutlet"] }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentFormFieldOutletDirective, decorators: [{ type: Directive, args: [{ selector: '[fluentFormFieldOutlet]', hostDirectives: [ { directive: NgTemplateOutlet, inputs: ['ngTemplateOutletContext: fluentFormFieldOutlet'] } ] }] }], ctorParameters: () => [] }); const Breakpoints = { xs: '(max-width:575px)', sm: '(min-width:576px)', md: '(min-width:768px)', lg: '(min-width:992px)', xl: '(min-width:1200px)', xxl: '(min-width:1400px)' }; function createBreakpointInfix(breakpoint) { return breakpoint !== 'xs' ? `-${breakpoint}` : ''; } function withStyle(component) { const environmentInjector = inject(EnvironmentInjector); const destroyRef = inject(DestroyRef); const componentRef = createComponent(component, { environmentInjector }); destroyRef.onDestroy(() => componentRef.destroy()); } const SCHEMA_PATCHERS = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'SchemaPatchers' : ''); function provideSchemaPatcher(patcher) { return { provide: SCHEMA_PATCHERS, useValue: patcher, multi: true }; } /** * @public */ var SchemaType; (function (SchemaType) { SchemaType[SchemaType["Control"] = 0] = "Control"; SchemaType[SchemaType["ControlGroup"] = 1] = "ControlGroup"; SchemaType[SchemaType["ControlArray"] = 2] = "ControlArray"; SchemaType[SchemaType["ControlWrapper"] = 3] = "ControlWrapper"; SchemaType[SchemaType["Component"] = 4] = "Component"; SchemaType[SchemaType["ComponentContainer"] = 5] = "ComponentContainer"; SchemaType[SchemaType["ComponentWrapper"] = 6] = "ComponentWrapper"; })(SchemaType || (SchemaType = {})); var SchemaKind; (function (SchemaKind) { SchemaKind["Headless"] = "headless"; SchemaKind["Headful"] = "headful"; SchemaKind["Template"] = "template"; SchemaKind["Row"] = "row"; })(SchemaKind || (SchemaKind = {})); const ANY_SCHEMA_SELECTOR = '*'; class SchemaUtil { constructor() { this.schemaMap = inject(SCHEMA_MAP); this.schemaPatchers = inject(SCHEMA_PATCHERS, { optional: true }) ?? []; } patch(schema) { if (this.isControlWrapper(schema) || this.isComponentWrapper(schema) || this.isControlContainer(schema) || this.isComponentContainer(schema)) { schema.schemas = schema.schemas.map(schema => this.patch(schema)); } const config = this.schemaMap.get(schema.kind); if (typeof ngDevMode !== 'undefined' && ngDevMode && !config) { throwWidgetNotFoundError(schema.kind); } const { type } = config; return this.schemaPatchers .filter(patcher => { // Convert selector to an array for unified processing. const selector = isArray(patcher.selector) ? patcher.selector : [patcher.selector]; return selector.some(kindOrType => { if (isString(kindOrType)) { return kindOrType === ANY_SCHEMA_SELECTOR || kindOrType === schema.kind; } return kindOrType === type; }); }) .reduce((patchedSchema, patcher) => { return patcher.patch(patchedSchema); }, schema); } /** * Filter out top-level control/control container schemas. */ filterControls(schemas) { return schemas.reduce((schemas, schema) => { if (this.isControlWrapper(schema) || this.isComponentContainer(schema)) { schemas = schemas.concat(this.filterControls(schema.schemas)); } else if (this.isControl(schema) || this.isControlContainer(schema)) { schemas.push(schema); } return schemas; }, []); } isControlGroup(schema) { return this.typeOf(schema) === SchemaType.ControlGroup; } isControlArray(schema) { return this.typeOf(schema) === SchemaType.ControlArray; } isControlContainer(schema) { return this.isControlGroup(schema) || this.isControlArray(schema); } isControlWrapper(schema) { return this.typeOf(schema) === SchemaType.ControlWrapper; } isControl(schema) { return this.typeOf(schema) === SchemaType.Control; } isComponentContainer(schema) { return this.typeOf(schema) === SchemaType.ComponentContainer; } isComponentWrapper(schema) { return this.typeOf(schema) === SchemaType.ComponentWrapper; } isComponent(schema) { return this.typeOf(schema) === SchemaType.Component; } /** * Whether it is a multi-field schema. */ isMultiKeySchema(schema) { return isArray(schema.key); } /** * Whether it is a path field schema. */ isPathKeySchema(schema) { return isString(schema.key) && schema.key.includes('.'); } /** * Non-control schema, indicating that it or its child nodes * do not contain any control schemas. */ isNonControl(schema) { return this.isComponent(schema) || this.isComponentWrapper(schema); } typeOf(schema) { return this.schemaMap.get(schema.kind)?.type; } validatorsOf(schema) { const validators = this.schemaMap.get(schema.kind)?.validators?.(schema) ?? []; if (schema.required === true) { validators.push(Validators.required); } return validators; } parsePathKey(key) { return key.split('.'); } norimalizePaths(paths) { return (isArray(paths) ? paths : [paths]).map(o => o.toString()); } find(schema, paths) { let currentSchema = schema; for (const path of this.norimalizePaths(paths)) { currentSchema = currentSchema['schemas'].find((o) => o.key?.toString() === path) ?? null; if (currentSchema === null) { return currentSchema; } } return currentSchema; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SchemaUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SchemaUtil, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SchemaUtil, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * @internal */ class ValueUtil { constructor() { this.schemaUtil = inject(SchemaUtil); } valueOfModel(model, schema) { let value; // If the value read from the model is undefined, it means the model did // not provide a value, so take the default value from the schema here. // If it is a multi-key schema, the values of these keys need to be retrieved // separately from the model and combined into an array. if (this.schemaUtil.isMultiKeySchema(schema)) { value = schema.key.map((key, index) => { const val = model[key]; return isUndefined(val) ? schema.defaultValue?.[index] ?? null : val; }); // If all elements in the array are `null`, assign `null` directly. if (value.every(o => o === null)) { value = null; } } else if (this.schemaUtil.isPathKeySchema(schema)) { const paths = this.schemaUtil.parsePathKey(schema.key); value = paths.reduce((obj, key) => obj?.[key], model); } else { value = model[schema.key]; } if (isUndefined(value)) { value = schema.defaultValue ?? null; } if (schema.mapper) { return schema.mapper.parser(value, schema); } return value; } valueOfControl(control, schema) { const value = control.value; if (schema.mapper) { return schema.mapper.formatter(value, schema); } return value; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ValueUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ValueUtil, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ValueUtil, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class FormUtil { constructor() { this.schemaUtil = inject(SchemaUtil); this.valueUtil = inject(ValueUtil); this.valueTransformer = inject(ValueTransformer); } createFormControl(schema, model) { const validators = this.schemaUtil.validatorsOf(schema); const value = this.valueUtil.valueOfModel(model, schema) ?? schema.defaultValue; return new FormControl(value, { nonNullable: !isUndefined(schema.defaultValue), validators: schema.validators ? validators.concat(schema.validators) : validators, asyncValidators: schema.asyncValidators, updateOn: schema.config?.updateOn }); } createFormGroup(schemaOrSchemas, model) { let schemas; let options = {}; if (isArray(schemaOrSchemas)) { schemas = schemaOrSchemas; } else { schemas = schemaOrSchemas.schemas; options = { validators: schemaOrSchemas.validators, asyncValidators: schemaOrSchemas.asyncValidators, updateOn: schemaOrSchemas.config?.updateOn }; } return new FormGroup(this.createControlMap(schemas, model), options); } createControlMap(schemas, model) { return schemas.reduce((controls, schema) => { if (this.schemaUtil.isControlGroup(schema)) { const key = schema.key.toString(); controls[key] = this.createFormGroup(schema, model[key] ?? {}); } else if (this.schemaUtil.isControlArray(schema)) { const key = schema.key.toString(); controls[key] = this.createFormArray(schema, model[key] ?? []); } else if (this.schemaUtil.isControlWrapper(schema) || this.schemaUtil.isComponentContainer(schema)) { Object.assign(controls, this.createControlMap(schema.schemas, model)); } else if (this.schemaUtil.isControl(schema)) { if (this.schemaUtil.isPathKeySchema(schema)) { const paths = this.schemaUtil.parsePathKey(schema.key); const key = paths.pop(); const parent = paths.reduce((previousGroup, path) => { let group = previousGroup.get(path); if (!group) { group = new FormGroup({}); previousGroup.addControl(path, group); } return group; }, (controls[paths.shift()] ??= new FormGroup({}))); parent.addControl(key, this.createFormControl(schema, model)); } else { controls[schema.key.toString()] = this.createFormControl(schema, model); } } return controls; }, {}); } createFormArray(schema, model) { const controls = this.createFormArrayElements(schema.schemas, model); const validators = schema.validators ?? []; if (schema.length) { validators.push(Validators.required); if (isNumber(schema.length)) { validators.push(Validators.minLength(schema.length), Validators.maxLength(schema.length)); } else { if (schema.length.min) { validators.push(Validators.minLength(schema.length.min)); } if (schema.length.max) { validators.push(Validators.maxLength(schema.length.max)); } } } return new FormArray(controls, { validators, asyncValidators: schema.asyncValidators, updateOn: schema.config?.updateOn }); } createFormArrayElements(schemas, model) { // Only take the first one, ignore the rest. const [schema] = this.schemaUtil.filterControls(schemas); if (typeof ngDevMode !== 'undefined' && ngDevMode && !schema) { throw Error('FormArray element control schema not found'); } return model.map((_, index) => { return this.createAnyControl({ ...schema, key: index }, model); }); } createAnyControl(schema, model) { if (this.schemaUtil.isControlGroup(schema)) { return this.createFormGroup(schema, model[schema.key] ?? {}); } if (this.schemaUtil.isControlArray(schema)) { return this.createFormArray(schema, model[schema.key] ?? []); } return this.createFormControl(schema, model); } updateForm(form, model, schemas) { for (const schema of schemas) { if (this.schemaUtil.isControlGroup(schema)) { const key = schema.key; const formGroup = getChildControl(form, key); this.updateForm(formGroup, model[key], schema.schemas); continue; } if (this.schemaUtil.isControlArray(schema)) { const key = schema.key; const formArray = getChildControl(form, key); const [elementSchema] = this.schemaUtil.filterControls(schema.schemas); const elementSchemas = formArray.controls.map((_, index) => ({ ...elementSchema, key: index })); this.updateForm(formArray, model[schema.key], elementSchemas); continue; } if (this.schemaUtil.isControlWrapper(schema) || this.schemaUtil.isComponentContainer(schema)) { this.updateForm(form, model, schema.schemas); continue; } if (this.schemaUtil.isControl(schema)) { const control = getChildControl(form, schema.key); if (schema.value) { const value = this.valueTransformer.transform(schema.value, { model, schema, control }); control.setValue(value, { onlySelf: true }); } // update disabled const disabled = this.valueTransformer.transform(schema.disabled, { model, schema, control }); if (control.enabled !== !disabled) { // Update only if inconsistent if (disabled) { // Using `onlySelf: true` here instead of `emitEvent: false` because the // control's own value/statusChanges events need to trigger properly. control.disable({ onlySelf: true }); } else { control.enable({ onlySelf: true }); } } // update required validator const required = this.valueTransformer.transform(schema.required, { model, schema, control }); if (required) { control.addValidators(Validators.required); } else { control.removeValidators(Validators.required); } control.updateValueAndValidity({ emitEvent: false }); } } } updateModel(model, form, schemas) { for (const schema of schemas) { if (this.schemaUtil.isControlGroup(schema)) { const key = schema.key; const formGroup = getChildControl(form, key); this.updateModel(model[key] = {}, formGroup, schema.schemas); continue; } if (this.schemaUtil.isControlArray(schema)) { const key = schema.key; const formArray = getChildControl(form, key); const [elementSchema] = this.schemaUtil.filterControls(schema.schemas); const elementSchemas = formArray.controls.map((_, index) => ({ ...elementSchema, key: index })); this.updateModel(model[key] = [], formArray, elementSchemas); continue; } if (this.schemaUtil.isControlWrapper(schema) || this.schemaUtil.isComponentContainer(schema)) { this.updateModel(model, form, schema.schemas); continue; } if (this.schemaUtil.isControl(schema)) { const key = schema.key.toString(); const control = getChildControl(form, key); if (!schema.config?.valueCollectionStrategy || schema.config.valueCollectionStrategy === 'value') { const disabled = this.valueTransformer.transform(schema.disabled, { model, schema, control }); // skip disabled controls if (disabled) continue; } const value = this.valueUtil.valueOfControl(control, schema); // Multi-field case. if (this.schemaUtil.isMultiKeySchema(schema)) { schema.key.map((prop, idx) => { model[prop] = value?.[idx] ?? null; }); } else if (this.schemaUtil.isPathKeySchema(schema)) { const paths = this.schemaUtil.parsePathKey(schema.key); let _model = model; for (let i = 0; i < paths.length - 1; i++) { const path = paths[i]; _model = _model[path] ??= {}; } _model[paths.pop()] = value; } else { model[key] = value; } } } return model; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FormUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FormUtil, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FormUtil, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Automatically choose to use `.get()` or `.at()` to get a child control * based on the type of the form. * @param form * @param key * @returns */ function getChildControl(form, key) { return form instanceof FormArray ? form.at(key) : form.get(key.toString()); } /** * @internal */ class ModelUtil { constructor() { this.schemaUtil = inject(SchemaUtil); this.valueUtil = inject(ValueUtil); this.formUtil = inject(FormUtil); } updateForm(form, model, schemas, completed = true) { for (const schema of schemas) { if (this.schemaUtil.isControlGroup(schema)) { const key = schema.key; const formGroup = getChildControl(form, key); this.updateForm(formGroup, model[key] ??= {}, schema.schemas, false); continue; } if (this.schemaUtil.isControlArray(schema)) { const key = schema.key; const array = model[key] ??= []; const formArray = getChildControl(form, key); // / If the model array length matches the form array length, update the // form values in place; otherwise, rebuild the form array. if (array.length === formArray.length) { const [elementSchema] = this.schemaUtil.filterControls(schema.schemas); const elementSchemas = array.map((_, index) => ({ ...elementSchema, key: index })); this.updateForm(formArray, array, elementSchemas, false); } else { const controls = this.formUtil.createFormArrayElements(schema.schemas, array); formArray.clear({ emitEvent: false }); for (const control of controls) { formArray.push(control, { emitEvent: false }); } formArray.updateValueAndValidity({ onlySelf: true }); } continue; } if (this.schemaUtil.isComponentContainer(schema) || this.schemaUtil.isControlWrapper(schema)) { this.updateForm(form, model, schema.schemas, false); continue; } if (this.schemaUtil.isControl(schema)) { const value = this.valueUtil.valueOfModel(model, schema); const control = getChildControl(form, schema.key); control.setValue(value, { onlySelf: true }); } } // Only disable `onlySelf` after all child updates are complete. form.updateValueAndValidity({ onlySelf: !completed }); return form; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ModelUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ModelUtil, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ModelUtil, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class FluentColComponent { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentColComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: FluentColComponent, isStandalone: true, selector: "fluent-col", ngImport: i0, template: '', isInline: true, styles: [".fluent-column{position:relative;flex:0 0 auto;min-width:auto}.fluent-column-fill{flex:1 1 auto}.fluent-column-offset-auto{margin-left:auto}.fluent-column-1{flex:0 0 auto;width:8.3333333333%;max-width:8.3333333333%}.fluent-column-offset-1{margin-left:8.3333333333%}.fluent-column-2{flex:0 0 auto;width:16.6666666667%;max-width:16.6666666667%}.fluent-column-offset-2{margin-left:16.6666666667%}.fluent-column-3{flex:0 0 auto;width:25%;max-width:25%}.fluent-column-offset-3{margin-left:25%}.fluent-column-4{flex:0 0 auto;width:33.3333333333%;max-width:33.3333333333%}.fluent-column-offset-4{margin-left:33.3333333333%}.fluent-column-5{flex:0 0 auto;width:41.6666666667%;max-width:41.6666666667%}.fluent-column-offset-5{margin-left:41.6666666667%}.fluent-column-6{flex:0 0 auto;width:50%;max-width:50%}.fluent-column-offset-6{margin-left:50%}.fluent-column-7{flex:0 0 auto;width:58.3333333333%;max-width:58.3333333333%}.fluent-column-offset-7{margin-left:58.3333333333%}.fluent-column-8{flex:0 0 auto;width:66.6666666667%;max-width:66.6666666667%}.fluent-column-offset-8{margin-left:66.6666666667%}.fluent-column-9{flex:0 0 auto;width:75%;max-width:75%}.fluent-column-offset-9{margin-left:75%}.fluent-column-10{flex:0 0 auto;width:83.3333333333%;max-width:83.3333333333%}.fluent-column-offset-10{margin-left:83.3333333333%}.fluent-column-11{flex:0 0 auto;width:91.6666666667%;max-width:91.6666666667%}.fluent-column-offset-11{margin-left:91.6666666667%}.fluent-column-12{flex:0 0 auto;width:100%;max-width:100%}.fluent-column-offset-12{margin-left:100%}@media (min-width: 576px){.fluent-column-sm-fill{flex:1 1 auto}.fluent-column-offset-sm-auto{margin-left:auto}.fluent-column-sm-1{flex:0 0 auto;width:8.3333333333%;max-width:8.3333333333%}.fluent-column-offset-sm-1{margin-left:8.3333333333%}.fluent-column-sm-2{flex:0 0 auto;width:16.6666666667%;max-width:16.6666666667%}.fluent-column-offset-sm-2{margin-left:16.6666666667%}.fluent-column-sm-3{flex:0 0 auto;width:25%;max-width:25%}.fluent-column-offset-sm-3{margin-left:25%}.fluent-column-sm-4{flex:0 0 auto;width:33.3333333333%;max-width:33.3333333333%}.fluent-column-offset-sm-4{margin-left:33.3333333333%}.fluent-column-sm-5{flex:0 0 auto;width:41.6666666667%;max-width:41.6666666667%}.fluent-column-offset-sm-5{margin-left:41.6666666667%}.fluent-column-sm-6{flex:0 0 auto;width:50%;max-width:50%}.fluent-column-offset-sm-6{margin-left:50%}.fluent-column-sm-7{flex:0 0 auto;width:58.3333333333%;max-width:58.3333333333%}.fluent-column-offset-sm-7{margin-left:58.3333333333%}.fluent-column-sm-8{flex:0 0 auto;width:66.6666666667%;max-width:66.6666666667%}.fluent-column-offset-sm-8{margin-left:66.6666666667%}.fluent-column-sm-9{flex:0 0 auto;width:75%;max-width:75%}.fluent-column-offset-sm-9{margin-left:75%}.fluent-column-sm-10{flex:0 0 auto;width:83.3333333333%;max-width:83.3333333333%}.fluent-column-offset-sm-10{margin-left:83.3333333333%}.fluent-column-sm-11{flex:0 0 auto;width:91.6666666667%;max-width:91.6666666667%}.fluent-column-offset-sm-11{margin-left:91.6666666667%}.fluent-column-sm-12{flex:0 0 auto;width:100%;max-width:100%}.fluent-column-offset-sm-12{margin-left:100%}}@media (min-width: 768px){.fluent-column-md-fill{flex:1 1 auto}.fluent-column-offset-md-auto{margin-left:auto}.fluent-column-md-1{flex:0 0 auto;width:8.3333333333%;max-width:8.3333333333%}.fluent-column-offset-md-1{margin-left:8.3333333333%}.fluent-column-md-2{flex:0 0 auto;width:16.6666666667%;max-width:16.6666666667%}.fluent-column-offset-md-2{margin-left:16.6666666667%}.fluent-column-md-3{flex:0 0 auto;width:25%;max-width:25%}.fluent-column-offset-md-3{margin-left:25%}.fluent-column-md-4{flex:0 0 auto;width:33.3333333333%;max-width:33.3333333333%}.fluent-column-offset-md-4{margin-left:33.3333333333%}.fluent-column-md-5{flex:0 0 auto;width:41.6666666667%;max-width:41.6666666667%}.fluent-column-offset-md-5{margin-left:41.6666666667%}.fluent-column-md-6{flex:0 0 auto;width:50%;max-width:50%}.fluent-column-offset-md-6{margin-left:50%}.fluent-column-md-7{flex:0 0 auto;width:58.3333333333%;max-width:58.3333333333%}.fluent-column-offset-md-7{margin-left:58.3333333333%}.fluent-column-md-8{flex:0 0 auto;width:66.6666666667%;max-width:66.6666666667%}.fluent-column-offset-md-8{margin-left:66.6666666667%}.fluent-column-md-9{flex:0 0 auto;width:75%;max-width:75%}.fluent-column-offset-md-9{margin-left:75%}.fluent-column-md-10{flex:0 0 auto;width:83.3333333333%;max-width:83.3333333333%}.fluent-column-offset-md-10{margin-left:83.3333333333%}.fluent-column-md-11{flex:0 0 auto;width:91.6666666667%;max-width:91.6666666667%}.fluent-column-offset-md-11{margin-left:91.6666666667%}.fluent-column-md-12{flex:0 0 auto;width:100%;max-width:100%}.fluent-column-offset-md-12{margin-left:100%}}@media (min-width: 992px){.fluent-column-lg-fill{flex:1 1 auto}.fluent-column-offset-lg-auto{margin-left:auto}.fluent-column-lg-1{flex:0 0 auto;width:8.3333333333%;max-width:8.3333333333%}.fluent-column-offset-lg-1{margin-left:8.3333333333%}.fluent-column-lg-2{flex:0 0 auto;width:16.6666666667%;max-width:16.6666666667%}.fluent-column-offset-lg-2{margin-left:16.6666666667%}.fluent-column-lg-3{flex:0 0 auto;width:25%;max-width:25%}.fluent-column-offset-lg-3{margin-left:25%}.fluent-column-lg-4{flex:0 0 auto;width:33.3333333333%;max-width:33.3333333333%}.fluent-column-offset-lg-4{margin-left:33.3333333333%}.fluent-column-lg-5{flex:0 0 auto;width:41.6666666667%;max-width:41.6666666667%}.fluent-column-offset-lg-5{margin-left:41.6666666667%}.fluent-column-lg-6{flex:0 0 auto;width:50%;max-width:50%}.fluent-column-offset-lg-6{margin-left:50%}.fluent-column-lg-7{flex:0 0 auto;width:58.3333333333%;max-width:58.3333333333%}.fluent-column-offset-lg-7{margin-left:58.3333333333%}.fluent-column-lg-8{flex:0 0 auto;width:66.6666666667%;max-width:66.6666666667%}.fluent-column-offset-lg-8{margin-left:66.6666666667%}.fluent-column-lg-9{flex:0 0 auto;width:75%;max-width:75%}.fluent-column-offset-lg-9{margin-left:75%}.fluent-column-lg-10{flex:0 0 auto;width:83.3333333333%;max-width:83.3333333333%}.fluent-column-offset-lg-10{margin-left:83.3333333333%}.fluent-column-lg-11{flex:0 0 auto;width:91.6666666667%;max-width:91.6666666667%}.fluent-column-offset-lg-11{margin-left:91.6666666667%}.fluent-column-lg-12{flex:0 0 auto;width:100%;max-width:100%}.fluent-column-offset-lg-12{margin-left:100%}}@media (min-width: 1200px){.fluent-column-xl-fill{flex:1 1 auto}.fluent-column-offset-xl-auto{margin-left:auto}.fluent-column-xl-1{flex:0 0 auto;width:8.3333333333%;max-width:8.3333333333%}.fluent-column-offset-xl-1{margin-left:8.3333333333%}.fluent-column-xl-2{flex:0 0 auto;width:16.6666666667%;max-width:16.6666666667%}.fluent-column-offset-xl-2{margin-left:16.6666666667%}.fluent-column-xl-3{flex:0 0 auto;width:25%;max-width:25%}.fluent-column-offset-xl-3{margin-left:25%}.fluent-column-xl-4{flex:0 0 auto;width:33.3333333333%;max-width:33.3333333333%}.fluent-column-offset-xl-4{margin-left:33.3333333333%}.fluent-column-xl-5{flex:0 0 auto;width:41.6666666667%;max-width:41.6666666667%}.fluent-column-offset-xl-5{margin-left:41.6666666667%}.fluent-column-xl-6{flex:0 0 auto;width:50%;max-width:50%}.fluent-column-offset-xl-6{margin-left:50%}.fluent-column-xl-7{flex:0 0 auto;width:58.3333333333%;max-width:58.3333333333%}.fluent-column-offset-xl-7{margin-left:58.3333333333%}.fluent-column-xl-8{flex:0 0 auto;width:66.6666666667%;max-width:66.6666666667%}.fluent-column-offset-xl-8{margin-left:66.6666666667%}.fluent-column-xl-9{flex:0 0 auto;width:75%;max-width:75%}.fluent-column-offset-xl-9{margin-left:75%}.fluent-column-xl-10{flex:0 0 auto;width:83.3333333333%;max-width:83.3333333333%}.fluent-column-offset-xl-10{margin-left:83.3333333333%}.fluent-column-xl-11{flex:0 0 auto;width:91.6666666667%;max-width:91.6666666667%}.fluent-column-offset-xl-11{margin-left:91.6666666667%}.fluent-column-xl-12{flex:0 0 auto;width:100%;max-width:100%}.fluent-column-offset-xl-12{margin-left:100%}}@media (min-width: 1400px){.fluent-column-xxl-fill{flex:1 1 auto}.fluent-column-offset-xxl-auto{margin-left:auto}.fluent-column-xxl-1{flex:0 0 auto;width:8.3333333333%;max-width:8.3333333333%}.fluent-column-offset-xxl-1{margin-left:8.3333333333%}.fluent-column-xxl-2{flex:0 0 auto;width:16.6666666667%;max-width:16.6666666667%}.fluent-column-offset-xxl-2{margin-left:16.6666666667%}.fluent-column-xxl-3{flex:0 0 auto;width:25%;max-width:25%}.fluent-column-offset-xxl-3{margin-left:25%}.fluent-column-xxl-4{flex:0 0 auto;width:33.3333333333%;max-width:33.3333333333%}.fluent-column-offset-xxl-4{margin-left:33.3333333333%}.fluent-column-xxl-5{flex:0 0 auto;width:41.6666666667%;max-width:41.6666666667%}.fluent-column-offset-xxl-5{margin-left:41.6666666667%}.fluent-column-xxl-6{flex:0 0 auto;width:50%;max-width:50%}.fluent-column-offset-xxl-6{margin-left:50%}.fluent-column-xxl-7{flex:0 0 auto;width:58.3333333333%;max-width:58.3333333333%}.fluent-column-offset-xxl-7{margin-left:58.3333333333%}.fluent-column-xxl-8{flex:0 0 auto;width:66.6666666667%;max-width:66.6666666667%}.fluent-column-offset-xxl-8{margin-left:66.6666666667%}.fluent-column-xxl-9{flex:0 0 auto;width:75%;max-width:75%}.fluent-column-offset-xxl-9{margin-left:75%}.fluent-column-xxl-10{flex:0 0 auto;width:83.3333333333%;max-width:83.3333333333%}.fluent-column-offset-xxl-10{margin-left:83.3333333333%}.fluent-column-xxl-11{flex:0 0 auto;width:91.6666666667%;max-width:91.6666666667%}.fluent-column-offset-xxl-11{margin-left:91.6666666667%}.fluent-column-xxl-12{flex:0 0 auto;width:100%;max-width:100%}.fluent-column-offset-xxl-12{margin-left:100%}}\n"], encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: FluentColComponent, decorators: [{ type: Component, args: [{ selector: 'fluent-col', template: '', encapsulation: ViewEncapsulation.None, styles: [".fluent-column{position:relative;flex:0 0 auto;min-width:auto}.fluent-column-fill{flex:1 1 auto}.fluent-column-offset-auto{margin-left:auto}.fluent-column-1{flex:0 0 auto;width:8.3333333333%;max-width:8.3333333333%}.fluent-column-offset-1{margin-left:8.3333333333%}.fluent-column-2{flex:0 0 auto;width:16.6666666667%;max-width:16.6666666667%}.fluent-column-offset-2{margin-left:16.6666666667%}.fluent-column-3{flex:0 0 auto;width:25%;max-width:25%}.fluent-column-offset-3{margin-left:25%}.fluent-column-4{flex:0 0 auto;width:33.3333333333%;max-width:33.3333333333%}.fluent-column-offset-4{margin-left:33.3333333333%}.fluent-column-5{flex:0 0 auto;width:41.6666666667%;max-width:41.6666666667%}.fluent-column-offset-5{margin-left:41.6666666667%}.fluent-column-6{flex:0 0 auto;width:50%;max-width:50%}.fluent-column-offset-6{margin-left:50%}.fluent-column-7{flex:0 0 auto;width:58.3333333333%;max-width:58.3333333333%}.fluent-column-offset-7{margin-left:58.3333333333%}.fluent-column-8{flex:0 0 auto;width:66.6666666667%;max-width:66.6666666667%}.fluent-column-offset-8{margin-left:66.6666666667%}.fluent-column-9{flex:0 0 auto;width:75%;max-width:75%}.fluent-column-offset-9{margin-left:75%}.fluent-column-10{flex:0 0 auto;width:83.3333333333%;max-width:83.3333333333%}.fluent-column-offset-10{margin-left:83.3333333333%}.fluent-column-11{flex:0 0 auto;width:91.6666666667%;max-width:91.6666666667%}.fluent-column-offset-11{margin-left:91.6666666667%}.fluent-column-12{flex:0 0 auto;width:100%;max-width:100%}.fluent-column-offset-12{margin-left:100%}@media (min-width: 576px){.fluent-column-sm-fill{flex:1 1