UNPKG

@zarlex/ngx-accessor

Version:

This library provides an adapter to interact with Angular forms. It also provides an adapter to work with object signals and forms

1,316 lines (1,282 loc) 68.8 kB
import * as i0 from '@angular/core'; import { forwardRef, Directive, Optional, Host, Input, signal, computed, untracked, effect, InjectionToken, Injectable, output, inject, ElementRef, input, Self, Inject, NgModule, model } from '@angular/core'; import { CheckboxControlValueAccessor, NG_VALUE_ACCESSOR, DefaultValueAccessor, NumberValueAccessor, RadioControlValueAccessor, RangeValueAccessor, SelectControlValueAccessor, SelectMultipleControlValueAccessor, NgModel, UntypedFormControl, PristineChangeEvent, NG_VALIDATORS, NG_ASYNC_VALIDATORS, NgControl, CheckboxRequiredValidator as CheckboxRequiredValidator$1, EmailValidator as EmailValidator$1, MaxLengthValidator as MaxLengthValidator$1, MaxValidator as MaxValidator$1, MinLengthValidator as MinLengthValidator$1, MinValidator as MinValidator$1, PatternValidator as PatternValidator$1, RequiredValidator as RequiredValidator$1 } from '@angular/forms'; import { Subscription, Subject, filter, map, Observable, firstValueFrom } from 'rxjs'; import { toObservable } from '@angular/core/rxjs-interop'; class CheckboxValueAccessorDirective extends CheckboxControlValueAccessor { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: CheckboxValueAccessorDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.5", type: CheckboxValueAccessorDirective, isStandalone: true, selector: "input[type=checkbox][ngxAccessor]", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckboxValueAccessorDirective), multi: true } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: CheckboxValueAccessorDirective, decorators: [{ type: Directive, args: [{ selector: 'input[type=checkbox][ngxAccessor]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckboxValueAccessorDirective), multi: true } ], standalone: true }] }] }); class DefaultValueAccessorDirective extends DefaultValueAccessor { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: DefaultValueAccessorDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.5", type: DefaultValueAccessorDirective, isStandalone: true, selector: "input:not([type=checkbox])[ngxAccessor],textarea[ngxAccessor]", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DefaultValueAccessorDirective), multi: true } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: DefaultValueAccessorDirective, decorators: [{ type: Directive, args: [{ selector: 'input:not([type=checkbox])[ngxAccessor],textarea[ngxAccessor]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DefaultValueAccessorDirective), multi: true } ], standalone: true }] }] }); class NumberValueAccessorDirective extends NumberValueAccessor { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: NumberValueAccessorDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.5", type: NumberValueAccessorDirective, isStandalone: true, selector: "input[type=number][ngxAccessor]", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NumberValueAccessorDirective), multi: true } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: NumberValueAccessorDirective, decorators: [{ type: Directive, args: [{ selector: 'input[type=number][ngxAccessor]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NumberValueAccessorDirective), multi: true } ], standalone: true }] }] }); class RadioControlValueAccessorDirective extends RadioControlValueAccessor { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: RadioControlValueAccessorDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.5", type: RadioControlValueAccessorDirective, isStandalone: true, selector: "input[type=radio][ngxAccessor]", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RadioControlValueAccessorDirective), multi: true } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: RadioControlValueAccessorDirective, decorators: [{ type: Directive, args: [{ selector: 'input[type=radio][ngxAccessor]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RadioControlValueAccessorDirective), multi: true } ], standalone: true }] }] }); class RangeValueAccessorDirective extends RangeValueAccessor { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: RangeValueAccessorDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.5", type: RangeValueAccessorDirective, isStandalone: true, selector: "input[type=range][ngxAccessor]", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RangeValueAccessorDirective), multi: true } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: RangeValueAccessorDirective, decorators: [{ type: Directive, args: [{ selector: 'input[type=range][ngxAccessor]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RangeValueAccessorDirective), multi: true } ], standalone: true }] }] }); class SelectControlValueAccessorDirective extends SelectControlValueAccessor { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: SelectControlValueAccessorDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.5", type: SelectControlValueAccessorDirective, isStandalone: true, selector: "select:not([multiple])[ngxAccessor]", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectControlValueAccessorDirective), multi: true } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: SelectControlValueAccessorDirective, decorators: [{ type: Directive, args: [{ selector: 'select:not([multiple])[ngxAccessor]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectControlValueAccessorDirective), multi: true } ], standalone: true }] }] }); class SelectMultipleControlValueAccessorDirective extends SelectMultipleControlValueAccessor { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: SelectMultipleControlValueAccessorDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.5", type: SelectMultipleControlValueAccessorDirective, isStandalone: true, selector: "select[multiple][ngxAccessor]", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectMultipleControlValueAccessorDirective), multi: true } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: SelectMultipleControlValueAccessorDirective, decorators: [{ type: Directive, args: [{ selector: 'select[multiple][ngxAccessor]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectMultipleControlValueAccessorDirective), multi: true } ], standalone: true }] }] }); function _buildValueString(id, value) { if (id == null) return `${value}`; if (typeof value === 'string') value = `'${value}'`; if (value && typeof value === 'object') value = 'Object'; return `${id}: ${value}`.slice(0, 50); } /** * Adapted code from Angular https://github.com/angular/angular/blob/main/packages/forms/src/directives/select_multiple_control_value_accessor.ts * This code defines SelectMultipleControlValueAccessorDirective as select * * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ class SelectMultipleOption { _element; _renderer; _select; // TODO(issue/24571): remove '!'. id; /** @internal */ _value; constructor(_element, _renderer, _select) { this._element = _element; this._renderer = _renderer; this._select = _select; if (this._select) { // @ts-ignore this.id = this._select._registerOption(this); } } /** * @description * Tracks the value bound to the option element. Unlike the value binding, * ngValue supports binding to objects. */ set ngValue(value) { if (this._select == null) return; this._value = value; this._setElementValue(_buildValueString(this.id, value)); this._select.writeValue(this._select.value); } /** * @description * Tracks simple string values bound to the option element. * For objects, use the `ngValue` input binding. */ set value(value) { if (this._select) { this._value = value; this._setElementValue(_buildValueString(this.id, value)); this._select.writeValue(this._select.value); } else { this._setElementValue(value); } } /** @internal */ _setElementValue(value) { this._renderer.setProperty(this._element.nativeElement, 'value', value); } /** @internal */ _setSelected(selected) { this._renderer.setProperty(this._element.nativeElement, 'selected', selected); } /** @nodoc */ ngOnDestroy() { if (this._select) { // @ts-ignore this._select._optionMap.delete(this.id); this._select.writeValue(this._select.value); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: SelectMultipleOption, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: SelectMultipleControlValueAccessorDirective, host: true, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.1.5", type: SelectMultipleOption, isStandalone: true, selector: "option", inputs: { ngValue: "ngValue", value: "value" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: SelectMultipleOption, decorators: [{ type: Directive, args: [{ selector: 'option', }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: SelectMultipleControlValueAccessorDirective, decorators: [{ type: Optional }, { type: Host }] }], propDecorators: { ngValue: [{ type: Input, args: ['ngValue'] }], value: [{ type: Input, args: ['value'] }] } }); function _throwError(dir, message) { let messageEnd; if (dir.path.length > 1) { messageEnd = `path: '${dir.path.join(' -> ')}'`; } else if (dir.path[0]) { messageEnd = `name: '${dir.path}'`; } else { messageEnd = 'unspecified name attribute'; } throw new Error(`${message} ${messageEnd}`); } const BUILTIN_ACCESSORS = [ CheckboxValueAccessorDirective, RangeValueAccessorDirective, NumberValueAccessorDirective, SelectControlValueAccessorDirective, SelectMultipleControlValueAccessorDirective, RadioControlValueAccessorDirective, ]; /** * Adapted code from https://github.com/angular/angular/blob/6789c7ef947952551d7598fe37a3d86093b75720/packages/forms/src/directives/shared.ts#L367 * * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ function isBuiltInAccessor(valueAccessor) { return BUILTIN_ACCESSORS.some(a => valueAccessor.constructor === a); } /** * Adapted code from https://github.com/angular/angular/blob/6789c7ef947952551d7598fe37a3d86093b75720/packages/forms/src/directives/shared.ts#L388 * * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ function selectValueAccessor(dir, valueAccessors) { if (!valueAccessors) { return null; } let defaultAccessor = undefined; let builtinAccessor = undefined; let customAccessor = undefined; valueAccessors.forEach((v) => { if (v.constructor === DefaultValueAccessorDirective) { defaultAccessor = v; } else if (isBuiltInAccessor(v)) { builtinAccessor = v; } else { customAccessor = v; } }); if (customAccessor) { return customAccessor; } if (builtinAccessor) { return builtinAccessor; } if (defaultAccessor) { return defaultAccessor; } _throwError(dir, 'No valid value accessor for form control with'); return null; } function getValidatorError(key, value, validator) { const result = {}; result[key] = validator.error(value); return result; } function keyValidatorsFromAccessorConfig(config) { return Object.entries(config || {}).reduce((previousValue, [key, propertyAccessorConfig]) => { if (propertyAccessorConfig?.validators?.length) { previousValue[key] = propertyAccessorConfig.validators; } return previousValue; }, {}); } function isEqual(val1, val2) { switch (typeof val1) { case 'number': case 'string': case 'boolean': return val1 === val2; default: return JSON.stringify(val1) === JSON.stringify(val2); } } const getProperty = (object, path) => { if (path === undefined || path === null) { return object; } return path.toString().split('.').reduce((previous, current) => { if (previous && current.length) { return previous[current]; } else { return previous; } }, object); }; const setProperty = (obj, path, value) => { if (path === undefined || path === null) { return; } const pathArray = path.toString().split('.'); pathArray.reduce((previous, current, index) => { if (index === pathArray.length - 1) { if (current.length) { previous[current] = value; } else { Object.assign(previous, value); } } else { if (!previous || !previous[current]) { // Check if the next index is a number in order to determine whether empty object should be an object or array if (Number.isFinite(parseInt(pathArray[index + 1], 10))) { previous[current] = []; } else { previous[current] = {}; } } } return previous[current]; }, obj); }; class ValidatorError extends Error { message; params; id; constructor(options) { super(options.message); this.id = options.id; this.message = options.message; this.params = options.params; } } class MaxLengthValidationError extends ValidatorError { constructor(params) { super({ params, id: 'maxlength', message: `Invalid length! Has to be smaller than ${params.maxLength}. Current length is ${params.currentLength}` }); } } class MaxValidationError extends ValidatorError { constructor(params) { super({ params, id: 'max', message: `Invalid number! Has to be smaller than ${params.max}. Current number is ${params.current}` }); } } class MinLengthValidationError extends ValidatorError { constructor(params) { super({ params, id: 'minlength', message: `Invalid length! Has to be larger than ${params.minLength}. Current length is ${params.currentLength}` }); } } class MinValidationError extends ValidatorError { constructor(params) { super({ params, id: 'min', message: `Invalid number! Has to be bigger than ${params.min}. Current number is ${params.current}` }); } } class RequiredValidationError extends ValidatorError { constructor() { super({ params: {}, id: 'required', message: `Required field` }); } } class EmailValidationError extends ValidatorError { constructor(params) { super({ id: 'email', message: `Not an e-mail! ${params.value} seems not to be a valid e-mail address`, params }); } } class InvalidNestedPropertyError extends ValidatorError { constructor(errors) { super({ params: { errors }, id: 'invalidNestedProperty', message: `${Object.keys(errors).length} invalid properties` }); } } class PatternValidationError extends ValidatorError { constructor(params) { super({ id: 'pattern', message: `Does not match pattern! ${params.value} does not match the required pattern ${params.pattern.toString()}`, params }); } } class Validation { validators; isValid; errors; isValidating; errorMap = new Map(); writableErrors = signal([]); writableIsValid = signal(false); writableIsValidating = signal(false); constructor(validators) { this.validators = validators; this.errors = this.writableErrors.asReadonly(); this.isValid = this.writableIsValid.asReadonly(); this.isValidating = this.writableIsValidating.asReadonly(); } update(key, update) { if (!update.isValid) { this.errorMap.set(key, update); } else { this.errorMap.delete(key); } const updates = Array.from(this.errorMap.values()); const nestedErrors = Array .from(this.errorMap.entries()) .reduce((previous, [key, value]) => { if (value.errors.length) { previous[key] = value.errors; } return previous; }, {}); if (Object.keys(nestedErrors).length) { this.writableErrors.set([ new InvalidNestedPropertyError(nestedErrors), ]); } else { this.writableErrors.set([]); } this.writableIsValidating.set(updates.find(update => update.isValidating) !== undefined); if (updates.find(update => !update.isValid)) { this.writableIsValid.set(false); } else { this.writableIsValid.set(true); } } } class AbstractAccessor { config; validation; isDirty; dirtyKeys = new Map(); writableIsDirty = signal(false); accessorMap = new Map(); accessorSubscriptionsMap = new Map(); constructor(config = {}) { this.config = config; this.validation = new Validation(keyValidatorsFromAccessorConfig(config)); this.isDirty = this.writableIsDirty.asReadonly(); } access(key) { if (key) { return this.accessProperty(key); } else { return this.accessRoot(); } } getAccessors() { return this.access().getAccessors(); } removeAccess(key) { this.accessorSubscriptionsMap.get(key)?.unsubscribe(); this.accessorMap.delete(key); this.accessorSubscriptionsMap.delete(key); } destroy() { Array.from(this.accessorMap.values()).forEach(accesor => accesor.destroy()); Array.from(this.accessorSubscriptionsMap.values()).forEach(subscription => subscription.unsubscribe()); this.accessorMap.clear(); this.accessorSubscriptionsMap.clear(); } setPristine() { this.writableIsDirty.set(false); this.accessorMap.forEach((accessor) => accessor.setPristine()); } accessRoot() { let existing = this.accessorMap.get('root'); if (!existing) { existing = this.createRootAccessor({ isEqual: this.config['']?.isEqual, validators: this.validation.validators, onValidationChange: (update) => { this.validation.update('', update); } }); this.accessorMap.set('root', existing); this.accessorSubscriptionsMap.set('root', new Subscription()); } return existing; } accessProperty(key) { let existing = this.accessorMap.get(key); if (!existing) { existing = this.createAccessor(key, { isEqual: this.config[key]?.isEqual, validators: this.validation.validators, onValidationChange: (update) => { this.validation.update(key, update); }, }); this.accessorMap.set(key, existing); this.accessorSubscriptionsMap.set(key, new Subscription()); } return existing; } } var ValidatorId; (function (ValidatorId) { ValidatorId["required"] = "required"; ValidatorId["minLength"] = "minLength"; ValidatorId["maxLength"] = "maxLength"; ValidatorId["min"] = "min"; ValidatorId["max"] = "max"; ValidatorId["pattern"] = "pattern"; ValidatorId["email"] = "email"; })(ValidatorId || (ValidatorId = {})); function maxValidator(max) { return { id: ValidatorId.max, isValid: (value) => { return !Number.isFinite(value) || value <= max; }, async: false, error: (value) => new MaxValidationError({ max: max, current: value }), setupElement: (el) => { el.setAttribute('max', max.toString()); } }; } function maxLengthValidator(maxLength) { return { id: ValidatorId.maxLength, isValid: (value) => { return !value || value && value.length <= maxLength; }, async: false, error: (value) => new MaxLengthValidationError({ maxLength: maxLength, currentLength: value.length }), setupElement: (el) => el.setAttribute('maxLength', maxLength.toString()) }; } function minValidator(min) { return { id: ValidatorId.min, isValid: (value) => { return !Number.isFinite(value) || value >= min; }, async: false, error: (value) => { return new MinValidationError({ min: min, current: value }); }, setupElement: (el) => { el.setAttribute('min', min.toString()); } }; } function minLengthValidator(minLength) { return { id: ValidatorId.minLength, isValid: (value) => { return !value || value.length >= minLength; }, async: false, error: (value) => new MinLengthValidationError({ minLength: minLength, currentLength: value.length }), setupElement: (el) => el.setAttribute('minLength', minLength.toString()) }; } function requiredValidator() { return { id: ValidatorId.required, isValid: (value) => { let valid; switch (typeof value) { case 'boolean': valid = value === true; break; case 'number': valid = Number.isFinite(value); break; case 'string': valid = value && value.length > 0; break; default: valid = !!value; } return valid; }, async: false, error: (value) => new RequiredValidationError(), setupElement: (el) => { el.setAttribute('required', 'required'); } }; } const EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; function emailValidator() { return { id: ValidatorId.email, isValid: (value) => { return !value || EMAIL_REGEXP.test(value); }, async: false, error: (value) => new EmailValidationError({ value: value }) }; } function patternValidator(pattern) { let regex; let regexStr; if (typeof pattern === 'string') { regexStr = ''; if (pattern.charAt(0) !== '^') { regexStr += '^'; } regexStr += pattern; if (pattern.charAt(pattern.length - 1) !== '$') { regexStr += '$'; } regex = new RegExp(regexStr); } else { regex = pattern; } return { id: ValidatorId.pattern, isValid: (value) => { if (!value) { return true; } return regex.test(value); }, async: false, error: (value) => new PatternValidationError({ value: value, pattern: regex }), setupElement: (el) => { el.setAttribute('pattern', pattern.toString()); } }; } class ValueValidation { validators; isValidating; errors; isValid = computed(() => { return !this.isValidating() && this.errors().length === 0; }); update = new Subject(); nestedErrors = new Map(); nestedValidating = new Map(); writableIsValidating = signal(false); writableErrors = signal([]); constructor(validators) { this.validators = validators; this.isValidating = this.writableIsValidating.asReadonly(); this.errors = this.writableErrors.asReadonly(); } setErrors(errors) { this.writableErrors.set(errors); this.update.next({ isValid: this.isValid(), errors: errors, isValidating: this.isValidating() }); } setIsValidating(isValidating) { if (this.isValidating() !== isValidating) { this.writableIsValidating.set(isValidating); this.update.next({ isValid: this.isValid(), errors: this.errors(), isValidating: isValidating }); } } } class SignalValueAccessor { signalValue; property; options; id = Math.random().toString(36).substr(2); isDirty = signal(false); validation; isRequired = computed(() => { return this.validation.validators?.find(validator => validator.id === ValidatorId.required) !== undefined; }); arrayAccessors = []; get; errors = signal([]); accessorMap = new Map(); dirtyMap = new Map(); constructor(signalValue, property, options) { this.signalValue = signalValue; this.property = property; this.options = options; this.validation = new ValueValidation(options.validators[this.getValidatorKey()]); this.validation.update.subscribe(update => { options.onValidationChange(update); }); this.setArrayAccessors(); this .get$() .subscribe((newValue) => { if (Array.isArray(newValue) && this.arrayAccessorsHaveChanged()) { this.setArrayAccessors(); this.updateValidation(); } }); this.get = computed(() => { return this.getValue(); }); } getAccessors() { return this.arrayAccessors; } getAccessKey() { return this.options.parentKeys.length ? `${this.options.parentKeys.join('.')}.${this.property}` : this.property; } getValidatorKey() { const parentKeys = this.options.parentKeys.filter(key => typeof key !== 'number'); return parentKeys.length ? `${parentKeys.join('.')}.${this.property}` : this.property; } get$() { return this.options.observable.pipe(filter(change => { const key = this.getAccessKey(); return !this.isEqual(getProperty(change.new, key), getProperty(change.previous, key)); }), map(() => this.getValue())); } set(value) { const key = this.getAccessKey(); this.signalValue.update(state => { if (key !== undefined && key !== null) { const clone = structuredClone(state || {}); setProperty(clone, key, value); return clone; } else { return value; } }); } access(key) { let existing = this.accessorMap.get(key); if (!existing) { existing = this.createItemAccessor(key); this.accessorMap.set(key, existing); } return existing; } setDirty(isDirty) { this.isDirty.set(isDirty); this.options.onIsDirty(isDirty); } setPristine() { this.isDirty.set(false); for (const entry of this.accessorMap.values()) { entry.setPristine(); } this.arrayAccessors.forEach(accessor => { accessor.setPristine(); }); } destroy() { Array.from(this.accessorMap.values()).forEach(accesor => accesor.destroy()); this.accessorMap.clear(); this.validation.setErrors([]); this.validation.setIsValidating(false); this.options.onDestroy(); } getValue() { const key = this.getAccessKey(); if (key !== undefined && key !== null) { return getProperty(this.signalValue(), key); } else { return this.signalValue(); } } setArrayAccessors() { if (Array.isArray(this.getValue())) { const items = this.getValue(); if (this.arrayAccessors.length > items.length) { const findDeleteIndexes = this.arrayAccessors.reduce((previousValue, currentValue, currentIndex, array) => { if (this.findItemIndexForAccessor(currentValue) === -1) { previousValue.push(currentIndex); } return previousValue; }, []); findDeleteIndexes.sort().reverse().forEach(deleteIndex => { this.arrayAccessors[deleteIndex].destroy(); this.arrayAccessors.splice(deleteIndex, 1); }); } items.forEach((item, index) => { const accessorIndex = this.findItemAccessorIndex(item); if (accessorIndex === -1) { this.arrayAccessors[index] = this.createItemAccessor(index); } else if (accessorIndex !== index) { if (!this.arrayAccessors[index] || this.isEqual(items[index], this.arrayAccessors[index].getValue())) { this.arrayAccessors[index] = this.createItemAccessor(index); } else { const accessor = this.arrayAccessors[accessorIndex]; this.arrayAccessors[accessorIndex] = this.arrayAccessors[index]; this.arrayAccessors[index] = accessor; } } }); } } updateValidation() { let errors = []; if (this.validation.nestedErrors.get('')?.length) { errors = errors.concat(this.validation.nestedErrors.get('')); } const nestedErrors = Array .from(this.validation.nestedErrors) .filter(([key, value]) => key !== '' && value?.length > 0) .reduce((previous, current) => { previous[current[0]] = current[1]; return previous; }, {}); if (Object.keys(nestedErrors).length > 0) { errors.push(new InvalidNestedPropertyError(nestedErrors)); } this.validation.setErrors(errors); const nestedValidating = Array.from(this.validation.nestedValidating.values()).find(value => value === true); this.validation.setIsValidating(nestedValidating); } arrayAccessorsHaveChanged() { const newValue = this.getValue(); let changed = false; if (this.arrayAccessors.length !== newValue?.length) { changed = true; } else { this.arrayAccessors.forEach((accessor, index) => { if (!changed) { changed = accessor.getValue() !== newValue[index]; } }); } return changed; } createItemAccessor(key) { return new SignalValueAccessor(this.signalValue, key, { ...this.options, parentKeys: (this.property !== undefined && this.property !== null) ? this.options.parentKeys.concat(this.property) : this.options.parentKeys, onValidationChange: (update) => { this.validation.nestedErrors.set(key.toString(), update.errors); this.validation.nestedValidating.set(key.toString(), update.isValidating); this.updateValidation(); }, onDestroy: () => { this.dirtyMap.delete(key); this.validation.nestedErrors.set(key.toString(), []); this.validation.nestedValidating.set(key.toString(), false); this.validation.setErrors([]); this.validation.setIsValidating(false); this.accessorMap.delete(key); if (this.accessorMap.size === 0 && this.arrayAccessors.length === 0) { this.destroy(); } }, onIsDirty: (isDirty) => { this.dirtyMap.set(key, isDirty); const isSomethingDirty = Array.from(this.dirtyMap.values()).find(isDirty => isDirty); if (this.isDirty() !== isSomethingDirty) { this.setDirty(isSomethingDirty); } } }); } findItemAccessorIndex(item) { return this .arrayAccessors .findIndex(accessor => accessor && this.isEqual(accessor.getValue(), item)); } findItemIndexForAccessor(accessor) { const items = this.getValue(); return items .findIndex(item => this.isEqual(accessor.getValue(), item)); } isEqual(val1, val2) { return typeof this.options.isEqual === 'function' ? this.options.isEqual(val1, val2) : isEqual(val1, val2); } } function computedPrevious(s) { let current = null; let previous = untracked(() => s()); // initial value is the current value return computed(() => { current = s(); const result = previous; previous = structuredClone(current); return result; }); } class SignalAccessor extends AbstractAccessor { valueSignal; arrayAccessors; signalObservable; previous; constructor(valueSignal, options) { super(options); this.valueSignal = valueSignal; this.arrayAccessors = signal([]); this.previous = computedPrevious(this.valueSignal); effect(() => { const newValue = this.valueSignal(); const previousValue = this.previous(); this.signalObservable.next({ new: newValue, previous: previousValue }); this.arrayAccessors.set(this.access().getAccessors()); }); this.signalObservable = new Subject(); } getAccessors() { return this.access().getAccessors(); } createAccessor(key, options) { return new SignalValueAccessor(this.valueSignal, key, { isEqual: options.isEqual, validators: options.validators, onValidationChange: (update) => { options.onValidationChange(update); }, onIsDirty: (isDirty) => { this.dirtyKeys.set(key, isDirty); const somethingIsDirty = Array.from(this.dirtyKeys.values()).find(isDirty => isDirty); this.writableIsDirty.set(somethingIsDirty); }, onDestroy: () => { this.accessorMap.delete(key); this.accessorSubscriptionsMap.get(key)?.unsubscribe(); this.accessorSubscriptionsMap.delete(key); this.dirtyKeys.delete(key); }, parentKeys: [], observable: this.signalObservable }); } createRootAccessor(options) { return new SignalValueAccessor(this.valueSignal, undefined, { isEqual: options.isEqual, validators: {}, onValidationChange: (update) => { options?.onValidationChange(update); }, onIsDirty: (isDirty) => { this.dirtyKeys.set('', isDirty); const somethingIsDirty = Array.from(this.dirtyKeys.values()).find(isDirty => isDirty); this.writableIsDirty.set(somethingIsDirty); }, onDestroy: () => { this.accessorMap.delete(''); this.accessorSubscriptionsMap.get('')?.unsubscribe(); this.accessorSubscriptionsMap.delete(''); this.dirtyKeys.delete(''); }, parentKeys: [], observable: this.signalObservable }); } } const ngxAccessorHost = new InjectionToken('AccessorHost'); const ACCESSOR = new InjectionToken('Accessor'); class AccessorService { accessor = signal(undefined); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: AccessorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: AccessorService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: AccessorService, decorators: [{ type: Injectable }] }); class AccessorDirective extends NgModel { valueChange = output({ alias: 'ngxAccessorValueChange' }); accessorValueChange = output(); element = inject(ElementRef, { optional: true }); subscriptions = new Subscription(); accessor = input.required({ alias: 'ngxAccessor' }); required = input(); control = new UntypedFormControl(); accessorService = inject(ACCESSOR); constructor(validators, asyncValidators, valueAccessors) { if (!valueAccessors) { throw new Error('Accessor directive is applied on incompatible object. Make sure to provide the angular ControlValueAccessor'); } super(null, validators, asyncValidators, [valueAccessors[0]]); this.valueAccessor = selectValueAccessor(this, valueAccessors); toObservable(this.accessor) .subscribe((accessor) => { this.subscriptions.unsubscribe(); this.subscriptions = new Subscription(); this.modelToControlUpdate(); this.observeModelChanges(); this.observeValidationErrors(); accessor.validation.setIsValidating(this.control.status === 'PENDING'); if (this.element) { accessor.validation.validators?.forEach((validator) => { if (typeof validator.setupElement === 'function') { validator.setupElement(this.element.nativeElement); } }); } this.accessorService.accessor.set(accessor); }); effect(() => { if (!this.accessor().isDirty()) { this.control.markAsPristine({}); } }); this.control.statusChanges.subscribe((status) => { if (status === 'PENDING') { this.accessor().validation.setIsValidating(true); } else { this.accessor().validation.setIsValidating(false); } }); this.control.events.subscribe((event) => { if (event instanceof PristineChangeEvent) { if (this.control.dirty) { this.accessor().setDirty(this.control.dirty); } } }); } ngAfterViewInit() { this.control.markAsPristine(); this.control.updateValueAndValidity({ onlySelf: true }); } ngOnDestroy() { this.subscriptions.unsubscribe(); super.ngOnDestroy(); this.accessor()?.destroy(); } viewToModelUpdate(newValue) { if (newValue !== this.accessor()?.getValue()) { this.accessor()?.set(newValue); this.accessorValueChange.emit(newValue); this.valueChange.emit(newValue); } } modelToControlUpdate() { if (this.accessor()) { this.control.setValue(this.accessor().getValue()); this.setValidationErrorsOnModel(); } } setValidationErrorsOnModel() { if (!this.accessor()) { return; } if (this.control.valid) { this.accessor().validation.setErrors([]); } else if (this.control.errors) { const validationErrors = []; Object.entries(this.control.errors).forEach(([key, value]) => { if (value instanceof ValidatorError) { validationErrors.push(value); } else { validationErrors.push(new ValidatorError({ id: key, message: `Invalid value (${key})`, params: value })); } }); this.accessor().validation.setErrors(validationErrors); } } observeModelChanges() { if (!this.accessor()) { return; } this.subscriptions.add(this .accessor() .get$() .subscribe(value => { this.modelToControlUpdate(); })); } observeValidationErrors() { this.subscriptions.add(this.control.statusChanges.subscribe(ev => { this.setValidationErrorsOnModel(); })); if (this.accessor().validation.validators?.length > 0) { this.control.clearValidators(); this.accessor().validation.validators?.forEach(validator => { if (validator.async) { this.control.addAsyncValidators((control) => { const result = validator.isValid(control.value); let validationPromise; if (result instanceof Promise) { validationPromise = result; } else if (result instanceof Observable) { validationPromise = firstValueFrom(result); } else { throw new Error('Validator is marked as async but does not return async value!'); } return validationPromise.then(isValid => { if (!isValid) { return [validator.error(control.value)]; } else { return undefined; } }); }); } else { this.control.addValidators((control) => { if (!validator.isValid(control.value)) { return [validator.error(control.value)]; } else { return undefined; } }); } }); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: AccessorDirective, deps: [{ token: NG_VALIDATORS, optional: true, self: true }, { token: NG_ASYNC_VALIDATORS, optional: true, self: true }, { token: NG_VALUE_ACCESSOR, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.1.5", type: AccessorDirective, isStandalone: false, selector: "[ngxAccessor]", inputs: { accessor: { classPropertyName: "accessor", publicName: "ngxAccessor", isSignal: true, isRequired: true, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "ngxAccessorValueChange", accessorValueChange: "accessorValueChange" }, providers: [ { provide: NgControl, useExisting: forwardRef(() => AccessorDirective) }, { provide: ngxAccessorHost, useExisting: forwardRef(() => AccessorDirective) }, { provide: ACCESSOR, useClass: AccessorService, multi: false } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: AccessorDirective, decorators: [{ type: Directive, args: [{ selector: '[ngxAccessor]', providers: [ { provide: NgControl, useExisting: forwardRef(() => AccessorDirective) }, { provide: ngxAccessorHost, useExisting: forwardRef(() => AccessorDirective) }, { provide: ACCESSOR, useClass: AccessorService, multi: false } ], standalone: false }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Optional }, { t