@fluent-form/core
Version:
An Angular dynamic forms library powered by Fluent API and JSON.
839 lines (819 loc) • 110 kB
JavaScript
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