@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
JavaScript
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