@fluent-form/core
Version:
An Angular dynamic forms library powered by Fluent API and JSON.
1 lines • 145 kB
Source Map (JSON)
{"version":3,"file":"fluent-form-core.mjs","sources":["../../../packages/core/src/lib/services/destroyed.service.ts","../../../packages/core/src/lib/services/evaluator.service.ts","../../../packages/core/src/lib/utils/is.utils.ts","../../../packages/core/src/lib/services/value-transformer.service.ts","../../../packages/core/src/lib/errors/runtime.ts","../../../packages/core/src/lib/tokens.ts","../../../packages/core/src/lib/services/widget-template-registry.service.ts","../../../packages/core/src/lib/directives/binding.directive.ts","../../../packages/core/src/lib/directives/context-guard.directive.ts","../../../packages/core/src/lib/directives/control-wrapper.directive.ts","../../../packages/core/src/lib/directives/form-field-outlet.directive.ts","../../../packages/core/src/lib/breakpoints.ts","../../../packages/core/src/lib/style.ts","../../../packages/core/src/lib/patcher/provider.ts","../../../packages/core/src/lib/schemas/interfaces.ts","../../../packages/core/src/lib/utils/schema.utils.ts","../../../packages/core/src/lib/utils/value.utils.ts","../../../packages/core/src/lib/utils/form.utils.ts","../../../packages/core/src/lib/utils/model.utils.ts","../../../packages/core/src/lib/directives/grid/col/col.component.ts","../../../packages/core/src/lib/directives/grid/row/row.component.ts","../../../packages/core/src/lib/directives/grid/module.ts","../../../packages/core/src/lib/directives/render/models/control-container.ts","../../../packages/core/src/lib/directives/render/form.directive.ts","../../../packages/core/src/lib/directives/render/outlet.directive.ts","../../../packages/core/src/lib/directives/render/module.ts","../../../packages/core/src/lib/directives/template-outlet.ts","../../../packages/core/src/lib/directives/template-ref-holder.directive.ts","../../../packages/core/src/lib/directives/template.directive.ts","../../../packages/core/src/lib/directives/with-injector.directive.ts","../../../packages/core/src/lib/components/form-content/form-content.component.ts","../../../packages/core/src/lib/components/form-field-content/form-field-content.component.ts","../../../packages/core/src/lib/components/form/form.component.ts","../../../packages/core/src/lib/components/form/form.component.html","../../../packages/core/src/lib/compose/builder.ts","../../../packages/core/src/lib/compose/component-container.ts","../../../packages/core/src/lib/compose/control.ts","../../../packages/core/src/lib/features/interface.ts","../../../packages/core/src/lib/features/helper.ts","../../../packages/core/src/lib/features/schema-patcher.feature.ts","../../../packages/core/src/lib/features/static-expression.feature.ts","../../../packages/core/src/lib/pipes/col.pipe.ts","../../../packages/core/src/lib/pipes/control.pipe.ts","../../../packages/core/src/lib/pipes/inject.pipe.ts","../../../packages/core/src/lib/pipes/invoke.pipe.ts","../../../packages/core/src/lib/pipes/new.pipe.ts","../../../packages/core/src/lib/pipes/reactive.pipe.ts","../../../packages/core/src/lib/pipes/renderable.pipe.ts","../../../packages/core/src/lib/pipes/schema-type.pipe.ts","../../../packages/core/src/lib/pipes/schema.pipe.ts","../../../packages/core/src/lib/pipes/template.pipe.ts","../../../packages/core/src/lib/pipes/widget-template.pipe.ts","../../../packages/core/src/lib/widgets/widget.ts","../../../packages/core/src/lib/widgets/row/row.widget.ts","../../../packages/core/src/lib/widgets/row/row.widget.html","../../../packages/core/src/lib/widgets/use.ts","../../../packages/core/src/lib/features/widget-configs.feature.ts","../../../packages/core/src/lib/module.ts","../../../packages/core/src/lib/provider.ts","../../../packages/core/src/fluent-form-core.ts"],"sourcesContent":["import { DestroyRef, inject, Injectable } from '@angular/core';\nimport { Subject } from 'rxjs';\n\n@Injectable()\nexport class DestroyedSubject extends Subject<void> {\n constructor() {\n super();\n\n inject(DestroyRef).onDestroy(() => {\n this.next();\n this.complete();\n });\n }\n}\n","import { Injectable } from '@angular/core';\nimport type { AnyObject, SafeAny } from '@ngify/core';\n\nconst RETURN_STR = 'return ';\nconst STATIC_EXPRESSION_PATTERN = /^{{.+}}$/;\nconst INTERPOLATION_PATTERN = /^{{|}}$/g;\n\nexport abstract class CodeEvaluator {\n abstract evaluate(code: string, context: AnyObject): SafeAny;\n}\n\nexport function isStaticExpression(str: string) {\n return STATIC_EXPRESSION_PATTERN.test(str);\n}\n\n@Injectable({ providedIn: 'root' })\nexport class DynamicCodeEvaluator implements CodeEvaluator {\n evaluate(code: string, context: AnyObject) {\n code = RETURN_STR + code.replace(INTERPOLATION_PATTERN, '');\n\n // TODO: This doesn't work with `unsafe-eval` enabled in CSP.\n const fn = new Function('o', `with(o){${code}}`);\n const proxy = new Proxy(Object.freeze(context), {\n // Intercept all properties to prevent lookup beyond the `Proxy` object's scope chain.\n has() {\n return true;\n },\n get(target, key, receiver) {\n if (key === Symbol.unscopables) {\n return undefined;\n }\n return Reflect.get(target, key, receiver);\n }\n });\n\n return fn(proxy);\n }\n}\n","import type { SafeAny } from '@ngify/core';\n\nexport const isObject = (o: unknown): o is object => typeof o === 'object';\nexport const isNumber = (o: unknown): o is number => typeof o === 'number';\nexport const isString = (o: unknown): o is string => typeof o === 'string';\nexport const isFunction = (o: unknown): o is (...args: SafeAny) => SafeAny => typeof o === 'function';\nexport const isBoolean = (o: unknown): o is boolean => typeof o === 'boolean';\nexport const isUndefined = (o: unknown): o is undefined => o === undefined;\nexport const isArray = (o: unknown): o is SafeAny[] => Array.isArray(o);\n","import { inject, Injectable } from '@angular/core';\nimport type { AnyObject, SafeAny } from '@ngify/core';\nimport { isFunction, isString } from '../utils/is.utils';\nimport { CodeEvaluator, isStaticExpression } from './evaluator.service';\n\n/**\n * @internal\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class ValueTransformer {\n private readonly evaluator = inject(CodeEvaluator, { optional: true });\n\n transform(value: SafeAny, context: AnyObject = {}): SafeAny {\n if (isFunction(value)) {\n return value(context);\n }\n\n if (isString(value) && this.evaluator && isStaticExpression(value)) {\n return this.evaluator.evaluate(value, context);\n }\n\n return value;\n }\n}\n","export function throwWidgetNotFoundError(name: string | number): never {\n throw new Error(`The '${name}' widget was not found`);\n}\n\nexport function throwCustomTemplateNotFoundError(name: string): never {\n throw new Error(`The custom '${name}' template was not found`);\n}\n","import { InjectionToken, type TemplateRef, type Type } from '@angular/core';\nimport type { SafeAny } from '@ngify/core';\nimport type { AbstractFormContentComponent, AbstractFormFieldContentComponent } from './components';\nimport type { SchemaConfig } from './interfaces';\nimport type { AbstractWidget } from './widgets/widget';\n\ndeclare const ngDevMode: boolean | undefined;\n\nexport const WIDGET_MAP = new InjectionToken<Map<string, Type<AbstractWidget<unknown>>>>(\n typeof ngDevMode !== 'undefined' && ngDevMode ? 'WIDGET_MAP' : ''\n);\n\nexport const SCHEMA_MAP = new InjectionToken<Map<string, SchemaConfig<SafeAny>>>(\n typeof ngDevMode !== 'undefined' && ngDevMode ? 'SCHEMA_MAP' : ''\n);\n\nexport const NAMED_TEMPLATES = new InjectionToken<{ name: string, templateRef: TemplateRef<SafeAny> }[]>(\n typeof ngDevMode !== 'undefined' && ngDevMode ? 'NAMED_TEMPLATES' : ''\n);\n\nexport const FLUENT_FORM_CONTENT = new InjectionToken<Type<AbstractFormContentComponent>>(\n typeof ngDevMode !== 'undefined' && ngDevMode ? 'FLUENT_FORM_CONTENT' : ''\n);\nexport const FLUENT_FORM_FIELD_CONTENT = new InjectionToken<Type<AbstractFormFieldContentComponent>>(\n typeof ngDevMode !== 'undefined' && ngDevMode ? 'FLUENT_FORM_FIELD_CONTENT' : ''\n);\n","import { createComponent, EnvironmentInjector, inject, Injectable, TemplateRef } from '@angular/core';\nimport { throwWidgetNotFoundError } from '../errors';\nimport { WIDGET_MAP } from '../tokens';\n\ndeclare const ngDevMode: boolean | undefined;\n\n@Injectable({ providedIn: 'root' })\nexport class WidgetTemplateRegistry extends Map<string, TemplateRef<unknown>> {\n private readonly envInjector = inject(EnvironmentInjector);\n private readonly map = inject(WIDGET_MAP);\n\n override get(kind: string): TemplateRef<unknown> {\n return super.get(kind) ?? this.register(kind);\n }\n\n private register(kind: string) {\n const component = this.map.get(kind);\n\n if (typeof ngDevMode !== 'undefined' && ngDevMode && !component) {\n throwWidgetNotFoundError(kind);\n }\n\n const { instance } = createComponent(component!, {\n environmentInjector: this.envInjector\n });\n\n this.set(kind, instance.templateRef);\n\n return instance.templateRef;\n }\n}\n","import {\n DestroyRef, Directive, ElementRef, type OnInit, type OutputRef, effect, inject, input, isSignal, untracked\n} from '@angular/core';\nimport { SIGNAL, type SignalNode, signalSetFn } from '@angular/core/primitives/signals';\nimport { outputToObservable } from '@angular/core/rxjs-interop';\nimport { AbstractControl } from '@angular/forms';\nimport type { AnyObject, SafeAny } from '@ngify/core';\nimport { Observable, fromEvent, map, takeUntil } from 'rxjs';\nimport type { AbstractSchema, EventListenerHolder, EventObserverHolder, HooksHolder, PropertyHolder, SchemaContext } from '../schemas';\nimport { DestroyedSubject } from '../services';\n\nfunction isHookHolder(value: SafeAny): value is Required<HooksHolder> {\n return 'hooks' in value;\n}\n\nfunction isListenerHolder(value: SafeAny): value is Required<EventListenerHolder> {\n return 'listeners' in value;\n}\n\nfunction isPropertyHolder(value: SafeAny): value is Required<PropertyHolder> {\n return 'properties' in value;\n}\n\nfunction isObserverHolder(value: SafeAny): value is Required<EventObserverHolder> {\n return 'observers' in value;\n}\n\n/**\n * @internal\n */\n@Directive({\n selector: '[fluentBindingSchema]',\n providers: [DestroyedSubject]\n})\nexport class FluentBindingDirective<E extends HTMLElement, C extends object, S extends AbstractSchema> implements OnInit {\n private readonly destroyRef = inject(DestroyRef);\n\n readonly fluentBindingComponent = input<C>();\n readonly fluentBindingSchema = input.required<S>();\n readonly fluentBindingControl = input.required<AbstractControl>();\n readonly fluentBindingModel = input.required<AnyObject>();\n\n constructor() {\n const elementRef: ElementRef<E> = inject(ElementRef);\n const destroyed = inject(DestroyedSubject);\n\n effect(() => {\n const component = this.fluentBindingComponent();\n const schema = this.fluentBindingSchema();\n const control = this.fluentBindingControl();\n const host = component ?? elementRef.nativeElement;\n\n untracked(() => {\n const context = (): SchemaContext => ({ control, schema, model: this.fluentBindingModel() });\n destroyed.next();\n\n if (isPropertyHolder(schema)) {\n for (const [property, value] of Object.entries(schema.properties)) {\n const prop = host[property as keyof (C | E)];\n if (isSignal(prop)) {\n signalSetFn(prop[SIGNAL] as SignalNode<SafeAny>, value);\n } else {\n host[property as keyof (C | E)] = value;\n }\n }\n }\n\n if (isListenerHolder(schema)) {\n for (const [eventName, listener] of Object.entries(schema.listeners)) {\n if (eventName === 'valueChange') {\n control.valueChanges.pipe(\n takeUntil(destroyed)\n ).subscribe(value => {\n listener!(value, context());\n });\n } else if (eventName === 'statusChange') {\n control.statusChanges.pipe(\n takeUntil(destroyed)\n ).subscribe(status => {\n listener!(status, context());\n });\n } else if (host instanceof HTMLElement) {\n fromEvent(host, eventName).pipe(\n takeUntil(destroyed)\n ).subscribe(event => {\n listener!(event, context());\n });\n } else {\n const output = host[eventName as keyof C] as Observable<SafeAny> | OutputRef<SafeAny>;\n const observable = output instanceof Observable ? output : outputToObservable(output);\n\n observable.pipe(\n takeUntil(destroyed)\n ).subscribe(event => {\n listener!(event, context());\n });\n }\n }\n }\n\n if (isObserverHolder(schema)) {\n for (const [eventName, observer] of Object.entries(schema.observers)) {\n if (eventName === 'valueChange') {\n control.valueChanges.pipe(\n map(event => ({ event, context: context() })),\n observer!,\n takeUntil(destroyed)\n ).subscribe();\n } else if (eventName === 'statusChange') {\n control.statusChanges.pipe(\n map(event => ({ event, context: context() })),\n observer!,\n takeUntil(destroyed)\n ).subscribe();\n } else if (host instanceof HTMLElement) {\n fromEvent(host, eventName).pipe(\n map(event => ({ event, context: context() })),\n observer!,\n takeUntil(destroyed)\n ).subscribe();\n } else {\n const output = host[eventName as keyof C] as Observable<SafeAny> | OutputRef<SafeAny>;\n const observable = output instanceof Observable ? output : outputToObservable(output);\n\n observable.pipe(\n map(event => ({ event, context: context() })),\n observer!,\n takeUntil(destroyed)\n ).subscribe();\n }\n }\n }\n });\n });\n }\n\n ngOnInit() {\n const schema = this.fluentBindingSchema();\n const control = this.fluentBindingControl();\n const model = this.fluentBindingModel();\n const context: SchemaContext = { control, schema, model };\n\n if (isHookHolder(schema)) {\n schema.hooks.onInit?.(context);\n this.destroyRef.onDestroy(() =>\n schema.hooks.onDestroy?.(context));\n }\n }\n}\n","import { Directive, Input } from '@angular/core';\n\n/**\n * @internal\n */\n@Directive({\n selector: 'ng-template[fluentContextGuard]'\n})\nexport class FluentContextGuardDirective<T> {\n @Input() fluentContextGuard!: T;\n\n /**\n * @internal\n */\n static ngTemplateContextGuard<T>(_: FluentContextGuardDirective<T>, context: unknown): context is T {\n return true;\n }\n}\n","import { Directive } from '@angular/core';\n\n@Directive({\n selector: '[fluentControlWrapper]'\n})\nexport class FluentControlWrapperDirective { }\n","import { NgTemplateOutlet } from '@angular/common';\nimport { Directive, EnvironmentInjector, Injector, createComponent, inject } from '@angular/core';\nimport type { SafeAny } from '@ngify/core';\nimport { FLUENT_FORM_FIELD_CONTENT } from '../tokens';\n\n@Directive({\n selector: '[fluentFormFieldOutlet]',\n hostDirectives: [\n {\n directive: NgTemplateOutlet,\n inputs: ['ngTemplateOutletContext: fluentFormFieldOutlet']\n }\n ]\n})\nexport class FluentFormFieldOutletDirective {\n constructor() {\n const outlet = inject(NgTemplateOutlet);\n const injector = inject(Injector);\n const { templateRef } = createComponent(inject(FLUENT_FORM_FIELD_CONTENT), {\n environmentInjector: inject(EnvironmentInjector),\n elementInjector: injector\n }).instance;\n outlet.ngTemplateOutlet = templateRef;\n outlet.ngTemplateOutletInjector = Injector.create({\n providers: [],\n parent: injector\n });\n outlet.ngOnChanges({ ngTemplateOutlet: {} as SafeAny });\n }\n}\n","export const Breakpoints = {\n xs: '(max-width:575px)',\n sm: '(min-width:576px)',\n md: '(min-width:768px)',\n lg: '(min-width:992px)',\n xl: '(min-width:1200px)',\n xxl: '(min-width:1400px)'\n} as const;\n\nexport type Breakpoints = typeof Breakpoints;\n\nexport function createBreakpointInfix(breakpoint?: keyof Breakpoints) {\n return breakpoint !== 'xs' ? `-${breakpoint}` : '';\n}\n","import { DestroyRef, EnvironmentInjector, Type, createComponent, inject } from '@angular/core';\n\nexport function withStyle(component: Type<unknown>): void {\n const environmentInjector = inject(EnvironmentInjector);\n const destroyRef = inject(DestroyRef);\n const componentRef = createComponent(component, { environmentInjector });\n destroyRef.onDestroy(() => componentRef.destroy());\n}\n","import { InjectionToken, type Provider } from '@angular/core';\nimport type { SchemaPatcher } from './interfaces';\n\ndeclare const ngDevMode: boolean | undefined;\n\nexport const SCHEMA_PATCHERS = new InjectionToken<SchemaPatcher[]>(\n typeof ngDevMode !== 'undefined' && ngDevMode ? 'SchemaPatchers' : ''\n);\n\nexport function provideSchemaPatcher(patcher: SchemaPatcher): Provider {\n return {\n provide: SCHEMA_PATCHERS,\n useValue: patcher,\n multi: true\n };\n}\n","import { AbstractControl } from '@angular/forms';\nimport type { SafeAny } from '@ngify/core';\nimport type { AbstractSchema } from './abstract.schema';\nimport type { SchemaKey } from './types';\n\n/**\n * @public\n */\nexport enum SchemaType {\n Control,\n ControlGroup,\n ControlArray,\n ControlWrapper,\n Component,\n ComponentContainer,\n ComponentWrapper\n}\n\nexport const enum SchemaKind {\n Headless = 'headless',\n Headful = 'headful',\n Template = 'template',\n Row = 'row'\n}\n\nexport interface SchemaLike<Key extends SchemaKey = SchemaKey> {\n kind: string;\n key?: Key;\n}\n\nexport interface SchemaContext<S extends SchemaLike = AbstractSchema> {\n schema: S;\n /**\n * If there is no corresponding control, the parent control will be returned,\n * usually a `FormGroup` or `FormArray`.\n */\n control: AbstractControl;\n model: SafeAny;\n}\n\nexport type Length = number | { max?: number, min?: number };\n\nexport type SchemaReactiveFn<S extends AbstractSchema, R> = (ctx: SchemaContext<S>) => R;\n\nexport type MaybeSchemaReactiveFn<S extends AbstractSchema, R> = R | SchemaReactiveFn<S, R>;\n\nexport type WithoutSchemaReactiveFn<T extends MaybeSchemaReactiveFn<AbstractSchema, SafeAny>> = Exclude<T, SchemaReactiveFn<SafeAny, SafeAny>>;\n","import { inject, Injectable } from '@angular/core';\nimport { type ValidatorFn, Validators } from '@angular/forms';\nimport { throwWidgetNotFoundError } from '../errors';\nimport { SCHEMA_PATCHERS } from '../patcher';\nimport type {\n AbstractBranchSchema,\n AbstractComponentContainerSchema,\n AbstractComponentWrapperSchema,\n AbstractControlContainerSchema,\n AbstractControlSchema,\n AbstractControlWrapperSchema,\n AbstractSchema,\n SchemaKey,\n SingleSchemaKey\n} from '../schemas';\nimport { type SchemaLike, SchemaType } from '../schemas/interfaces';\nimport { SCHEMA_MAP } from '../tokens';\nimport type { Indexable } from '../types';\nimport { isArray, isString } from './is.utils';\n\ndeclare const ngDevMode: boolean | undefined;\n\nconst ANY_SCHEMA_SELECTOR = '*';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class SchemaUtil {\n private readonly schemaMap = inject(SCHEMA_MAP);\n private readonly schemaPatchers = inject(SCHEMA_PATCHERS, { optional: true }) ?? [];\n\n patch<T extends Indexable<AbstractSchema>>(schema: T): T {\n if (\n this.isControlWrapper(schema) ||\n this.isComponentWrapper(schema) ||\n this.isControlContainer(schema) ||\n this.isComponentContainer(schema)\n ) {\n schema.schemas = schema.schemas.map(schema => this.patch(schema));\n }\n\n const config = this.schemaMap.get(schema.kind);\n\n if (typeof ngDevMode !== 'undefined' && ngDevMode && !config) {\n throwWidgetNotFoundError(schema.kind);\n }\n\n const { type } = config!;\n\n return this.schemaPatchers\n .filter(patcher => {\n // Convert selector to an array for unified processing.\n const selector = isArray(patcher.selector) ? patcher.selector : [patcher.selector];\n\n return selector.some(kindOrType => {\n if (isString(kindOrType)) {\n return kindOrType === ANY_SCHEMA_SELECTOR || kindOrType === schema.kind;\n }\n\n return kindOrType === type;\n });\n })\n .reduce((patchedSchema, patcher) => {\n return patcher.patch(patchedSchema) as T;\n }, schema);\n }\n\n /**\n * Filter out top-level control/control container schemas.\n */\n filterControls(schemas: Indexable<AbstractSchema>[]) {\n return schemas.reduce((schemas, schema) => {\n if (this.isControlWrapper(schema) || this.isComponentContainer(schema)) {\n schemas = schemas.concat(this.filterControls(schema.schemas));\n } else if (this.isControl(schema) || this.isControlContainer(schema)) {\n schemas.push(schema);\n }\n\n return schemas;\n }, [] as (AbstractControlSchema | AbstractControlContainerSchema)[]);\n }\n\n isControlGroup(schema: SchemaLike): schema is AbstractControlContainerSchema {\n return this.typeOf(schema) === SchemaType.ControlGroup;\n }\n\n isControlArray(schema: SchemaLike): schema is AbstractControlContainerSchema {\n return this.typeOf(schema) === SchemaType.ControlArray;\n }\n\n isControlContainer(schema: SchemaLike): schema is AbstractControlContainerSchema {\n return this.isControlGroup(schema) || this.isControlArray(schema);\n }\n\n isControlWrapper(schema: SchemaLike): schema is AbstractControlWrapperSchema {\n return this.typeOf(schema) === SchemaType.ControlWrapper;\n }\n\n isControl(schema: SchemaLike): schema is AbstractControlSchema {\n return this.typeOf(schema) === SchemaType.Control;\n }\n\n isComponentContainer(schema: SchemaLike): schema is AbstractComponentContainerSchema {\n return this.typeOf(schema) === SchemaType.ComponentContainer;\n }\n\n isComponentWrapper(schema: SchemaLike): schema is AbstractComponentWrapperSchema {\n return this.typeOf(schema) === SchemaType.ComponentWrapper;\n }\n\n isComponent(schema: SchemaLike): schema is AbstractSchema {\n return this.typeOf(schema) === SchemaType.Component;\n }\n\n /**\n * Whether it is a multi-field schema.\n */\n isMultiKeySchema(schema: SchemaLike) {\n return isArray(schema.key);\n }\n\n /**\n * Whether it is a path field schema.\n */\n isPathKeySchema(schema: SchemaLike) {\n return isString(schema.key) && schema.key.includes('.');\n }\n\n /**\n * Non-control schema, indicating that it or its child nodes\n * do not contain any control schemas.\n */\n isNonControl(schema: SchemaLike): schema is AbstractSchema {\n return this.isComponent(schema) || this.isComponentWrapper(schema);\n }\n\n typeOf(schema: SchemaLike) {\n return this.schemaMap.get(schema.kind)?.type;\n }\n\n validatorsOf(schema: AbstractControlSchema) {\n const validators: ValidatorFn[] = this.schemaMap.get(schema.kind)?.validators?.(schema) ?? [];\n\n if (schema.required === true) {\n validators.push(Validators.required);\n }\n\n return validators;\n }\n\n parsePathKey(key: string) {\n return key.split('.');\n }\n\n norimalizePaths(paths: SingleSchemaKey | SchemaKey[]) {\n return (isArray(paths) ? paths : [paths]).map(o => o.toString());\n }\n\n find(schema: Indexable<AbstractBranchSchema>, key: SingleSchemaKey): AbstractSchema | null;\n find(schema: Indexable<AbstractBranchSchema>, key: SchemaKey[]): AbstractSchema | null;\n find(schema: Indexable<AbstractBranchSchema>, paths: SingleSchemaKey | SchemaKey[]): AbstractSchema | null;\n find(schema: Indexable<AbstractBranchSchema>, paths: SingleSchemaKey | SchemaKey[]): AbstractSchema | null {\n let currentSchema: Indexable<AbstractSchema> | null = schema;\n\n for (const path of this.norimalizePaths(paths)) {\n currentSchema = currentSchema['schemas'].find((o: AbstractSchema) => o.key?.toString() === path) ?? null;\n\n if (currentSchema === null) {\n return currentSchema;\n }\n }\n\n return currentSchema;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport type { AbstractControl } from '@angular/forms';\nimport type { AnyArray, AnyObject } from '@ngify/core';\nimport type { AbstractControlSchema } from '../schemas';\nimport { isUndefined } from './is.utils';\nimport { SchemaUtil } from './schema.utils';\n\n/**\n * @internal\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class ValueUtil {\n private readonly schemaUtil = inject(SchemaUtil);\n\n valueOfModel<M extends AnyObject | AnyArray>(model: M, schema: AbstractControlSchema): unknown {\n let value: unknown;\n // If the value read from the model is undefined, it means the model did\n // not provide a value, so take the default value from the schema here.\n // If it is a multi-key schema, the values of these keys need to be retrieved\n // separately from the model and combined into an array.\n if (this.schemaUtil.isMultiKeySchema(schema)) {\n value = (schema.key as string[]).map((key, index) => {\n const val = model[key as keyof M];\n return isUndefined(val) ? schema.defaultValue?.[index] ?? null : val;\n });\n // If all elements in the array are `null`, assign `null` directly.\n if ((value as []).every(o => o === null)) {\n value = null;\n }\n } else if (this.schemaUtil.isPathKeySchema(schema)) {\n const paths = this.schemaUtil.parsePathKey(schema.key as string);\n value = paths.reduce((obj, key) => obj?.[key as keyof M] as M, model);\n } else {\n value = model[schema.key as keyof M];\n }\n\n if (isUndefined(value)) {\n value = schema.defaultValue ?? null;\n }\n\n if (schema.mapper) {\n return schema.mapper.parser(value, schema);\n }\n\n return value;\n }\n\n valueOfControl(control: AbstractControl, schema: AbstractControlSchema): unknown {\n const value = control.value;\n\n if (schema.mapper) {\n return schema.mapper.formatter(value, schema);\n }\n\n return value;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { AbstractControl, type AbstractControlOptions, FormArray, FormControl, FormGroup, type ValidatorFn, Validators } from '@angular/forms';\nimport type { AnyArray, AnyObject, SafeAny } from '@ngify/core';\nimport type { AbstractControlSchema, AbstractFormArraySchema, AbstractFormGroupSchema, AbstractSchema, SchemaKey } from '../schemas';\nimport { ValueTransformer } from '../services/value-transformer.service';\nimport type { Indexable } from '../types';\nimport { isArray, isNumber, isUndefined } from './is.utils';\nimport { SchemaUtil } from './schema.utils';\nimport { ValueUtil } from './value.utils';\n\ndeclare const ngDevMode: boolean | undefined;\n\n@Injectable({\n providedIn: 'root'\n})\nexport class FormUtil {\n private readonly schemaUtil = inject(SchemaUtil);\n private readonly valueUtil = inject(ValueUtil);\n private readonly valueTransformer = inject(ValueTransformer);\n\n createFormControl(schema: AbstractControlSchema, model: AnyObject): FormControl {\n const validators: ValidatorFn[] = this.schemaUtil.validatorsOf(schema);\n const value = this.valueUtil.valueOfModel(model, schema) ?? schema.defaultValue;\n\n return new FormControl(value, {\n nonNullable: !isUndefined(schema.defaultValue),\n validators: schema.validators ? validators.concat(schema.validators) : validators,\n asyncValidators: schema.asyncValidators,\n updateOn: schema.config?.updateOn\n });\n }\n\n createFormGroup(schema: AbstractFormGroupSchema, model: AnyObject): FormGroup;\n createFormGroup(schemas: Indexable<AbstractSchema>[], model: AnyObject): FormGroup;\n createFormGroup(schemaOrSchemas: AbstractFormGroupSchema | Indexable<AbstractSchema>[], model: AnyObject): FormGroup;\n createFormGroup(schemaOrSchemas: AbstractFormGroupSchema | Indexable<AbstractSchema>[], model: AnyObject): FormGroup {\n let schemas: Indexable<AbstractSchema>[];\n let options: AbstractControlOptions = {};\n\n if (isArray(schemaOrSchemas)) {\n schemas = schemaOrSchemas;\n } else {\n schemas = schemaOrSchemas.schemas;\n options = {\n validators: schemaOrSchemas.validators,\n asyncValidators: schemaOrSchemas.asyncValidators,\n updateOn: schemaOrSchemas.config?.updateOn\n };\n }\n\n return new FormGroup(\n this.createControlMap(schemas, model),\n options\n );\n }\n\n private createControlMap(schemas: Indexable<AbstractSchema>[], model: AnyObject) {\n return schemas.reduce((controls, schema) => {\n if (this.schemaUtil.isControlGroup(schema)) {\n const key = schema.key!.toString();\n controls[key] = this.createFormGroup(schema, model[key] ?? {});\n } else if (this.schemaUtil.isControlArray(schema)) {\n const key = schema.key!.toString();\n controls[key] = this.createFormArray(schema, model[key] ?? []);\n } else if (this.schemaUtil.isControlWrapper(schema) || this.schemaUtil.isComponentContainer(schema)) {\n Object.assign(controls, this.createControlMap(schema.schemas, model));\n } else if (this.schemaUtil.isControl(schema)) {\n if (this.schemaUtil.isPathKeySchema(schema)) {\n const paths = this.schemaUtil.parsePathKey(schema.key as string);\n const key = paths.pop()!;\n\n const parent: FormGroup = paths.reduce((previousGroup, path) => {\n let group = previousGroup.get(path) as FormGroup;\n\n if (!group) {\n group = new FormGroup({});\n previousGroup.addControl(path, group);\n }\n\n return group;\n }, (controls[paths.shift()!] ??= new FormGroup({})) as FormGroup);\n parent.addControl(key, this.createFormControl(schema, model));\n } else {\n controls[schema.key!.toString()] = this.createFormControl(schema, model);\n }\n }\n\n return controls;\n }, {} as Record<string, AbstractControl>);\n }\n\n createFormArray(schema: AbstractFormArraySchema, model: AnyArray): FormArray {\n const controls = this.createFormArrayElements(schema.schemas, model);\n const validators: ValidatorFn[] = schema.validators ?? [];\n\n if (schema.length) {\n validators.push(Validators.required);\n if (isNumber(schema.length)) {\n validators.push(\n Validators.minLength(schema.length),\n Validators.maxLength(schema.length)\n );\n } else {\n if (schema.length.min) {\n validators.push(Validators.minLength(schema.length.min));\n }\n if (schema.length.max) {\n validators.push(Validators.maxLength(schema.length.max));\n }\n }\n }\n\n return new FormArray<SafeAny>(controls, {\n validators,\n asyncValidators: schema.asyncValidators,\n updateOn: schema.config?.updateOn\n });\n }\n\n createFormArrayElements(schemas: Indexable<AbstractSchema>[], model: AnyArray) {\n // Only take the first one, ignore the rest.\n const [schema] = this.schemaUtil.filterControls(schemas);\n\n if (typeof ngDevMode !== 'undefined' && ngDevMode && !schema) {\n throw Error('FormArray element control schema not found');\n }\n\n return model.map((_, index) => {\n return this.createAnyControl({ ...schema, key: index }, model);\n });\n }\n\n createAnyControl(schema: AbstractControlSchema, model: AnyObject | AnyArray): AbstractControl {\n if (this.schemaUtil.isControlGroup(schema)) {\n return this.createFormGroup(schema, (model as AnyObject)[schema.key!] ?? {});\n }\n\n if (this.schemaUtil.isControlArray(schema)) {\n return this.createFormArray(schema, (model as AnyArray)[schema.key as number] ?? []);\n }\n\n return this.createFormControl(schema, model);\n }\n\n /**\n * Update the form state, currently includes:\n * - Updating validators\n * - Updating status - enabled / disabled\n * - Updating value\n * @param form\n * @param model\n * @param schemas\n */\n updateForm(form: FormGroup, model: AnyObject, schemas: AbstractSchema[]): void;\n updateForm(form: FormArray, model: AnyArray, schemas: AbstractSchema[]): void;\n updateForm(form: FormGroup | FormArray, model: AnyObject, schemas: AbstractSchema[]): void {\n for (const schema of schemas) {\n if (this.schemaUtil.isControlGroup(schema)) {\n const key = schema.key!;\n const formGroup = getChildControl(form, key) as FormGroup;\n\n this.updateForm(formGroup, model[key], schema.schemas);\n continue;\n }\n\n if (this.schemaUtil.isControlArray(schema)) {\n const key = schema.key!;\n const formArray = getChildControl(form, key) as FormArray;\n const [elementSchema] = this.schemaUtil.filterControls(schema.schemas);\n const elementSchemas = formArray.controls.map((_, index) => ({ ...elementSchema, key: index }));\n\n this.updateForm(formArray, model[schema.key!], elementSchemas);\n continue;\n }\n\n if (this.schemaUtil.isControlWrapper(schema) || this.schemaUtil.isComponentContainer(schema)) {\n this.updateForm(form as FormGroup, model, schema.schemas);\n continue;\n }\n\n if (this.schemaUtil.isControl(schema)) {\n const control = getChildControl(form, schema.key!)!;\n\n if (schema.value) {\n const value = this.valueTransformer.transform(schema.value, { model, schema, control });\n control.setValue(value, { onlySelf: true });\n }\n // update disabled\n const disabled = this.valueTransformer.transform(schema.disabled, { model, schema, control });\n if (control.enabled !== !disabled) { // Update only if inconsistent\n if (disabled) {\n // Using `onlySelf: true` here instead of `emitEvent: false` because the\n // control's own value/statusChanges events need to trigger properly.\n control.disable({ onlySelf: true });\n } else {\n control.enable({ onlySelf: true });\n }\n }\n // update required validator\n const required = this.valueTransformer.transform(schema.required, { model, schema, control });\n if (required) {\n control.addValidators(Validators.required);\n } else {\n control.removeValidators(Validators.required);\n }\n\n control.updateValueAndValidity({ emitEvent: false });\n }\n }\n }\n\n /**\n * Assign value from form to model\n * @param model\n * @param form\n * @param schemas\n */\n updateModel(model: AnyObject, form: FormGroup, schemas: AbstractSchema[]): AnyObject;\n updateModel(model: AnyArray, form: FormArray, schemas: AbstractSchema[]): AnyArray;\n updateModel(model: AnyObject, form: FormGroup | FormArray, schemas: AbstractSchema[]): AnyObject | AnyArray {\n for (const schema of schemas) {\n if (this.schemaUtil.isControlGroup(schema)) {\n const key = schema.key!;\n const formGroup = getChildControl(form, key) as FormGroup;\n\n this.updateModel(model[key] = {}, formGroup, schema.schemas);\n continue;\n }\n\n if (this.schemaUtil.isControlArray(schema)) {\n const key = schema.key!;\n const formArray = getChildControl(form, key) as FormArray;\n const [elementSchema] = this.schemaUtil.filterControls(schema.schemas);\n const elementSchemas = formArray.controls.map((_, index) => ({ ...elementSchema, key: index }));\n\n this.updateModel(model[key] = [], formArray, elementSchemas);\n continue;\n }\n\n if (this.schemaUtil.isControlWrapper(schema) || this.schemaUtil.isComponentContainer(schema)) {\n this.updateModel(model, form as FormGroup, schema.schemas);\n continue;\n }\n\n if (this.schemaUtil.isControl(schema)) {\n const key = schema.key!.toString();\n const control = getChildControl(form, key)!;\n\n if (!schema.config?.valueCollectionStrategy || schema.config.valueCollectionStrategy === 'value') {\n const disabled = this.valueTransformer.transform(schema.disabled, { model, schema, control });\n // skip disabled controls\n if (disabled) continue;\n }\n\n const value = this.valueUtil.valueOfControl(control, schema);\n\n // Multi-field case.\n if (this.schemaUtil.isMultiKeySchema(schema)) {\n (schema.key as string[]).map((prop, idx) => {\n model[prop] = (value as [unknown, unknown])?.[idx] ?? null;\n });\n } else if (this.schemaUtil.isPathKeySchema(schema)) {\n const paths = this.schemaUtil.parsePathKey(schema.key as string);\n let _model = model;\n for (let i = 0; i < paths.length - 1; i++) {\n const path = paths[i];\n _model = _model[path] ??= {};\n }\n _model[paths.pop()!] = value;\n } else {\n model[key] = value;\n }\n }\n }\n\n return model;\n }\n}\n\n/**\n * Automatically choose to use `.get()` or `.at()` to get a child control\n * based on the type of the form.\n * @param form\n * @param key\n * @returns\n */\nexport function getChildControl(form: FormGroup | FormArray, key: SchemaKey): AbstractControl | null {\n return form instanceof FormArray ? form.at(key as number) : form.get(key.toString() as string);\n}\n","import { inject, Injectable } from '@angular/core';\nimport { FormArray, FormGroup } from '@angular/forms';\nimport type { AnyArray, AnyObject } from '@ngify/core';\nimport type { AbstractSchema } from '../schemas';\nimport type { Indexable } from '../types';\nimport { FormUtil, getChildControl } from './form.utils';\nimport { SchemaUtil } from './schema.utils';\nimport { ValueUtil } from './value.utils';\n\n/**\n * @internal\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class ModelUtil {\n private readonly schemaUtil = inject(SchemaUtil);\n private readonly valueUtil = inject(ValueUtil);\n private readonly formUtil = inject(FormUtil);\n\n /**\n * 从 model 赋值到 form\n * @param form\n * @param model\n * @param schemas\n * @param completed\n */\n updateForm(form: FormGroup, model: AnyObject, schemas: Indexable<AbstractSchema>[], completed?: boolean): FormGroup;\n updateForm(form: FormArray, model: AnyArray, schemas: Indexable<AbstractSchema>[], completed?: boolean): FormArray;\n updateForm(form: FormGroup | FormArray, model: AnyObject, schemas: Indexable<AbstractSchema>[], completed = true): FormGroup | FormArray {\n for (const schema of schemas) {\n if (this.schemaUtil.isControlGroup(schema)) {\n const key = schema.key!;\n const formGroup = getChildControl(form, key) as FormGroup;\n\n this.updateForm(formGroup, model[key] ??= {}, schema.schemas, false);\n continue;\n }\n\n if (this.schemaUtil.isControlArray(schema)) {\n const key = schema.key!;\n const array: AnyArray = model[key] ??= [];\n const formArray = getChildControl(form, key) as FormArray;\n\n // / If the model array length matches the form array length, update the\n // form values in place; otherwise, rebuild the form array.\n if (array.length === formArray.length) {\n const [elementSchema] = this.schemaUtil.filterControls(schema.schemas);\n const elementSchemas = array.map((_, index) => ({ ...elementSchema, key: index }));\n\n this.updateForm(formArray, array, elementSchemas, false);\n } else {\n const controls = this.formUtil.createFormArrayElements(schema.schemas, array);\n\n formArray.clear({ emitEvent: false });\n for (const control of controls) {\n formArray.push(control, { emitEvent: false });\n }\n formArray.updateValueAndValidity({ onlySelf: true });\n }\n continue;\n }\n\n if (this.schemaUtil.isComponentContainer(schema) || this.schemaUtil.isControlWrapper(schema)) {\n this.updateForm(form as FormGroup, model, schema.schemas, false);\n continue;\n }\n\n if (this.schemaUtil.isControl(schema)) {\n const value = this.valueUtil.valueOfModel(model, schema);\n const control = getChildControl(form, schema.key!)!;\n\n control.setValue(value, { onlySelf: true });\n }\n }\n\n // Only disable `onlySelf` after all child updates are complete.\n form.updateValueAndValidity({ onlySelf: !completed });\n\n return form;\n }\n}\n","import { Component, Directive, ViewEncapsulation, computed, input } from '@angular/core';\nimport { Breakpoints, createBreakpointInfix } from '../../../breakpoints';\nimport { withStyle } from '../../../style';\nimport type { Stringify } from '../../../types';\nimport { isObject } from '../../../utils';\n\ntype Cell = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;\ntype Span = Cell | 'fill' | null;\ntype Offset = Cell | 'auto' | null;\n\n@Component({\n selector: 'fluent-col',\n template: '',\n styleUrl: './col.component.scss',\n encapsulation: ViewEncapsulation.None\n})\nclass FluentColComponent { }\n\n@Directive({\n selector: 'fluent-col,[fluentCol]',\n exportAs: 'fluentCol',\n host: {\n class: 'fluent-column',\n '[class]': 'classes()',\n '[style.flex]': 'flex()'\n }\n})\nexport class FluentColDirective {\n readonly span = input<Span | Stringify<Span> | Partial<Record<keyof Breakpoints, Span>>>();\n readonly flex = input<string | number | null>();\n readonly offset = input<Offset | Stringify<Offset> | Partial<Record<keyof Breakpoints, Offset>>>();\n\n protected readonly classes = computed(() => {\n const span = this.span();\n const offset = this.offset();\n const classes: string[] = [];\n\n if (span) {\n if (isObject(span)) {\n for (const [breakpoint, _span] of Object.entries(span)) {\n classes.push(columnClassOf(_span, breakpoint as keyof Breakpoints));\n }\n } else {\n classes.push(columnClassOf(span));\n }\n }\n\n if (offset) {\n if (isObject(offset)) {\n for (const [breakpoint, _offset] of Object.entries(offset)) {\n classes.push(columnOffsetClassOf(_offset, breakpoint as keyof Breakpoints));\n }\n } else {\n classes.push(columnOffsetClassOf(offset));\n }\n }\n\n return classes;\n });\n\n constructor() {\n withStyle(FluentColComponent);\n }\n}\n\nfunction columnClassOf(span: Span | Stringify<Span>, breakpoint?: keyof Breakpoints) {\n let clazz = `fluent-column`;\n\n if (breakpoint) {\n clazz += createBreakpointInfix(breakpoint);\n }\n\n if (span) {\n clazz += `-${span}`;\n }\n\n return clazz;\n}\n\nfunction columnOffsetClassOf(offset: Offset | Stringify<Offset>, breakpoint?: keyof Breakpoints) {\n let clazz = `fluent-column-offset`;\n\n if (breakpoint) {\n clazz += createBreakpointInfix(breakpoint);\n }\n\n clazz += `-${offset}`;\n\n return clazz;\n}\n","import { BreakpointObserver } from '@angular/cdk/layout';\nimport { Component, Directive, ViewEncapsulation, computed, inject, input } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { map } from 'rxjs';\nimport { Breakpoints } from '../../../breakpoints';\nimport { withStyle } from '../../../style';\nimport type { Stringify } from '../../../types';\nimport { isArray, isNumber, isString } from '../../../utils';\n\ntype Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6;\ntype Justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between' | 'space-evenly';\ntype Align = 'start' | 'end' | 'center';\n\nconst SPACER = 16;\nconst GAPS = {\n 0: 0,\n 1: SPACER * 0.25, // 4\n 2: SPACER * 0.5, // 8\n 3: SPACER, // 16\n 4: SPACER * 1.5, // 24\n 5: SPACER * 2, // 32\n 6: SPACER * 3 // 48\n};\nconst DEFAULT_GAP: Partial<Record<keyof Breakpoints, Gap | [x: Gap, y: Gap]>> = { xs: 1, sm: 2, md: 3, lg: 4, xl: 5 };\n\nconst BREAKPOINTS: string[] = [\n Breakpoints.xs,\n Breakpoints.sm,\n Breakpoints.md,\n Breakpoints.lg,\n Breakpoints.xl,\n Breakpoints.xxl\n];\n\n@Component({\n selector: 'fluent-row',\n template: '',\n styleUrl: './row.component.scss',\n encapsulation: ViewEncapsulation.None\n})\nclass FluentRowComponent { }\n\n@Directive({\n selector: 'fluent-row,[fluentRow]',\n exportAs: 'fluentRow',\n host: {\n class: 'fluent-row',\n '[style.--gap-x.px]': 'gapPx()[0]',\n '[style.--gap-y.px]': 'gapPx()[1]',\n '[style.justify-content]': 'justifyContent()',\n '[style.align-items]': 'alignItems()'\n }\n})\nexport class FluentRowDirective {\n readonly gap = input<Gap | Stringify<Gap> | [x: Gap, y: Gap] | Partial<Record<keyof Breakpoints, Gap | [x: Gap, y: Gap]>> | null>();\n readonly justify = input<Justify | null>();\n readonly align = input<Align | null>();\n /** Currently matched breakpoints. */\n private readonly breakpoints = toSignal(\n inject(BreakpointObserver).observe(BREAKPOINTS).pipe(\n map(state =>\n Object.keys(state.breakpoints)\n .filter(breakpoint => state.breakpoints[breakpoint])\n .sort((a, b) => BREAKPOINTS.indexOf(b) - BREAKPOINTS.indexOf(a)))\n ),\n { initialValue: [] }\n );\n\n protected readonly gapPx = computed(() => {\n if (this.gap() === null) {\n return getGapPixel(0);\n }\n\n const breakpoints = this.breakpoints();\n const gap = this.gap() ?? DEFAULT_GAP;\n\n if (isArray(gap) || isNumber(gap) || isString(gap)) {\n return getGapPixel(gap);\n }\n\n for (const breakpointValue of breakpoints) {\n for (const breakpointName of Object.keys(gap)) {\n if (Breakpoints[breakpointName as keyof Breakpoints] === breakpointValue) {\n return getGapPixel(gap[breakpointName as keyof Breakpoints]!);\n }\n }\n }\n\n return getGapPixel(0);\n });\n\n protected readonly justifyContent = computed(() => parseJustifyOrAlign(this.justify()));\n protected readonly alignItems = computed(() => parseJustifyOrAlign(this.align()));\n\n constructor() {\n withStyle(FluentRowComponent);\n }\n}\n\nfunction getGapPixel(level: Gap | Stringify<Gap> | [x: Gap, y: Gap]): [number, number?] {\n if (isArray(level)) {\n return [GAPS[level[0]], GAPS[level[1]]];\n }\n\n return [GAPS[level], 0];\n}\n\nfunction parseJustifyOrAlign(value?: Justify | Align | null) {\n if (value === 'start' || value === 'end') {\n return `flex-${value}`;\n }\n\n return value;\n}\n","import { NgModule } from '@angular/core';\nimport { FluentColDirective } from './col/col.component';\nimport { FluentRowDirective } from './row/row.component';\n\n@NgModule({\n imports: [FluentRowDirective, FluentColDirective],\n exports: [FluentRowDirective, FluentColDirective]\n})\nexport class FluentGridModule { }\n","import { type Signal } from '@angular/core';\nimport { FormGroup } from '@angular/forms';\nimport type { AnyArray, AnyObject } from '@ngify/core';\nimport type { AbstractControlContainerSchema } from '../../../schemas';\n\nexport abstract class FluentControlContainer<T extends AnyObject | AnyArray> {\n abstract readonly schema: Signal<AbstractControlContainerSchema>;\n abstract readonly patchedSchema: Signal<AbstractControlContainerSchema>;\n abstract readonly form: Signal<FormGroup>;\n abstract readonly model: Signal<T>;\n}\n","import {\n computed,\n DestroyRef,\n Directive,\n effect,\n forwardRef,\n HostListener,\n inject,\n input,\n model,\n output,\n untracked\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { type FormControlStatus, FormGroup } from '@angular/forms';\nimport type { AnyArray, AnyObject } from '@ngify/core';\nimport type { AbstractFormGroupSchema } from '../../schemas';\nimport { NAMED_TEMPLATES } from '../../tokens';\nimport { FormUtil, ModelUtil, SchemaUtil } from '../../utils';\nimport { FluentControlContainer } from './models/control-container';\n\n@Directive({\n selector: '[fluentSchema]',\n exportAs: 'fluentForm',\n providers: [\n {\n provide: FluentControlContainer,\n useExisting: forwardRef(() => FluentFormDirective)\n },\n {\n provide: NAMED_TEMPLATES,\n useFactory: () => []\n }\n ]\n})\nexport class FluentFormDirective<T extends AnyObject | AnyArray> implements FluentControlContainer<T> {\n private readonly formUtil = inject(FormUtil);\n private readonly schemaUtil = inject(SchemaUtil);\n private readonly modelUtil = inject(ModelUtil);\n private internalModel!: T;\n\n readonly schema = input.required<AbstractFormGroupSchema>({ alias: 'fluentSchema' });\n readonly model = model.required<T>({ alias: 'fluentModel' });\n\n readonly patchedSchema = computed(() => this.schemaUtil.patch(this.schema()));\n readonly form = computed<FormGroup>(() =>\n this.formUtil.createFormGroup(this.patchedSchema(), untracked(() => this.model())));\n\n readonly formChange = output<FormGroup>({ alias: 'fluentFormChange' });\n readonly valueChanges = output<T>({ alias: 'fluentValueChanges' });\n readonly statusChanges = output<FormControlStatus>({ alias: 'fluentStatusChanges' });\n /** The submit event will only be triggered when the host element is a form element */\n // eslint-disable-next-line @angular-eslint/no-output-nat