ng-dynamic-json-form
Version:
Generate Angular reactive form dynamically using JSON. Support conditional rendering and toggle of validators.
999 lines (976 loc) • 319 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, Component, inject, ElementRef, input, computed, effect, Directive, InjectionToken, Injector, ChangeDetectorRef, isSignal, SimpleChange, HostListener, Pipe, Injectable, PLATFORM_ID, viewChild, ViewContainerRef, untracked, DestroyRef, afterNextRender, isDevMode, LOCALE_ID, forwardRef, viewChildren, output, ChangeDetectionStrategy } from '@angular/core';
import * as i2 from '@angular/forms';
import { isFormControl, isFormGroup, FormControl, Validators, UntypedFormGroup, FormArray, ReactiveFormsModule, FormGroup, UntypedFormControl, NG_VALUE_ACCESSOR, NG_VALIDATORS, NgControl, NG_ASYNC_VALIDATORS } from '@angular/forms';
import { IMaskDirective } from 'angular-imask';
import * as i1 from '@angular/common';
import { isPlatformServer, CommonModule, formatDate } from '@angular/common';
import { rxResource, takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop';
import { BehaviorSubject, Subject, EMPTY, merge, fromEvent, tap, of, startWith, map, filter, takeWhile, from, mergeMap, Observable, finalize, catchError, takeUntil, combineLatest, switchMap, distinctUntilChanged, debounceTime, take } from 'rxjs';
import IMask from 'imask/esm/index';
import { HttpClient } from '@angular/common/http';
/**Get all the errors in the AbstractControl recursively
* @example
* root: {
* control1: ValidationErrors,
* control2: {
* childA: ValidationErrors
* }
* }
*/
function getControlErrors(control) {
if (!control) {
return null;
}
if (isFormControl(control)) {
return control.errors;
}
if (isFormGroup(control)) {
const result = Object.keys(control.controls).reduce((acc, key) => {
const childControl = control.controls[key];
const err = getControlErrors(childControl);
return err ? { ...acc, [key]: err } : acc;
}, {});
return !Object.keys(result).length ? null : result;
}
return null;
}
class CustomControlComponent {
constructor() {
this.hostForm = signal(undefined, ...(ngDevMode ? [{ debugName: "hostForm" }] : []));
this.data = signal(undefined, ...(ngDevMode ? [{ debugName: "data" }] : []));
this.hideErrorMessage = signal(undefined, ...(ngDevMode ? [{ debugName: "hideErrorMessage" }] : []));
}
writeValue(obj) {
this.control?.patchValue(obj);
}
registerOnChange(fn) { }
registerOnTouched(fn) { }
setDisabledState(isDisabled) {
isDisabled ? this.control?.disable() : this.control?.enable();
}
validate(control) {
return getControlErrors(this.control);
}
markAsDirty() { }
markAsPristine() { }
markAsTouched() { }
markAsUntouched() { }
setErrors(errors) { }
onOptionsGet(options) {
const data = this.data();
if (!data || !data.options) {
return;
}
this.data.update((x) => {
if (!x?.options) {
return x;
}
x.options.data = [...options];
return { ...x };
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CustomControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.3", type: CustomControlComponent, isStandalone: true, selector: "custom-control", ngImport: i0, template: ``, isInline: true }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CustomControlComponent, decorators: [{
type: Component,
args: [{
selector: 'custom-control',
template: ``,
standalone: true,
}]
}] });
class CustomErrorMessage {
constructor() {
this.control = new FormControl();
this.errorMessages = signal([], ...(ngDevMode ? [{ debugName: "errorMessages" }] : []));
}
}
class CustomFormLabel {
constructor() {
this.collapsible = signal(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : []));
this.expand = signal(false, ...(ngDevMode ? [{ debugName: "expand" }] : []));
this.label = signal(undefined, ...(ngDevMode ? [{ debugName: "label" }] : []));
this.layout = signal(undefined, ...(ngDevMode ? [{ debugName: "layout" }] : []));
this.props = signal(undefined, ...(ngDevMode ? [{ debugName: "props" }] : []));
}
}
function getClassListFromString(classNames) {
if (!classNames) {
return [];
}
try {
return classNames
.split(/\s{1,}/)
.map((x) => x.trim())
.filter(Boolean);
}
catch {
return [];
}
}
function getStyleListFromString(styles) {
if (!styles) {
return [];
}
try {
return styles
.split(';')
.map((x) => x.trim())
.filter(Boolean);
}
catch {
return [];
}
}
class ControlLayoutDirective {
constructor() {
this.el = inject(ElementRef);
this.controlLayout = input(...(ngDevMode ? [undefined, { debugName: "controlLayout" }] : []));
this.classNames = computed(() => {
const data = this.controlLayout();
if (!data) {
return [];
}
const { type, layout } = data;
const classString = layout?.[`${type ?? 'host'}Class`] ?? '';
const result = getClassListFromString(classString);
return result;
}, ...(ngDevMode ? [{ debugName: "classNames" }] : []));
this.styleList = computed(() => {
const data = this.controlLayout();
if (!data) {
return [];
}
const { type, layout } = data;
const styleString = layout?.[`${type ?? 'host'}Styles`] ?? '';
const result = getStyleListFromString(styleString);
return result;
}, ...(ngDevMode ? [{ debugName: "styleList" }] : []));
this.updateClass = effect(() => {
const host = this.el.nativeElement;
const classNames = this.classNames();
if (!classNames.length) {
return;
}
host.classList.add(...classNames);
}, ...(ngDevMode ? [{ debugName: "updateClass" }] : []));
this.updateStyles = effect(() => {
const host = this.el.nativeElement;
const styleList = this.styleList();
if (!styleList.length) {
return;
}
for (const item of styleList) {
const [name, value] = item.split(':').map((x) => x.trim());
host.style.setProperty(name, value);
}
}, ...(ngDevMode ? [{ debugName: "updateStyles" }] : []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ControlLayoutDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.1.3", type: ControlLayoutDirective, isStandalone: true, selector: "[controlLayout]", inputs: { controlLayout: { classPropertyName: "controlLayout", publicName: "controlLayout", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ControlLayoutDirective, decorators: [{
type: Directive,
args: [{
selector: '[controlLayout]',
standalone: true,
}]
}] });
class HostIdDirective {
constructor() {
this.el = inject(ElementRef);
this.hostId = input(...(ngDevMode ? [undefined, { debugName: "hostId" }] : []));
this.computedId = computed(() => {
const hostId = this.hostId();
if (!hostId) {
return;
}
const { parentId, controlName } = hostId;
return parentId ? `${parentId}.${controlName}` : controlName;
}, ...(ngDevMode ? [{ debugName: "computedId" }] : []));
this.updateAttribute = effect(() => {
const id = this.computedId();
const host = this.el.nativeElement;
if (id) {
host.setAttribute('id', id);
}
}, ...(ngDevMode ? [{ debugName: "updateAttribute" }] : []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostIdDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.1.3", type: HostIdDirective, isStandalone: true, selector: "[hostId]", inputs: { hostId: { classPropertyName: "hostId", publicName: "hostId", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostIdDirective, decorators: [{
type: Directive,
args: [{
selector: '[hostId]',
standalone: true,
}]
}] });
class ImaskValuePatchDirective {
constructor() {
this.imask = inject(IMaskDirective);
this.isNumber = false;
const iMask = this.imask;
iMask.writeValue = (value) => {
// ----- Modified area -----
if (!this.isNumber) {
this.isNumber = typeof value === 'number';
}
value = value == null && iMask.unmask !== 'typed' ? '' : `${value}`;
// ----- Modified area -----
if (iMask.maskRef) {
iMask.beginWrite(value);
iMask.maskValue = value;
iMask.endWrite();
}
else {
iMask['_renderer'].setProperty(iMask.element, 'value', value);
iMask['_initialValue'] = value;
}
};
iMask['_onAccept'] = () => {
// ----- Modified area -----
const valueParsed = this.isNumber
? parseFloat(iMask.maskValue)
: iMask.maskValue;
const value = isNaN(valueParsed) ? null : valueParsed;
// ----- Modified area -----
// if value was not changed during writing don't fire events
// for details see https://github.com/uNmAnNeR/imaskjs/issues/136
if (iMask['_writing'] && value === iMask.endWrite())
return;
iMask.onChange(value);
iMask.accept.emit(value);
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImaskValuePatchDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.1.3", type: ImaskValuePatchDirective, isStandalone: true, selector: "[imaskValuePatch]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImaskValuePatchDirective, decorators: [{
type: Directive,
args: [{
selector: '[imaskValuePatch]',
standalone: true,
}]
}], ctorParameters: () => [] });
const PROPS_BINDING_INJECTORS = new InjectionToken('property-binding-injector');
function providePropsBinding(value) {
return {
provide: PROPS_BINDING_INJECTORS,
useValue: value,
};
}
class PropsBindingDirective {
constructor() {
this.injectionTokens = inject(PROPS_BINDING_INJECTORS, {
optional: true,
});
this.injector = inject(Injector);
this.cd = inject(ChangeDetectorRef);
this.el = inject(ElementRef);
this.propsBinding = input([], ...(ngDevMode ? [{ debugName: "propsBinding" }] : []));
this.validPropsData = computed(() => this.propsBinding().filter((x) => Boolean(x) &&
typeof x.props === 'object' &&
Object.keys(x.props).length > 0), ...(ngDevMode ? [{ debugName: "validPropsData" }] : []));
this.handlePropsGet = effect(() => {
const propsData = this.validPropsData();
if (!propsData.length) {
return;
}
const hostEl = this.el.nativeElement;
for (const item of propsData) {
const { props, key, omit = [] } = item;
const providerToken = this.injectionTokens?.find((x) => x.key === key)?.token;
const component = !providerToken
? null
: this.injector.get(providerToken);
for (const key in props) {
const value = props[key];
if (value == undefined || omit.includes(key)) {
continue;
}
if (component) {
this.updateComponentProperty({ component, key, value });
}
if (hostEl) {
// Only set CSS custom properties (starting with --) or valid HTML attributes
if (key.startsWith('--')) {
hostEl.style.setProperty(key, value);
}
else if (this.isValidHtmlAttribute(key)) {
hostEl.setAttribute(key, value);
}
}
}
}
this.cd.markForCheck();
this.cd.detectChanges();
}, ...(ngDevMode ? [{ debugName: "handlePropsGet" }] : []));
}
updateComponentProperty(data) {
const { component, key, value } = data;
const hasProperty = this.hasProperty(component, key);
if (!hasProperty) {
return;
}
const property = component[key];
if (isSignal(property)) {
if (typeof property.set === 'function') {
property.set(value);
}
}
else {
component[key] = value;
}
// For compatibility
if (component['ngOnChanges']) {
const simpleChange = new SimpleChange(undefined, value, true);
component.ngOnChanges({ [key]: simpleChange });
}
}
hasProperty(obj, key) {
return Object.hasOwn(obj, key) || key in obj;
}
isValidHtmlAttribute(attributeName) {
// Common HTML attributes - this is not exhaustive but covers most use cases
const validAttributes = new Set([
'id',
'class',
'style',
'title',
'lang',
'dir',
'hidden',
'tabindex',
'accesskey',
'contenteditable',
'draggable',
'spellcheck',
'translate',
'role',
'aria-label',
'aria-labelledby',
'aria-describedby',
'aria-hidden',
'aria-expanded',
'aria-selected',
'aria-checked',
'aria-disabled',
'data-testid',
'disabled',
'readonly',
'required',
'placeholder',
'value',
'checked',
'selected',
'multiple',
'size',
'rows',
'cols',
'min',
'max',
'step',
'pattern',
'minlength',
'maxlength',
'src',
'alt',
'href',
'target',
'rel',
'type',
'name',
'for',
]);
// Allow data-* and aria-* attributes
return (validAttributes.has(attributeName.toLowerCase()) ||
attributeName.startsWith('data-') ||
attributeName.startsWith('aria-'));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: PropsBindingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.1.3", type: PropsBindingDirective, isStandalone: true, selector: "[propsBinding]", inputs: { propsBinding: { classPropertyName: "propsBinding", publicName: "propsBinding", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: PropsBindingDirective, decorators: [{
type: Directive,
args: [{
selector: '[propsBinding]',
standalone: true,
}]
}] });
class TextareaAutHeightDirective {
constructor() {
this.el = inject(ElementRef);
this.autoResize = input(true, ...(ngDevMode ? [{ debugName: "autoResize" }] : []));
}
onInput() {
this.setHeight();
}
ngOnInit() {
this.removeResizeProperty();
}
removeResizeProperty() {
const hostEl = this.el.nativeElement;
hostEl.style.setProperty('resize', 'none');
}
setHeight() {
const autoResize = this.autoResize();
const hostEl = this.el.nativeElement;
if (!hostEl || !autoResize) {
return;
}
const borderWidth = Math.ceil(parseFloat(window.getComputedStyle(hostEl).borderWidth));
hostEl.style.removeProperty('height');
hostEl.style.setProperty('height', `${hostEl.scrollHeight + borderWidth * 2}px`);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: TextareaAutHeightDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.1.3", type: TextareaAutHeightDirective, isStandalone: true, selector: "[textareaAutoHeight]", inputs: { autoResize: { classPropertyName: "autoResize", publicName: "autoResize", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "input": "onInput($event)" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: TextareaAutHeightDirective, decorators: [{
type: Directive,
args: [{
selector: '[textareaAutoHeight]',
standalone: true,
}]
}], propDecorators: { onInput: [{
type: HostListener,
args: ['input', ['$event']]
}] } });
var ValidatorsEnum;
(function (ValidatorsEnum) {
ValidatorsEnum["required"] = "required";
ValidatorsEnum["requiredTrue"] = "requiredTrue";
ValidatorsEnum["min"] = "min";
ValidatorsEnum["max"] = "max";
ValidatorsEnum["minLength"] = "minLength";
ValidatorsEnum["maxLength"] = "maxLength";
ValidatorsEnum["email"] = "email";
ValidatorsEnum["pattern"] = "pattern";
})(ValidatorsEnum || (ValidatorsEnum = {}));
var ConditionsActionEnum;
(function (ConditionsActionEnum) {
ConditionsActionEnum["control.hidden"] = "control.hidden";
ConditionsActionEnum["control.disabled"] = "control.disabled";
ConditionsActionEnum["validator.required"] = "validator.required";
ConditionsActionEnum["validator.requiredTrue"] = "validator.requiredTrue";
ConditionsActionEnum["validator.min"] = "validator.min";
ConditionsActionEnum["validator.max"] = "validator.max";
ConditionsActionEnum["validator.minLength"] = "validator.minLength";
ConditionsActionEnum["validator.maxLength"] = "validator.maxLength";
ConditionsActionEnum["validator.email"] = "validator.email";
ConditionsActionEnum["validator.pattern"] = "validator.pattern";
})(ConditionsActionEnum || (ConditionsActionEnum = {}));
class ControlTypeByConfigPipe {
transform(config) {
if (!config.children) {
return 'FormControl';
}
return 'FormGroup';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ControlTypeByConfigPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.1.3", ngImport: i0, type: ControlTypeByConfigPipe, isStandalone: true, name: "controlTypeByConfig" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ControlTypeByConfigPipe, decorators: [{
type: Pipe,
args: [{
name: 'controlTypeByConfig',
standalone: true,
}]
}] });
class GlobalVariableService {
constructor() {
this.hideErrorMessage$ = new BehaviorSubject(undefined);
this.rootConfigs = [];
this.showErrorsOnTouched = true;
}
// ======================================================================
setup(variables) {
for (const key in variables) {
this[key] = variables[key];
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: GlobalVariableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: GlobalVariableService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: GlobalVariableService, decorators: [{
type: Injectable
}] });
class IsControlRequiredPipe {
transform(value) {
if (!value) {
return false;
}
return value.hasValidator(Validators.required);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IsControlRequiredPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.1.3", ngImport: i0, type: IsControlRequiredPipe, isStandalone: true, name: "isControlRequired" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IsControlRequiredPipe, decorators: [{
type: Pipe,
args: [{
name: 'isControlRequired',
standalone: true,
}]
}] });
class HostEventService {
constructor() {
this.platformId = inject(PLATFORM_ID);
this.focusOut$ = new Subject();
this.keyUp$ = new Subject();
this.keyDown$ = new Subject();
this.pointerUp$ = new Subject();
this.pointerDown$ = new Subject();
}
start$(hostElement) {
if (this.isServer) {
return EMPTY;
}
return merge(this.event$(hostElement, 'focusout', this.focusOut$), this.event$(hostElement, 'keyup', this.keyUp$), this.event$(hostElement, 'keydown', this.keyDown$), this.event$(hostElement, 'pointerup', this.pointerUp$), this.event$(hostElement, 'pointerdown', this.pointerDown$));
}
event$(target, name, subject) {
if (this.isServer) {
return EMPTY;
}
return fromEvent(target, name, { passive: true }).pipe(tap((x) => subject.next(x)));
}
get isServer() {
return isPlatformServer(this.platformId);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostEventService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostEventService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
function emailValidator(control) {
const emailValid = RegExp(/^[^@\s!(){}<>]+@[\w-]+(\.[A-Za-z]+)+$/).test(control.value);
if (!control.value) {
return null;
}
return emailValid ? null : { email: 'Invalid email format' };
}
const builtInValidators = (value) => ({
[ValidatorsEnum.required]: Validators.required,
[ValidatorsEnum.requiredTrue]: Validators.requiredTrue,
[ValidatorsEnum.email]: emailValidator,
[ValidatorsEnum.pattern]: Validators.pattern(value),
[ValidatorsEnum.min]: Validators.min(value),
[ValidatorsEnum.max]: Validators.max(value),
[ValidatorsEnum.minLength]: Validators.minLength(value),
[ValidatorsEnum.maxLength]: Validators.maxLength(value),
});
class FormValidationService {
constructor() {
this.globalVariableService = inject(GlobalVariableService);
}
getErrorMessages$(control, validators) {
if (!control || !validators?.length) {
return of([]);
}
return control.statusChanges.pipe(startWith(control.status), map(() => this.getErrorMessages(control.errors, control.value, validators)));
}
getValidators(input) {
if (!input || !input.length) {
return [];
}
// Remove duplicates
const filteredConfigs = [
...new Map(input.map((v) => [v.name, v])).values(),
];
const customValidators = this.globalVariableService.customValidators;
const validatorFns = filteredConfigs.map((item) => {
const { name } = item;
const value = this.getValidatorValue(item);
const builtInValidator = builtInValidators(value)[name];
const customValidator = this.getValidatorFn(item, customValidators?.[name]);
const result = customValidator ?? builtInValidator;
return result;
});
return validatorFns.filter(Boolean);
}
getAsyncValidators(input) {
if (!input || !input.length) {
return [];
}
// Remove duplicates
const filteredConfigs = [
...new Map(input.map((v) => [v.name, v])).values(),
];
const customAsyncValidators = this.globalVariableService.customAsyncValidators;
const validatorFns = filteredConfigs.map((item) => {
const validatorFn = customAsyncValidators?.[item.name];
return this.getValidatorFn(item, validatorFn);
});
return validatorFns.filter(Boolean);
}
/**Get the error messages of the control
*
* @description
* Try to get the custom error message specified in the config first,
* else use the error message in the `ValidationErrors`.
*
* When using custom validator, the custom message most likely will not working,
* it's because we are using the key in the errors to find the config message.
* Since user can define the error object, it becomes very difficult to match the config name
* with the keys in the error object.
*/
getErrorMessages(controlErrors, controlValue, validatorConfigs) {
if (!controlErrors) {
return [];
}
const errorMessage = (error) => {
return typeof error === 'string' ? error : JSON.stringify(error);
};
return Object.keys(controlErrors).reduce((acc, key) => {
const error = controlErrors[key];
const config = this.getConfigFromErrorKey({ [key]: error }, validatorConfigs);
const configMessage = config?.message;
const defaultMessage = this.globalVariableService.validationMessages?.[config?.name ?? ''];
const customMessage = (configMessage || defaultMessage)
?.replace(/{{value}}/g, controlValue || '')
.replace(/{{validatorValue}}/g, config?.value);
acc.push(customMessage || errorMessage(error));
return acc;
}, []);
}
getConfigFromErrorKey(error, configs) {
// The key mapping of the `ValidationErrors` with the `ValidatorConfig`,
// to let us get the correct message by using `name` of `ValidatorConfig`.
const valueKeyMapping = {
pattern: 'requiredPattern',
min: 'min',
max: 'max',
minLength: 'requiredLength',
maxLength: 'requiredLength',
};
const getValidatorValue = (v) => {
return typeof v !== 'number' && !isNaN(v) ? parseFloat(v) : v;
};
const [errorKey, errorValue] = Object.entries(error)[0];
const result = configs.find((item) => {
const { name, value } = item;
if (errorKey !== name.toLowerCase()) {
return false;
}
if (value === undefined || value === null || value === '') {
return true;
}
const targetKey = valueKeyMapping[name] ?? '';
const requiredValue = errorValue[targetKey];
const validatorValue = getValidatorValue(value);
return requiredValue && name === 'pattern'
? requiredValue.includes(validatorValue)
: requiredValue === validatorValue;
});
return result;
}
getValidatorValue(validatorConfig) {
const { name, value, flags } = validatorConfig;
switch (name) {
case ValidatorsEnum.pattern:
return value instanceof RegExp ? value : new RegExp(value, flags);
case ValidatorsEnum.min:
case ValidatorsEnum.max:
case ValidatorsEnum.minLength:
case ValidatorsEnum.maxLength:
try {
return typeof value !== 'number' ? parseFloat(value) : value;
}
catch {
break;
}
default:
return value;
}
}
/**
* Get validatorFn from either validatorFn or factory function that return a validatorFn.
* If it's a factory function, return the validatorFn instead.
*
* @param validatorConfig
* @param validatorFn
*/
getValidatorFn(validatorConfig, validatorFn) {
const { value } = validatorConfig;
if (!validatorFn) {
return null;
}
const result = typeof validatorFn({}) !== 'function'
? validatorFn
: validatorFn(value);
return result;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormValidationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormValidationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormValidationService, decorators: [{
type: Injectable
}] });
class ErrorMessageComponent {
constructor() {
this.internal_formValidationService = inject(FormValidationService);
this.customComponentInstance = signal(null, ...(ngDevMode ? [{ debugName: "customComponentInstance" }] : []));
this.control = input(...(ngDevMode ? [undefined, { debugName: "control" }] : []));
this.validators = input(...(ngDevMode ? [undefined, { debugName: "validators" }] : []));
this.customComponent = input(...(ngDevMode ? [undefined, { debugName: "customComponent" }] : []));
this.customTemplate = input(null, ...(ngDevMode ? [{ debugName: "customTemplate" }] : []));
this.useDefaultTemplate = computed(() => !this.customComponent() && !this.customTemplate(), ...(ngDevMode ? [{ debugName: "useDefaultTemplate" }] : []));
this.componentAnchor = viewChild.required('componentAnchor', {
read: ViewContainerRef,
});
this.errorMessages = rxResource({
params: () => {
const control = this.control();
const validators = this.validators();
return { control, validators };
},
stream: ({ params }) => {
return this.internal_formValidationService.getErrorMessages$(params.control, params.validators);
},
defaultValue: [],
});
this.updateControlErrors = effect(() => {
const messages = this.errorMessages.value();
const customComponent = this.customComponentInstance();
if (!customComponent) {
return;
}
customComponent.errorMessages.set([...messages]);
}, ...(ngDevMode ? [{ debugName: "updateControlErrors" }] : []));
this.injectComponent = effect(() => {
const control = this.control();
const customComponent = this.customComponent();
const anchor = this.componentAnchor();
if (!customComponent || !anchor || !control) {
return;
}
untracked(() => {
anchor.clear();
const componentRef = anchor.createComponent(customComponent);
componentRef.instance.control = control;
this.customComponentInstance.set(componentRef.instance);
this.injectComponent.destroy();
});
}, ...(ngDevMode ? [{ debugName: "injectComponent" }] : []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ErrorMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: ErrorMessageComponent, isStandalone: true, selector: "error-message", inputs: { control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, validators: { classPropertyName: "validators", publicName: "validators", isSignal: true, isRequired: false, transformFunction: null }, customComponent: { classPropertyName: "customComponent", publicName: "customComponent", isSignal: true, isRequired: false, transformFunction: null }, customTemplate: { classPropertyName: "customTemplate", publicName: "customTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "error-message" }, viewQueries: [{ propertyName: "componentAnchor", first: true, predicate: ["componentAnchor"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- Custom error message component -->\r\n<ng-container #componentAnchor></ng-container>\r\n<ng-container\r\n [ngTemplateOutlet]=\"customTemplate()\"\r\n [ngTemplateOutletContext]=\"{\r\n control: control(),\r\n messages: errorMessages.value(),\r\n }\"\r\n></ng-container>\r\n\r\n<!-- Default error message component -->\r\n@if (useDefaultTemplate()) {\r\n @for (error of errorMessages.value(); track $index) {\r\n <div>{{ error }}</div>\r\n }\r\n}\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ErrorMessageComponent, decorators: [{
type: Component,
args: [{ selector: 'error-message', imports: [CommonModule], host: {
class: 'error-message',
}, template: "<!-- Custom error message component -->\r\n<ng-container #componentAnchor></ng-container>\r\n<ng-container\r\n [ngTemplateOutlet]=\"customTemplate()\"\r\n [ngTemplateOutletContext]=\"{\r\n control: control(),\r\n messages: errorMessages.value(),\r\n }\"\r\n></ng-container>\r\n\r\n<!-- Default error message component -->\r\n@if (useDefaultTemplate()) {\r\n @for (error of errorMessages.value(); track $index) {\r\n <div>{{ error }}</div>\r\n }\r\n}\r\n" }]
}] });
class FormLabelComponent {
constructor() {
this.el = inject(ElementRef);
this.destroyRef = inject(DestroyRef);
this.collapsibleElCssText = '';
this.label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : []));
this.layout = input(...(ngDevMode ? [undefined, { debugName: "layout" }] : []));
this.props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : []));
this.collapsibleEl = input(...(ngDevMode ? [undefined, { debugName: "collapsibleEl" }] : []));
/**
* State that comes from the root component.
* This will overwrite the current collapsible state
*/
this.state = input(...(ngDevMode ? [undefined, { debugName: "state" }] : []));
this.customComponent = input(...(ngDevMode ? [undefined, { debugName: "customComponent" }] : []));
this.customTemplate = input(...(ngDevMode ? [undefined, { debugName: "customTemplate" }] : []));
this.componentAnchor = viewChild.required('componentAnchor', {
read: ViewContainerRef,
});
this.expand = signal(false, ...(ngDevMode ? [{ debugName: "expand" }] : []));
this.useDefaultTemplate = computed(() => !this.customComponent() && !this.customTemplate(), ...(ngDevMode ? [{ debugName: "useDefaultTemplate" }] : []));
this.isCollapsible = computed(() => {
const layout = this.layout();
if (!layout) {
return false;
}
return (layout.contentCollapsible === 'collapse' ||
layout.contentCollapsible === 'expand');
}, ...(ngDevMode ? [{ debugName: "isCollapsible" }] : []));
this.injectCustomComponent = effect(() => {
const anchor = this.componentAnchor();
const customComponent = this.customComponent();
if (!anchor || !customComponent) {
return;
}
anchor.clear();
untracked(() => {
const componentRef = anchor.createComponent(customComponent);
componentRef.instance.label.set(this.label());
componentRef.instance.layout.set(this.layout());
componentRef.instance.props.set(this.props());
componentRef.instance.collapsible.set(this.isCollapsible());
componentRef.instance.expand.set(this.expand());
this.componentRef = componentRef.instance;
this.injectCustomComponent.destroy();
});
}, ...(ngDevMode ? [{ debugName: "injectCustomComponent" }] : []));
this.handleStateChange = effect(() => {
const isCollapsible = this.isCollapsible();
const state = this.state();
if (!isCollapsible || !state) {
return;
}
this.toggle(state === 'expand');
}, ...(ngDevMode ? [{ debugName: "handleStateChange" }] : []));
this.setExpandInitialState = effect(() => {
const state = this.state();
if (state) {
this.expand.set(state === 'expand');
}
else {
this.expand.set(this.layout()?.contentCollapsible === 'expand');
}
this.setExpandInitialState.destroy();
}, ...(ngDevMode ? [{ debugName: "setExpandInitialState" }] : []));
this.setHostStyle = effect(() => {
const host = this.el.nativeElement;
const label = this.label();
const customComponent = this.customComponent();
const isCollapsible = this.isCollapsible();
if (!label || !!customComponent) {
return;
}
if (isCollapsible) {
host.style.setProperty('display', 'flex');
host.style.setProperty('cursor', 'pointer');
}
else {
host.style.setProperty('display', 'inline-block');
host.style.removeProperty('cursor');
}
}, ...(ngDevMode ? [{ debugName: "setHostStyle" }] : []));
this.toggle = (expand) => {
const collapsible = this.isCollapsible();
if (!collapsible) {
return;
}
this.expand.update((x) => expand ?? !x);
this.setElementHeight();
if (this.componentRef) {
this.componentRef.expand.set(this.expand());
}
};
afterNextRender(() => {
this.initCollapsibleEl();
this.listenClickEvent();
});
}
listenTransition() {
const el = this.collapsibleEl();
if (!el) {
return;
}
const transitionEnd$ = fromEvent(el, 'transitionend', {
passive: true,
}).pipe(filter(() => this.expand()), tap(() => {
el?.classList.remove(...['height', 'overflow']);
}));
transitionEnd$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
}
setElementHeight() {
this.setExpandStyle();
if (!this.expand()) {
requestAnimationFrame(() => this.setCollapseStyle());
}
}
initCollapsibleEl() {
const el = this.collapsibleEl();
if (!el || !this.isCollapsible()) {
return;
}
this.collapsibleElCssText = el.style.cssText || '';
el.classList.add('collapsible-container');
this.listenTransition();
if (!this.expand()) {
this.setCollapseStyle();
}
}
setCollapseStyle() {
const el = this.collapsibleEl();
const stylesToRemove = ['border', 'padding', 'margin'];
if (!el) {
return;
}
stylesToRemove.forEach((style) => {
if (!this.collapsibleElCssText.includes(style))
return;
el.style.removeProperty(style);
});
el.style.setProperty('overflow', 'hidden');
el.style.setProperty('height', '0px');
}
setExpandStyle() {
const el = this.collapsibleEl();
const height = !el ? 0 : el.scrollHeight + 1;
// Set existing styles from collapsible element first
if (this.collapsibleElCssText) {
el?.setAttribute('style', this.collapsibleElCssText);
}
// Then set height later to overwrite height style
el?.style.setProperty('height', `${height}px`);
}
listenClickEvent() {
const host = this.el.nativeElement;
const collapsible = this.isCollapsible();
if (!collapsible) {
return;
}
fromEvent(host, 'click', { passive: true })
.pipe(tap(() => this.toggle()), takeUntilDestroyed(this.destroyRef))
.subscribe();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormLabelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: FormLabelComponent, isStandalone: true, selector: "form-label", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, collapsibleEl: { classPropertyName: "collapsibleEl", publicName: "collapsibleEl", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, customComponent: { classPropertyName: "customComponent", publicName: "customComponent", isSignal: true, isRequired: false, transformFunction: null }, customTemplate: { classPropertyName: "customTemplate", publicName: "customTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "form-label" }, viewQueries: [{ propertyName: "componentAnchor", first: true, predicate: ["componentAnchor"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "@if (useDefaultTemplate()) {\r\n <span class=\"text\">{{ label() }}</span>\r\n @if (isCollapsible()) {\r\n <span\r\n style=\"margin-left: auto\"\r\n [ngStyle]=\"{\r\n transform: !expand() ? 'rotate(-180deg)' : 'rotate(0deg)',\r\n }\"\r\n >\r\n <!-- prettier-ignore -->\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" fill=\"currentColor\" class=\"bi bi-chevron-up\" viewBox=\"0 0 16 16\">\r\n <path fill-rule=\"evenodd\" d=\"M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z\"/>\r\n </svg>\r\n </span>\r\n }\r\n}\r\n\r\n<ng-container #componentAnchor></ng-container>\r\n\r\n@if (customTemplate(); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template\"\r\n [ngTemplateOutletContext]=\"{\r\n label: label(),\r\n layout: layout(),\r\n toggle,\r\n collapsible: isCollapsible(),\r\n expand: expand(),\r\n props: props(),\r\n }\"\r\n ></ng-container>\r\n}\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormLabelComponent, decorators: [{
type: Component,
args: [{ selector: 'form-label', imports: [CommonModule], host: {
class: 'form-label',
}, template: "@if (useDefaultTemplate()) {\r\n <span class=\"text\">{{ label() }}</span>\r\n @if (isCollapsible()) {\r\n <span\r\n style=\"margin-left: auto\"\r\n [ngStyle]=\"{\r\n transform: !expand() ? 'rotate(-180deg)' : 'rotate(0deg)',\r\n }\"\r\n >\r\n <!-- prettier-ignore -->\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" fill=\"currentColor\" class=\"bi bi-chevron-up\" viewBox=\"0 0 16 16\">\r\n <path fill-rule=\"evenodd\" d=\"M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z\"/>\r\n </svg>\r\n </span>\r\n }\r\n}\r\n\r\n<ng-container #componentAnchor></ng-container>\r\n\r\n@if (customTemplate(); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template\"\r\n [ngTemplateOutletContext]=\"{\r\n label: label(),\r\n layout: layout(),\r\n toggle,\r\n collapsible: isCollapsible(),\r\n expand: expand(),\r\n props: props(),\r\n }\"\r\n ></ng-container>\r\n}\r\n" }]
}], ctorParameters: () => [] });
class ContentWrapperComponent {
constructor() {
this.destroyRef = inject(DestroyRef);
this.global = inject(GlobalVariableService);
this.hostEventService = inject(HostEventService);
this.showErrorsOnTouched = this.global.showErrorsOnTouched;
this.config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : []));
this.control = input(...(ngDevMode ? [undefined, { debugName: "control" }] : []));
this.collapsibleState = input(...(ngDevMode ? [undefined, { debugName: "collapsibleState" }] : []));
this.isDirty = signal(false, ...(ngDevMode ? [{ debugName: "isDirty" }] : []));
this.isTouched = signal(false, ...(ngDevMode ? [{ debugName: "isTouched" }] : []));
this.controlErrors = rxResource({
params: () => this.control(),
stream: ({ params }) => merge(params.valueChanges, params.statusChanges).pipe(startWith(params.status), map(() => params.errors)),
defaultValue: null,
});
this.hideErrors = toSignal(this.global.hideErrorMessage$.pipe(tap(() => this.updateControlStatus())));
this.formControlName = computed(() => this.config()?.formControlName ?? '', ...(ngDevMode ? [{ debugName: "formControlName" }] : []));
this.description = computed(() => this.config()?.description, ...(ngDevMode ? [{ debugName: "description" }] : []));
this.descriptionPosition = computed(() => {
const position = this.layout()?.descriptionPosition ?? this.global.descriptionPosition;
return position;
}, ...(ngDevMode ? [{ debugName: "descriptionPosition" }] : []));
this.label = computed(() => this.config()?.label, ...(ngDevMode ? [{ debugName: "label" }] : []));
this.layout = computed(() => this.config()?.layout, ...(ngDevMode ? [{ debugName: "layout" }] : []));
this.props = computed(() => this.config()?.props, ...(ngDevMode ? [{ debugName: "props" }] : []));
this.validators = computed(() => {
const { validators, asyncValidators } = this.config() ?? {};
return [...(validators || []), ...(asyncValidators || [])];
}, ...(ngDevMode ? [{ debugName: "validators" }] : []));
this.showLabel = computed(() => this.label() && this.layout()?.hideLabel !== true, ...(ngDevMode ? [{ debugName: "showLabel" }] : []));
this.customErrorComponent = computed(() => {
const name = this.formControlName();
const components = this.global.errorComponents;
const defaultComponent = this.global.errorComponentDefault;
return components?.[name] ?? defaultComponent;