UNPKG

@fluent-form/core

Version:

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

1,020 lines (993 loc) 127 kB
import * as i0 from '@angular/core'; import { TemplateRef, ViewChild, Directive, InjectionToken, Injectable, inject, createComponent, Injector, EnvironmentInjector, input, model, computed, untracked, output, DestroyRef, effect, ChangeDetectionStrategy, Component, ElementRef, reflectComponentType, isSignal, runInInjectionContext, Input, ViewEncapsulation, booleanAttribute, NgModule, forwardRef, HostListener, ViewContainerRef, Pipe, APP_BOOTSTRAP_LISTENER, ApplicationRef, makeEnvironmentProviders } from '@angular/core'; import * as i1 from '@angular/common'; import { NgTemplateOutlet, AsyncPipe } from '@angular/common'; import { takeUntilDestroyed, outputToObservable, toSignal } from '@angular/core/rxjs-interop'; import { Validators, FormControl, FormGroup, FormArray } from '@angular/forms'; import { signalSetFn, SIGNAL } from '@angular/core/primitives/signals'; import { Subject, takeUntil, Observable, fromEvent, map } from 'rxjs'; import { _CdkPrivateStyleLoader } from '@angular/cdk/private'; import { BreakpointObserver } from '@angular/cdk/layout'; class TemplateRefHolder { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: TemplateRefHolder, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: TemplateRefHolder, isStandalone: true, viewQueries: [{ propertyName: "templateRef", first: true, predicate: TemplateRef, descendants: true, static: true }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: TemplateRefHolder, decorators: [{ type: Directive }], propDecorators: { templateRef: [{ type: ViewChild, args: [TemplateRef, { static: true }] }] } }); class AbstractFormContentComponent extends TemplateRefHolder { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AbstractFormContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: AbstractFormContentComponent, isStandalone: true, usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AbstractFormContentComponent, decorators: [{ type: Directive }] }); 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_WIDGET_WRAPPERS = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FLUENT_WIDGET_WRAPPERS' : ''); 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); 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: "20.3.16", ngImport: i0, type: DynamicCodeEvaluator, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DynamicCodeEvaluator, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DynamicCodeEvaluator, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * @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: "20.3.16", ngImport: i0, type: ValueTransformer, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ValueTransformer, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", 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 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: "20.3.16", ngImport: i0, type: SchemaUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: SchemaUtil, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", 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: "20.3.16", ngImport: i0, type: ValueUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ValueUtil, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", 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 modalValue = this.valueUtil.valueOfModel(model, schema); const control = new FormControl(schema.defaultValue ?? null, { nonNullable: !isUndefined(schema.defaultValue), validators: schema.validators ? validators.concat(schema.validators) : validators, asyncValidators: schema.asyncValidators, updateOn: schema.config?.updateOn }); if (modalValue !== null) { control.setValue(modalValue, { emitEvent: false }); } return control; } 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); 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: "20.3.16", ngImport: i0, type: FormUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FormUtil, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", 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: "20.3.16", ngImport: i0, type: ModelUtil, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ModelUtil, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ModelUtil, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class FluentForm { constructor() { this.formUtil = inject(FormUtil); this.modelUtil = inject(ModelUtil); this.schemaUtil = inject(SchemaUtil); this.formContentRef = createComponent(inject(FLUENT_FORM_CONTENT), { environmentInjector: inject(EnvironmentInjector), elementInjector: inject(Injector) }); this.schema = input.required(...(ngDevMode ? [{ debugName: "schema" }] : [])); this.model = model.required(...(ngDevMode ? [{ debugName: "model" }] : [])); this.patchedSchema = computed(() => this.schemaUtil.patch(this.schema()), ...(ngDevMode ? [{ debugName: "patchedSchema" }] : [])); this.form = computed(() => this.formUtil.createFormGroup(this.patchedSchema(), untracked(this.model)), ...(ngDevMode ? [{ debugName: "form" }] : [])); this.formChange = output(); this.valueChanges = output(); this.statusChanges = output(); // eslint-disable-next-line @angular-eslint/no-output-native this.submit = output(); this.onSubmit = (event) => { event.stopPropagation(); this.submit.emit(event); // Forms with `method="dialog"` have some special behavior that won't reload the page and that // shouldn't be prevented. Note that we need to null check the `event` and the `target`, because // some internal apps call this method directly with the wrong arguments. return event?.target?.method === 'dialog'; }; const destroyRef = inject(DestroyRef); effect(() => { const form = this.form(); untracked(() => { this.formChange.emit(form); this.onValueChanges(); form.valueChanges.pipe(takeUntilDestroyed(destroyRef)).subscribe(value => { this.onValueChanges(); this.valueChanges.emit(value); }); form.statusChanges.pipe(takeUntilDestroyed(destroyRef)).subscribe(status => this.statusChanges.emit(status)); }); }); let modelChangeCounter = 0; effect(() => { const model = this.model(); untracked(() => { // If the change comes from outside, assign the value to the form. if (modelChangeCounter++ > 0 && model !== this.internalModel) { this.modelUtil.updateForm(this.form(), model, this.patchedSchema().schemas); } }); }); destroyRef.onDestroy(() => this.formContentRef.destroy()); } onValueChanges() { const form = this.form(); const { schemas } = this.patchedSchema(); this.formUtil.updateForm(form, this.internalModel = this.formUtil.updateModel({}, form, schemas), schemas); this.model.set(this.internalModel); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FluentForm, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: FluentForm, isStandalone: true, selector: "fluent-form", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { model: "modelChange", formChange: "formChange", valueChanges: "valueChanges", statusChanges: "statusChanges", submit: "submit" }, providers: [ { provide: NAMED_TEMPLATES, useFactory: () => [] } ], ngImport: i0, template: "<ng-container\n [ngTemplateOutlet]=\"formContentRef.instance.templateRef\"\n [ngTemplateOutletContext]=\"{ schema: patchedSchema(), model: model(), form: form(), onSubmit }\" />\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FluentForm, decorators: [{ type: Component, args: [{ selector: 'fluent-form', imports: [NgTemplateOutlet], providers: [ { provide: NAMED_TEMPLATES, useFactory: () => [] } ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container\n [ngTemplateOutlet]=\"formContentRef.instance.templateRef\"\n [ngTemplateOutletContext]=\"{ schema: patchedSchema(), model: model(), form: form(), onSubmit }\" />\n" }] }], ctorParameters: () => [], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], model: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: true }] }, { type: i0.Output, args: ["modelChange"] }], formChange: [{ type: i0.Output, args: ["formChange"] }], valueChanges: [{ type: i0.Output, args: ["valueChanges"] }], statusChanges: [{ type: i0.Output, args: ["statusChanges"] }], submit: [{ type: i0.Output, args: ["submit"] }] } }); /** * @deprecated * This component will be removed in future versions. Please use {@link FluentForm} instead. */ const FluentFormComponent = FluentForm; class AbstractWidgetWrapper extends TemplateRefHolder { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AbstractWidgetWrapper, deps: null, target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: AbstractWidgetWrapper, isStandalone: true, usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AbstractWidgetWrapper, decorators: [{ type: Directive }] }); const COMPOSE_KEY = 'schemas'; const SCHEMA_STACK = []; function getCurrentSchema() { return SCHEMA_STACK[SCHEMA_STACK.length - 1]; } function enterSchema(schema) { SCHEMA_STACK.push(schema); } function leaveSchema() { SCHEMA_STACK.pop(); } function joinSchema(schema) { const currentSchema = getCurrentSchema(); // If a schema already exists, just push it in as a subschema. currentSchema?.schemas.push(schema); } const IS_BUILDER = Symbol(); const BUILD_KEY = 'build'; function composeBuilder(target = {}, append = true) { append && joinSchema(target); const builder = new Proxy(target, { get(target, property) { switch (property) { case IS_BUILDER: return true; case BUILD_KEY: return () => target; case COMPOSE_KEY: return (composeFn) => { target[COMPOSE_KEY] = []; enterSchema(target); try { composeFn(builder); } finally { leaveSchema(); } return builder; }; } return (value) => { if (value !== target[property]) { target[property] = value; } return builder; }; } }); return builder; } /** * Determines whether the value is a builder. */ function isBuilder(value) { return value[IS_BUILDER] ?? false; } function row(key) { return composeBuilder().kind('row').key(key); } function headless(key) { return composeBuilder().kind('headless').key(key); } function form(schemasOrComposeFn) { if (isArray(schemasOrComposeFn)) { return { kind: 'group', key: 'root', schemas: schemasOrComposeFn }; } return computed(() => composeBuilder() .kind('group') .key('root') .schemas(schemasOrComposeFn) .build()); } class DestroyedSubject extends Subject { constructor() { super(); inject(DestroyRef).onDestroy(() => { this.next(); this.complete(); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DestroyedSubject, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DestroyedSubject }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DestroyedSubject, decorators: [{ type: Injectable }], ctorParameters: () => [] }); class WidgetTemplateRegistry extends Map { constructor() { super(...arguments); this.envInjector = inject(EnvironmentInjector); this.widgetMap = inject(WIDGET_MAP); } get(kind) { return super.get(kind) ?? this.register(kind); } register(kind) { const component = this.widgetMap.get(kind); if (typeof ngDevMode !== 'undefined' && ngDevMode && !component) { throwWidgetNotFoundError(kind); } const tmpl = component().then(comp => { const { instance } = createComponent(comp, { environmentInjector: this.envInjector }); return instance.templateRef; }); this.set(kind, tmpl); return tmpl; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: WidgetTemplateRegistry, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: WidgetTemplateRegistry, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: WidgetTemplateRegistry, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class WidgetWrapperTemplateRegistry extends Map { constructor() { super(...arguments); this.envInjector = inject(EnvironmentInjector); } get(type) { return super.get(type) ?? this.register(type); } register(type) { const { templateRef } = createComponent(type, { environmentInjector: this.envInjector }).instance; this.set(type, templateRef); return templateRef; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: WidgetWrapperTemplateRegistry, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: WidgetWrapperTemplateRegistry, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: WidgetWrapperTemplateRegistry, 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.injector = inject(Injector); this.fluentBinding = input.required(...(ngDevMode ? [{ debugName: "fluentBinding" }] : [])); this.schema = computed(() => this.fluentBinding().schema, ...(ngDevMode ? [{ debugName: "schema" }] : [])); this.model = computed(() => this.fluentBinding().model, ...(ngDevMode ? [{ debugName: "model" }] : [])); this.control = computed(() => this.fluentBinding().control, ...(ngDevMode ? [{ debugName: "control" }] : [])); this.component = computed(() => this.fluentBinding().component, ...(ngDevMode ? [{ debugName: "component" }] : [])); const elementRef = inject(ElementRef); const destroyed = inject(DestroyedSubject); effect(() => { const component = this.component(); const schema = this.schema(); const control = this.control(); const element = elementRef.nativeElement; const outputs = component ? reflectComponentType(component.constructor)?.outputs.map(output => output.propName) ?? [] : []; untracked(() => { const context = () => ({ control, schema, model: this.model(), element, component }); destroyed.next(); if (isPropertyHolder(schema)) { const host = component ?? element; 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 { // First try to bind to the component's output, then fallback to DOM event binding if (component) { if (outputs.includes(eventName)) { const output = component[eventName]; const observable = output instanceof Observable ? output : outputToObservable(output); observable.pipe(takeUntil(destroyed)).subscribe(event => { listener(event, context()); }); continue; } } fromEvent(element, eventName).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 { // First try to bind to the component's output, then fallback to DOM event binding if (component) { if (outputs.includes(eventName)) { const output = component[eventName]; const observable = output instanceof Observable ? output : outputToObservable(output); observable.pipe(map(event => ({ event, context: context() })), observer, takeUntil(destroyed)).subscribe(); continue; } } fromEvent(element, eventName).pipe(map(event => ({ event, context: context() })), observer, takeUntil(destroyed)).subscribe(); } } } }); }); } ngOnInit() { const schema = this.schema(); const control = this.control(); const context = () => ({ control, schema, model: this.model() }); if (isHookHolder(schema)) { runInInjectionContext(this.injector, () => schema.hooks.onInit?.(context())); this.destroyRef.onDestroy(() => runInInjectionContext(this.injector, () => schema.hooks.onDestroy?.(context()))); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FluentBindingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.16", type: FluentBindingDirective, isStandalone: true, selector: "[fluentBinding]", inputs: { fluentBinding: { classPropertyName: "fluentBinding", publicName: "fluentBinding", isSignal: true, isRequired: true, transformFunction: null } }, providers: [DestroyedSubject], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FluentBindingDirective, decorators: [{ type: Directive, args: [{ selector: '[fluentBinding]', providers: [DestroyedSubject] }] }], ctorParameters: () => [], propDecorators: { fluentBinding: [{ type: i0.Input, args: [{ isSignal: true, alias: "fluentBinding", required: true }] }] } }); /** * @internal */ class FluentContextGuardDirective { /** * @internal */ static ngTemplateContextGuard(_, context) { return true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FluentContextGuardDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: FluentContextGuardDirective, isStandalone: true, selector: "ng-template[fluentContextGuard]", inputs: { fluentContextGuard: "fluentContextGuard" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", 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: "20.3.16", ngImport: i0, type: FluentControlWrapperDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: FluentControlWrapperDirective, isStandalone: true, selector: "[fluentControlWrapper]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FluentControlWrapperDirective, decorators: [{ type: Directive, args: [{ selector: '[fluentControlWrapper]' }] }] }); 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}` : ''; } class FluentColComponent { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: FluentColComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", 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-co