UNPKG

@angular/forms

Version:

Angular - directives and services for creating forms

669 lines (644 loc) 18.9 kB
/** * @license Angular v21.0.5 * (c) 2010-2025 Google LLC. https://angular.dev/ * License: MIT */ import * as i0 from '@angular/core'; import { InjectionToken, inject, ElementRef, Injector, input, computed, ɵCONTROL as _CONTROL, effect, Directive, ɵɵcontrolCreate as __controlCreate, ɵcontrolUpdate as _controlUpdate, ɵisPromise as _isPromise, resource } from '@angular/core'; import { Validators, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms'; import { assertPathIsCurrent, FieldPathNode, isArray, addDefaultField, metadata, createMetadataKey, MAX, MAX_LENGTH, MIN, MIN_LENGTH, PATTERN, REQUIRED, createManagedMetadataKey, DEBOUNCER } from './_structure-chunk.mjs'; export { MetadataKey, MetadataReducer, apply, applyEach, applyWhen, applyWhenValue, form, schema, submit } from './_structure-chunk.mjs'; import { httpResource } from '@angular/common/http'; import '@angular/core/primitives/signals'; const SIGNAL_FORMS_CONFIG = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'SIGNAL_FORMS_CONFIG' : ''); function provideSignalFormsConfig(config) { return [{ provide: SIGNAL_FORMS_CONFIG, useValue: config }]; } class InteropNgControl { field; constructor(field) { this.field = field; } control = this; get value() { return this.field().value(); } get valid() { return this.field().valid(); } get invalid() { return this.field().invalid(); } get pending() { return this.field().pending(); } get disabled() { return this.field().disabled(); } get enabled() { return !this.field().disabled(); } get errors() { const errors = this.field().errors(); if (errors.length === 0) { return null; } const errObj = {}; for (const error of errors) { errObj[error.kind] = error; } return errObj; } get pristine() { return !this.field().dirty(); } get dirty() { return this.field().dirty(); } get touched() { return this.field().touched(); } get untouched() { return !this.field().touched(); } get status() { if (this.field().disabled()) { return 'DISABLED'; } if (this.field().valid()) { return 'VALID'; } if (this.field().invalid()) { return 'INVALID'; } if (this.field().pending()) { return 'PENDING'; } throw Error('AssertionError: unknown form control status'); } valueAccessor = null; hasValidator(validator) { if (validator === Validators.required) { return this.field().required(); } return false; } updateValueAndValidity() {} } const FIELD = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FIELD' : ''); const controlInstructions = { create: __controlCreate, update: _controlUpdate }; class Field { element = inject(ElementRef).nativeElement; injector = inject(Injector); field = input.required(...(ngDevMode ? [{ debugName: "field" }] : [])); state = computed(() => this.field()(), ...(ngDevMode ? [{ debugName: "state" }] : [])); [_CONTROL] = controlInstructions; config = inject(SIGNAL_FORMS_CONFIG, { optional: true }); classes = Object.entries(this.config?.classes ?? {}).map(([className, computation]) => [className, computed(() => computation(this.state()))]); controlValueAccessors = inject(NG_VALUE_ACCESSOR, { optional: true, self: true }); interopNgControl; get ɵinteropControl() { return this.controlValueAccessors?.[0] ?? this.interopNgControl?.valueAccessor ?? undefined; } getOrCreateNgControl() { return this.interopNgControl ??= new InteropNgControl(this.state); } ɵregister() { effect(onCleanup => { const fieldNode = this.state(); fieldNode.nodeState.fieldBindings.update(controls => [...controls, this]); onCleanup(() => { fieldNode.nodeState.fieldBindings.update(controls => controls.filter(c => c !== this)); }); }, { injector: this.injector }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: Field, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.5", type: Field, isStandalone: true, selector: "[field]", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null } }, providers: [{ provide: FIELD, useExisting: Field }, { provide: NgControl, useFactory: () => inject(Field).getOrCreateNgControl() }], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: Field, decorators: [{ type: Directive, args: [{ selector: '[field]', providers: [{ provide: FIELD, useExisting: Field }, { provide: NgControl, useFactory: () => inject(Field).getOrCreateNgControl() }] }] }], propDecorators: { field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: true }] }] } }); function disabled(path, logic) { assertPathIsCurrent(path); const pathNode = FieldPathNode.unwrapFieldPath(path); pathNode.builder.addDisabledReasonRule(ctx => { let result = true; if (typeof logic === 'string') { result = logic; } else if (logic) { result = logic(ctx); } if (typeof result === 'string') { return { field: ctx.field, message: result }; } return result ? { field: ctx.field } : undefined; }); } function hidden(path, logic) { assertPathIsCurrent(path); const pathNode = FieldPathNode.unwrapFieldPath(path); pathNode.builder.addHiddenRule(logic); } function readonly(path, logic = () => true) { assertPathIsCurrent(path); const pathNode = FieldPathNode.unwrapFieldPath(path); pathNode.builder.addReadonlyRule(logic); } function requiredError(options) { return new RequiredValidationError(options); } function minError(min, options) { return new MinValidationError(min, options); } function maxError(max, options) { return new MaxValidationError(max, options); } function minLengthError(minLength, options) { return new MinLengthValidationError(minLength, options); } function maxLengthError(maxLength, options) { return new MaxLengthValidationError(maxLength, options); } function patternError(pattern, options) { return new PatternValidationError(pattern, options); } function emailError(options) { return new EmailValidationError(options); } function standardSchemaError(issue, options) { return new StandardSchemaValidationError(issue, options); } function customError(obj) { return new CustomValidationError(obj); } class CustomValidationError { __brand = undefined; kind = ''; field; message; constructor(options) { if (options) { Object.assign(this, options); } } } class _NgValidationError { __brand = undefined; kind = ''; field; message; constructor(options) { if (options) { Object.assign(this, options); } } } class RequiredValidationError extends _NgValidationError { kind = 'required'; } class MinValidationError extends _NgValidationError { min; kind = 'min'; constructor(min, options) { super(options); this.min = min; } } class MaxValidationError extends _NgValidationError { max; kind = 'max'; constructor(max, options) { super(options); this.max = max; } } class MinLengthValidationError extends _NgValidationError { minLength; kind = 'minLength'; constructor(minLength, options) { super(options); this.minLength = minLength; } } class MaxLengthValidationError extends _NgValidationError { maxLength; kind = 'maxLength'; constructor(maxLength, options) { super(options); this.maxLength = maxLength; } } class PatternValidationError extends _NgValidationError { pattern; kind = 'pattern'; constructor(pattern, options) { super(options); this.pattern = pattern; } } class EmailValidationError extends _NgValidationError { kind = 'email'; } class StandardSchemaValidationError extends _NgValidationError { issue; kind = 'standardSchema'; constructor(issue, options) { super(options); this.issue = issue; } } const NgValidationError = _NgValidationError; function getLengthOrSize(value) { const v = value; return typeof v.length === 'number' ? v.length : v.size; } function getOption(opt, ctx) { return opt instanceof Function ? opt(ctx) : opt; } function isEmpty(value) { if (typeof value === 'number') { return isNaN(value); } return value === '' || value === false || value == null; } function isPlainError(error) { return typeof error === 'object' && (Object.getPrototypeOf(error) === Object.prototype || Object.getPrototypeOf(error) === null); } function ensureCustomValidationError(error) { if (isPlainError(error)) { return customError(error); } return error; } function ensureCustomValidationResult(result) { if (result === null || result === undefined) { return result; } if (isArray(result)) { return result.map(ensureCustomValidationError); } return ensureCustomValidationError(result); } function validate(path, logic) { assertPathIsCurrent(path); const pathNode = FieldPathNode.unwrapFieldPath(path); pathNode.builder.addSyncErrorRule(ctx => { return ensureCustomValidationResult(addDefaultField(logic(ctx), ctx.field)); }); } 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 email(path, config) { validate(path, ctx => { if (isEmpty(ctx.value())) { return undefined; } if (!EMAIL_REGEXP.test(ctx.value())) { if (config?.error) { return getOption(config.error, ctx); } else { return emailError({ message: getOption(config?.message, ctx) }); } } return undefined; }); } function max(path, maxValue, config) { const MAX_MEMO = metadata(path, createMetadataKey(), ctx => typeof maxValue === 'number' ? maxValue : maxValue(ctx)); metadata(path, MAX, ({ state }) => state.metadata(MAX_MEMO)()); validate(path, ctx => { if (isEmpty(ctx.value())) { return undefined; } const max = ctx.state.metadata(MAX_MEMO)(); if (max === undefined || Number.isNaN(max)) { return undefined; } const value = ctx.value(); const numValue = !value && value !== 0 ? NaN : Number(value); if (numValue > max) { if (config?.error) { return getOption(config.error, ctx); } else { return maxError(max, { message: getOption(config?.message, ctx) }); } } return undefined; }); } function maxLength(path, maxLength, config) { const MAX_LENGTH_MEMO = metadata(path, createMetadataKey(), ctx => typeof maxLength === 'number' ? maxLength : maxLength(ctx)); metadata(path, MAX_LENGTH, ({ state }) => state.metadata(MAX_LENGTH_MEMO)()); validate(path, ctx => { if (isEmpty(ctx.value())) { return undefined; } const maxLength = ctx.state.metadata(MAX_LENGTH_MEMO)(); if (maxLength === undefined) { return undefined; } if (getLengthOrSize(ctx.value()) > maxLength) { if (config?.error) { return getOption(config.error, ctx); } else { return maxLengthError(maxLength, { message: getOption(config?.message, ctx) }); } } return undefined; }); } function min(path, minValue, config) { const MIN_MEMO = metadata(path, createMetadataKey(), ctx => typeof minValue === 'number' ? minValue : minValue(ctx)); metadata(path, MIN, ({ state }) => state.metadata(MIN_MEMO)()); validate(path, ctx => { if (isEmpty(ctx.value())) { return undefined; } const min = ctx.state.metadata(MIN_MEMO)(); if (min === undefined || Number.isNaN(min)) { return undefined; } const value = ctx.value(); const numValue = !value && value !== 0 ? NaN : Number(value); if (numValue < min) { if (config?.error) { return getOption(config.error, ctx); } else { return minError(min, { message: getOption(config?.message, ctx) }); } } return undefined; }); } function minLength(path, minLength, config) { const MIN_LENGTH_MEMO = metadata(path, createMetadataKey(), ctx => typeof minLength === 'number' ? minLength : minLength(ctx)); metadata(path, MIN_LENGTH, ({ state }) => state.metadata(MIN_LENGTH_MEMO)()); validate(path, ctx => { if (isEmpty(ctx.value())) { return undefined; } const minLength = ctx.state.metadata(MIN_LENGTH_MEMO)(); if (minLength === undefined) { return undefined; } if (getLengthOrSize(ctx.value()) < minLength) { if (config?.error) { return getOption(config.error, ctx); } else { return minLengthError(minLength, { message: getOption(config?.message, ctx) }); } } return undefined; }); } function pattern(path, pattern, config) { const PATTERN_MEMO = metadata(path, createMetadataKey(), ctx => pattern instanceof RegExp ? pattern : pattern(ctx)); metadata(path, PATTERN, ({ state }) => state.metadata(PATTERN_MEMO)()); validate(path, ctx => { if (isEmpty(ctx.value())) { return undefined; } const pattern = ctx.state.metadata(PATTERN_MEMO)(); if (pattern === undefined) { return undefined; } if (!pattern.test(ctx.value())) { if (config?.error) { return getOption(config.error, ctx); } else { return patternError(pattern, { message: getOption(config?.message, ctx) }); } } return undefined; }); } function required(path, config) { const REQUIRED_MEMO = metadata(path, createMetadataKey(), ctx => config?.when ? config.when(ctx) : true); metadata(path, REQUIRED, ({ state }) => state.metadata(REQUIRED_MEMO)()); validate(path, ctx => { if (ctx.state.metadata(REQUIRED_MEMO)() && isEmpty(ctx.value())) { if (config?.error) { return getOption(config.error, ctx); } else { return requiredError({ message: getOption(config?.message, ctx) }); } } return undefined; }); } function validateAsync(path, opts) { assertPathIsCurrent(path); const pathNode = FieldPathNode.unwrapFieldPath(path); const RESOURCE = createManagedMetadataKey(opts.factory); metadata(path, RESOURCE, ctx => { const node = ctx.stateOf(path); const validationState = node.validationState; if (validationState.shouldSkipValidation() || !validationState.syncValid()) { return undefined; } return opts.params(ctx); }); pathNode.builder.addAsyncErrorRule(ctx => { const res = ctx.state.metadata(RESOURCE); let errors; switch (res.status()) { case 'idle': return undefined; case 'loading': case 'reloading': return 'pending'; case 'resolved': case 'local': if (!res.hasValue()) { return undefined; } errors = opts.onSuccess(res.value(), ctx); return addDefaultField(errors, ctx.field); case 'error': errors = opts.onError(res.error(), ctx); return addDefaultField(errors, ctx.field); } }); } function validateTree(path, logic) { assertPathIsCurrent(path); const pathNode = FieldPathNode.unwrapFieldPath(path); pathNode.builder.addSyncTreeErrorRule(ctx => addDefaultField(logic(ctx), ctx.field)); } function validateStandardSchema(path, schema) { const VALIDATOR_MEMO = metadata(path, createMetadataKey(), ({ value }) => { return schema['~standard'].validate(value()); }); validateTree(path, ({ state, fieldTreeOf }) => { const result = state.metadata(VALIDATOR_MEMO)(); if (_isPromise(result)) { return []; } return result?.issues?.map(issue => standardIssueToFormTreeError(fieldTreeOf(path), issue)) ?? []; }); validateAsync(path, { params: ({ state }) => { const result = state.metadata(VALIDATOR_MEMO)(); return _isPromise(result) ? result : undefined; }, factory: params => { return resource({ params, loader: async ({ params }) => (await params)?.issues ?? [] }); }, onSuccess: (issues, { fieldTreeOf }) => { return issues.map(issue => standardIssueToFormTreeError(fieldTreeOf(path), issue)); }, onError: () => {} }); } function standardIssueToFormTreeError(field, issue) { let target = field; for (const pathPart of issue.path ?? []) { const pathKey = typeof pathPart === 'object' ? pathPart.key : pathPart; target = target[pathKey]; } return addDefaultField(standardSchemaError(issue, { message: issue.message }), target); } function validateHttp(path, opts) { validateAsync(path, { params: opts.request, factory: request => httpResource(request, opts.options), onSuccess: opts.onSuccess, onError: opts.onError }); } function debounce(path, durationOrDebouncer) { assertPathIsCurrent(path); const pathNode = FieldPathNode.unwrapFieldPath(path); const debouncer = typeof durationOrDebouncer === 'function' ? durationOrDebouncer : durationOrDebouncer > 0 ? debounceForDuration(durationOrDebouncer) : immediate; pathNode.builder.addMetadataRule(DEBOUNCER, () => debouncer); } function debounceForDuration(durationInMilliseconds) { return (_context, abortSignal) => { return new Promise(resolve => { const timeoutId = setTimeout(resolve, durationInMilliseconds); abortSignal.addEventListener('abort', () => clearTimeout(timeoutId)); }); }; } function immediate() {} export { CustomValidationError, EmailValidationError, FIELD, Field, MAX, MAX_LENGTH, MIN, MIN_LENGTH, MaxLengthValidationError, MaxValidationError, MinLengthValidationError, MinValidationError, NgValidationError, PATTERN, PatternValidationError, REQUIRED, RequiredValidationError, StandardSchemaValidationError, createManagedMetadataKey, createMetadataKey, customError, debounce, disabled, email, emailError, hidden, max, maxError, maxLength, maxLengthError, metadata, min, minError, minLength, minLengthError, pattern, patternError, provideSignalFormsConfig, readonly, required, requiredError, standardSchemaError, validate, validateAsync, validateHttp, validateStandardSchema, validateTree }; //# sourceMappingURL=signals.mjs.map