@angular/forms
Version:
Angular - directives and services for creating forms
1,658 lines (1,620 loc) • 48.6 kB
JavaScript
/**
* @license Angular v22.0.2
* (c) 2010-2026 Google LLC. https://angular.dev/
* License: MIT
*/
import * as i0 from '@angular/core';
import { InjectionToken, debounced, computed, ɵchain as _chain, resource, ɵisPromise as _isPromise, linkedSignal, inject, ɵRuntimeError as _RuntimeError, untracked, signal, CSP_NONCE, Injectable, forwardRef, input, Renderer2, DestroyRef, Injector, ElementRef, afterRenderEffect, effect, ɵformatRuntimeError as _formatRuntimeError, Directive, makeEnvironmentProviders, declareExperimentalWebMcpTool } from '@angular/core';
import { ɵFORM_CONTROL_INTEGRATION as _FORM_CONTROL_INTEGRATION, Validators, ɵsetNativeDomProperty as _setNativeDomProperty, NG_VALIDATORS, ɵisNativeFormElement as _isNativeFormElement, ɵisTextualFormElement as _isTextualFormElement, NG_VALUE_ACCESSOR, ɵselectValueAccessor as _selectValueAccessor, ɵelementAcceptsMinMax as _elementAcceptsMinMax, NgControl } from '@angular/forms';
import { assertPathIsCurrent, FieldPathNode, addDefaultField, createMetadataKey, metadata, MAX_NUMBER, MAX, MAX_DATE, MAX_LENGTH, MIN_NUMBER, MIN, MIN_DATE, MIN_LENGTH, PATTERN, REQUIRED, createManagedMetadataKey, IS_ASYNC_VALIDATION_RESOURCE, DEBOUNCER, shallowArrayEquals, signalErrorsToValidationErrors, reactiveErrorsToSignalErrors, submit, REGISTER_WEBMCP_FORM } from './_validation_errors-chunk.mjs';
export { MetadataKey, MetadataReducer, apply, applyEach, applyWhen, applyWhenValue, createLimitSelectionKey, form, schema } from './_validation_errors-chunk.mjs';
import { DOCUMENT } from '@angular/common';
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
}];
}
function disabled(path, configOrLogic) {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
let logic;
if (typeof configOrLogic === 'function' || typeof configOrLogic === 'string') {
logic = configOrLogic;
} else {
logic = configOrLogic?.when;
}
pathNode.builder.addDisabledReasonRule(ctx => {
let result = true;
if (typeof logic === 'string') {
result = logic;
} else if (logic) {
result = logic(ctx);
}
if (typeof result === 'string') {
return {
fieldTree: ctx.fieldTree,
message: result
};
}
return result ? {
fieldTree: ctx.fieldTree
} : undefined;
});
}
function hidden(path, configOrLogic) {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
const logic = typeof configOrLogic === 'function' ? configOrLogic : configOrLogic.when;
pathNode.builder.addHiddenRule(logic);
}
function readonly(path, configOrLogic) {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
let logic;
if (typeof configOrLogic === 'object' && configOrLogic !== null && 'when' in configOrLogic) {
logic = configOrLogic.when ?? (() => true);
} else if (typeof configOrLogic === 'function') {
logic = configOrLogic;
} else {
logic = () => true;
}
pathNode.builder.addReadonlyRule(logic);
}
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 normalizeErrors(error) {
if (error === undefined) {
return [];
}
if (Array.isArray(error)) {
return error;
}
return [error];
}
function validate(path, logic) {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
pathNode.builder.addSyncErrorRule(ctx => {
return addDefaultField(logic(ctx), ctx.fieldTree);
});
}
function requiredError(options) {
return new RequiredValidationError(options);
}
function minError(min, options) {
return new MinValidationError(min, options);
}
function minDateError(minDate, options) {
return new MinDateValidationError(minDate, options);
}
function maxError(max, options) {
return new MaxValidationError(max, options);
}
function maxDateError(maxDate, options) {
return new MaxDateValidationError(maxDate, 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);
}
class BaseNgValidationError {
__brand = undefined;
kind = '';
fieldTree;
message;
constructor(options) {
if (options) {
Object.assign(this, options);
}
}
}
class RequiredValidationError extends BaseNgValidationError {
kind = 'required';
}
class MinValidationError extends BaseNgValidationError {
min;
kind = 'min';
constructor(min, options) {
super(options);
this.min = min;
}
}
class MinDateValidationError extends BaseNgValidationError {
minDate;
kind = 'minDate';
constructor(minDate, options) {
super(options);
this.minDate = minDate;
}
}
class MaxValidationError extends BaseNgValidationError {
max;
kind = 'max';
constructor(max, options) {
super(options);
this.max = max;
}
}
class MaxDateValidationError extends BaseNgValidationError {
maxDate;
kind = 'maxDate';
constructor(maxDate, options) {
super(options);
this.maxDate = maxDate;
}
}
class MinLengthValidationError extends BaseNgValidationError {
minLength;
kind = 'minLength';
constructor(minLength, options) {
super(options);
this.minLength = minLength;
}
}
class MaxLengthValidationError extends BaseNgValidationError {
maxLength;
kind = 'maxLength';
constructor(maxLength, options) {
super(options);
this.maxLength = maxLength;
}
}
class PatternValidationError extends BaseNgValidationError {
pattern;
kind = 'pattern';
constructor(pattern, options) {
super(options);
this.pattern = pattern;
}
}
class EmailValidationError extends BaseNgValidationError {
kind = 'email';
}
class NativeInputParseError extends BaseNgValidationError {
kind = 'parse';
}
const NgValidationError = BaseNgValidationError;
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 (config?.when && !config.when(ctx)) {
return undefined;
}
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 = createMetadataKey();
metadata(path, MAX_MEMO, ctx => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return typeof maxValue === 'function' ? maxValue(ctx) : maxValue;
});
metadata(path, MAX_NUMBER, ({
state
}) => state.metadata(MAX_MEMO)());
metadata(path, MAX, () => MAX_NUMBER);
validate(path, ctx => {
const value = ctx.value();
if (value === null || Number.isNaN(value)) {
return undefined;
}
const max = ctx.state.metadata(MAX_MEMO)();
if (max === undefined || Number.isNaN(max)) {
return undefined;
}
if (value > max) {
if (config?.error) {
return getOption(config.error, ctx);
} else {
return maxError(max, {
message: getOption(config?.message, ctx)
});
}
}
return undefined;
});
}
function maxDate(path, maxDateValue, config) {
const MAX_MEMO = createMetadataKey();
metadata(path, MAX_MEMO, ctx => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return typeof maxDateValue === 'function' ? maxDateValue(ctx) : maxDateValue;
});
metadata(path, MAX_DATE, ({
state
}) => state.metadata(MAX_MEMO)());
metadata(path, MAX, () => MAX_DATE);
validate(path, ctx => {
const value = ctx.value();
if (value === null || Number.isNaN(value.getTime())) {
return undefined;
}
const max = ctx.state.metadata(MAX_MEMO)();
if (max === undefined || Number.isNaN(max.getTime())) {
return undefined;
}
if (value > max) {
if (config?.error) {
return getOption(config.error, ctx);
} else {
return maxDateError(max, {
message: getOption(config?.message, ctx)
});
}
}
return undefined;
});
}
function maxLength(path, maxLength, config) {
const MAX_LENGTH_MEMO = metadata(path, createMetadataKey(), ctx => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return 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 = createMetadataKey();
metadata(path, MIN_MEMO, ctx => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return typeof minValue === 'function' ? minValue(ctx) : minValue;
});
metadata(path, MIN_NUMBER, ({
state
}) => state.metadata(MIN_MEMO)());
metadata(path, MIN, () => MIN_NUMBER);
validate(path, ctx => {
const value = ctx.value();
if (value === null || Number.isNaN(value)) {
return undefined;
}
const min = ctx.state.metadata(MIN_MEMO)();
if (min === undefined || Number.isNaN(min)) {
return undefined;
}
if (value < min) {
if (config?.error) {
return getOption(config.error, ctx);
} else {
return minError(min, {
message: getOption(config?.message, ctx)
});
}
}
return undefined;
});
}
function minDate(path, minDateValue, config) {
const MIN_MEMO = createMetadataKey();
metadata(path, MIN_MEMO, ctx => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return typeof minDateValue === 'function' ? minDateValue(ctx) : minDateValue;
});
metadata(path, MIN_DATE, ({
state
}) => state.metadata(MIN_MEMO)());
metadata(path, MIN, () => MIN_DATE);
validate(path, ctx => {
const value = ctx.value();
if (value === null || Number.isNaN(value.getTime())) {
return undefined;
}
const min = ctx.state.metadata(MIN_MEMO)();
if (min === undefined || Number.isNaN(min.getTime())) {
return undefined;
}
if (value < min) {
if (config?.error) {
return getOption(config.error, ctx);
} else {
return minDateError(min, {
message: getOption(config?.message, ctx)
});
}
}
return undefined;
});
}
function minLength(path, minLength, config) {
const MIN_LENGTH_MEMO = metadata(path, createMetadataKey(), ctx => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return 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 => {
if (config?.when && !config.when(ctx)) {
return undefined;
}
return 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((_state, params) => {
if (opts.debounce !== undefined) {
const debouncedResource = debounced(() => params(), opts.debounce);
const wrappedParams = computed(() => _chain(debouncedResource), ...(ngDevMode ? [{
debugName: "wrappedParams"
}] : []));
return opts.factory(wrappedParams);
}
return opts.factory(params);
});
RESOURCE[IS_ASYNC_VALIDATION_RESOURCE] = true;
metadata(path, RESOURCE, ctx => {
const node = ctx.stateOf(path);
const validationState = node.validationState;
if (validationState.shouldSkipValidation() || !validationState.syncValid()) {
return undefined;
}
if (opts.when && !opts.when(ctx)) {
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.fieldTree);
case 'error':
errors = opts.onError(res.error(), ctx);
return addDefaultField(errors, ctx.fieldTree);
}
});
}
function validateTree(path, logic) {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
pathNode.builder.addSyncTreeErrorRule(ctx => addDefaultField(logic(ctx), ctx.fieldTree));
}
function validateStandardSchema(path, schema) {
const VALIDATOR_MEMO = metadata(path, createMetadataKey(), ctx => {
const resolvedSchema = typeof schema === 'function' ? schema(ctx) : schema;
return resolvedSchema ? resolvedSchema['~standard'].validate(ctx.value()) : undefined;
});
validateTree(path, ({
state,
fieldTreeOf
}) => {
const result = state.metadata(VALIDATOR_MEMO)();
if (!result || _isPromise(result)) {
return [];
}
return result?.issues?.map(issue => standardIssueToFormTreeError(fieldTreeOf(path), issue)) ?? [];
});
validateAsync(path, {
params: ({
state
}) => {
const result = state.metadata(VALIDATOR_MEMO)();
return result && _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 standardSchemaError(issue, options) {
return new StandardSchemaValidationError(issue, options);
}
function standardIssueToFormTreeError(fieldTree, issue) {
let target = fieldTree;
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);
}
class StandardSchemaValidationError extends BaseNgValidationError {
issue;
kind = 'standardSchema';
constructor(issue, options) {
super(options);
this.issue = issue;
}
}
function validateHttp(path, opts) {
validateAsync(path, {
params: opts.request,
debounce: opts.debounce,
factory: request => httpResource(request, opts.options),
onSuccess: opts.onSuccess,
onError: opts.onError,
when: opts.when
});
}
function debounce(path, config) {
assertPathIsCurrent(path);
const pathNode = FieldPathNode.unwrapFieldPath(path);
const debouncer = normalizeDebouncer(config);
pathNode.builder.addMetadataRule(DEBOUNCER, () => debouncer);
}
function normalizeDebouncer(debouncer) {
if (typeof debouncer === 'function') {
return debouncer;
}
if (debouncer === 'blur') {
return debounceUntilBlur();
}
if (debouncer > 0) {
return debounceForDuration(debouncer);
}
return immediate;
}
function debounceForDuration(durationInMilliseconds) {
return (_context, abortSignal) => {
return new Promise(resolve => {
let timeoutId;
const onAbort = () => {
clearTimeout(timeoutId);
resolve();
};
timeoutId = setTimeout(() => {
abortSignal.removeEventListener('abort', onAbort);
resolve();
}, durationInMilliseconds);
abortSignal.addEventListener('abort', onAbort, {
once: true
});
});
};
}
function debounceUntilBlur() {
return (_context, abortSignal) => {
return new Promise(resolve => {
abortSignal.addEventListener('abort', () => resolve(), {
once: true
});
});
};
}
function immediate() {}
function createParser(getValue, setValue, parse) {
const errors = linkedSignal({
...(ngDevMode ? {
debugName: "errors"
} : {}),
source: getValue,
computation: () => [],
equal: shallowArrayEquals
});
const setRawValue = rawValue => {
const result = parse(rawValue);
errors.set(normalizeErrors(result.error));
if (result.value !== undefined) {
setValue(result.value);
}
errors.set(normalizeErrors(result.error));
};
const reset = () => {
errors.set([]);
};
return {
errors: errors.asReadonly(),
setRawValue,
reset
};
}
function transformedValue(value, options) {
const {
parse,
format
} = options;
const parser = createParser(value, value.set, parse);
const rawValue = linkedSignal(() => format(value()), ...(ngDevMode ? [{
debugName: "rawValue"
}] : []));
const result = rawValue;
result.parseErrors = parser.errors;
const originalSet = result.set.bind(result);
const integration = inject(_FORM_CONTROL_INTEGRATION, {
self: true,
optional: true
});
if (integration) {
integration.setParseErrors(parser.errors);
integration.onReset = resetValue => {
parser.reset();
const modelValue = resetValue !== undefined ? resetValue : value();
originalSet(format(modelValue));
};
}
result.set = newRawValue => {
parser.setRawValue(newRawValue);
originalSet(newRawValue);
};
result.update = updateFn => {
result.set(updateFn(rawValue()));
};
return result;
}
class InteropNgControl {
field;
constructor(field) {
this.field = field;
}
control = this;
get value() {
return this.field().controlValue();
}
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() {
return signalErrorsToValidationErrors(this.field().errors());
}
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 new _RuntimeError(1910, ngDevMode && 'Unknown form control status');
}
valueAccessor = null;
hasValidator(validator) {
if (validator === Validators.required) {
return this.field().required();
}
return false;
}
updateValueAndValidity() {}
}
const FIELD_STATE_KEY_TO_CONTROL_BINDING = {
disabled: 'disabled',
disabledReasons: 'disabledReasons',
dirty: 'dirty',
errors: 'errors',
hidden: 'hidden',
invalid: 'invalid',
max: 'max',
maxLength: 'maxLength',
min: 'min',
minLength: 'minLength',
name: 'name',
pattern: 'pattern',
pending: 'pending',
readonly: 'readonly',
required: 'required',
touched: 'touched'
};
const CONTROL_BINDING_TO_FIELD_STATE_KEY = /* @__PURE__ */(() => {
const map = {};
for (const key of Object.keys(FIELD_STATE_KEY_TO_CONTROL_BINDING)) {
map[FIELD_STATE_KEY_TO_CONTROL_BINDING[key]] = key;
}
return map;
})();
function readFieldStateBindingValue(fieldState, key) {
const property = CONTROL_BINDING_TO_FIELD_STATE_KEY[key];
return fieldState[property]?.();
}
const CONTROL_BINDING_NAMES = /* @__PURE__ */(() => Object.values(FIELD_STATE_KEY_TO_CONTROL_BINDING))();
function createBindings() {
return {};
}
function bindingUpdated(bindings, key, value) {
if (bindings[key] !== value) {
bindings[key] = value;
return true;
}
return false;
}
function getNativeControlValue(element, currentValue, validityMonitor) {
let modelValue;
if (isInput(element) && validityMonitor.isBadInput(element)) {
return {
error: new NativeInputParseError()
};
}
switch (element.type) {
case 'checkbox':
return {
value: element.checked
};
case 'number':
case 'range':
case 'datetime-local':
modelValue = untracked(currentValue);
if (typeof modelValue === 'number' || modelValue === null) {
return {
value: element.value === '' ? null : element.valueAsNumber
};
}
break;
case 'date':
case 'month':
case 'time':
case 'week':
modelValue = untracked(currentValue);
if (modelValue === null || modelValue instanceof Date) {
return {
value: element.valueAsDate
};
} else if (typeof modelValue === 'number') {
return {
value: element.valueAsNumber
};
}
break;
}
if (element.tagName === 'INPUT' && element.type === 'text') {
modelValue ??= untracked(currentValue);
if (typeof modelValue === 'number' || modelValue === null) {
if (element.value === '') {
return {
value: null
};
}
const parsed = Number(element.value);
if (Number.isNaN(parsed)) {
return {
error: new NativeInputParseError()
};
}
return {
value: parsed
};
}
}
return {
value: element.value
};
}
function setNativeControlValue(element, value) {
switch (element.type) {
case 'checkbox':
element.checked = value;
return;
case 'radio':
element.checked = value === element.value;
return;
case 'number':
case 'range':
case 'datetime-local':
if (typeof value === 'number') {
setNativeNumberControlValue(element, value);
return;
} else if (value === null) {
element.value = '';
return;
}
break;
case 'date':
case 'month':
case 'time':
case 'week':
if (value === null || value instanceof Date) {
element.valueAsDate = value;
return;
} else if (typeof value === 'number') {
setNativeNumberControlValue(element, value);
return;
}
}
if (element.tagName === 'INPUT' && element.type === 'text') {
if (typeof value === 'number') {
element.value = isNaN(value) ? '' : String(value);
return;
}
if (value === null) {
element.value = '';
return;
}
}
element.value = value;
}
function setNativeNumberControlValue(element, value) {
if (isNaN(value)) {
element.value = '';
} else {
element.valueAsNumber = value;
}
}
function isInput(element) {
return element.tagName === 'INPUT';
}
function inputRequiresValidityTracking(input) {
return input.type === 'date' || input.type === 'datetime-local' || input.type === 'month' || input.type === 'time' || input.type === 'week';
}
function formatDateForInput(date, type) {
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
if (type === 'month') {
return `${year}-${month}`;
}
const day = String(date.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDateForMinMax(name, value, type) {
if (value instanceof Date && (name === 'min' || name === 'max') && (type === 'date' || type === 'month')) {
return formatDateForInput(value, type);
}
return value;
}
function customControlCreate(host, parent) {
host.listenToCustomControlModel(value => parent.state().controlValue.set(value));
host.listenToCustomControlOutput('touch', () => parent.state().markAsTouched());
parent.registerAsBinding(host.customControl);
const bindings = createBindings();
return () => {
const state = parent.state();
const controlValue = state.controlValue();
if (bindingUpdated(bindings, 'controlValue', controlValue)) {
host.setCustomControlModelInput(controlValue);
}
for (const name of CONTROL_BINDING_NAMES) {
let value;
if (name === 'errors') {
value = parent.errors();
} else {
value = readFieldStateBindingValue(state, name);
}
if (bindingUpdated(bindings, name, value)) {
host.setInputOnDirectives(name, value);
if (parent.elementAcceptsNativeProperty(name) && !host.customControlHasInput(name)) {
const domValue = formatDateForMinMax(name, value, parent.nativeFormElement.type);
_setNativeDomProperty(parent.renderer, parent.nativeFormElement, name, domValue);
}
}
}
};
}
function isValidatorObject(v) {
return typeof v === 'object' && v !== null;
}
function cvaControlCreate(host, parent) {
const bindings = createBindings();
parent.controlValueAccessor.registerOnChange(value => {
bindings['controlValue'] = value;
parent.state().controlValue.set(value);
});
parent.controlValueAccessor.registerOnTouched(() => parent.state().markAsTouched());
const legacyValidators = parent.injector.get(NG_VALIDATORS, null, {
optional: true,
self: true
});
if (legacyValidators) {
let version;
for (const v of legacyValidators) {
if (isValidatorObject(v) && v.registerOnValidatorChange) {
version ??= signal(0);
v.registerOnValidatorChange(() => {
version.update(n => n + 1);
});
}
}
const validatorFns = legacyValidators.map(v => typeof v === 'function' ? v : v.validate.bind(v));
const mergedValidator = Validators.compose(validatorFns);
const parseErrors = computed(() => {
version?.();
const errors = mergedValidator ? mergedValidator(parent.interopNgControl.control) : null;
return reactiveErrorsToSignalErrors(errors, parent.interopNgControl.control);
}, ...(ngDevMode ? [{
debugName: "parseErrors"
}] : []));
parent.parseErrorsSource.set(parseErrors);
}
parent.registerAsBinding({
reset: () => {
const value = parent.state().value();
bindings['controlValue'] = value;
untracked(() => parent.controlValueAccessor.writeValue(value));
}
});
return () => {
const fieldState = parent.state();
const value = fieldState.value();
if (bindingUpdated(bindings, 'controlValue', value)) {
untracked(() => parent.controlValueAccessor.writeValue(value));
}
for (const name of CONTROL_BINDING_NAMES) {
const value = readFieldStateBindingValue(fieldState, name);
if (bindingUpdated(bindings, name, value)) {
const propertyWasSet = host.setInputOnDirectives(name, value);
if (name === 'disabled' && parent.controlValueAccessor.setDisabledState) {
untracked(() => parent.controlValueAccessor.setDisabledState(value));
} else if (!propertyWasSet && parent.elementAcceptsNativeProperty(name)) {
_setNativeDomProperty(parent.renderer, parent.nativeFormElement, name, value);
}
}
}
};
}
function observeSelectMutations(select, onMutation, destroyRef) {
if (typeof MutationObserver !== 'function') {
return;
}
const observer = new MutationObserver(mutations => {
if (mutations.some(m => isRelevantSelectMutation(m))) {
onMutation();
}
});
observer.observe(select, {
attributes: true,
attributeFilter: ['value'],
characterData: true,
childList: true,
subtree: true
});
destroyRef.onDestroy(() => observer.disconnect());
}
function isRelevantSelectMutation(mutation) {
if (mutation.type === 'childList' || mutation.type === 'characterData') {
if (mutation.target instanceof Comment) {
return false;
}
for (const node of mutation.addedNodes) {
if (!(node instanceof Comment)) {
return true;
}
}
for (const node of mutation.removedNodes) {
if (!(node instanceof Comment)) {
return true;
}
}
return false;
}
if (mutation.type === 'attributes' && mutation.target instanceof HTMLOptionElement) {
return true;
}
return false;
}
function nativeControlCreate(host, parent, parseErrorsSource, validityMonitor) {
let updateMode = false;
const input = parent.nativeFormElement;
const parser = createParser(() => parent.state().value(), rawValue => parent.state().controlValue.set(rawValue), _rawValue => getNativeControlValue(input, parent.state().value, validityMonitor));
parseErrorsSource.set(parser.errors);
parent.onReset = () => {
parser.reset();
const value = parent.state().value();
bindings['controlValue'] = value;
setNativeControlValue(input, value);
};
host.listenToDom('input', () => parser.setRawValue(undefined));
host.listenToDom('blur', () => parent.state().markAsTouched());
if (isInput(input) && inputRequiresValidityTracking(input)) {
validityMonitor.watchValidity(parent.destroyRef, input, () => parser.setRawValue(undefined));
}
parent.registerAsBinding();
if (input.tagName === 'SELECT') {
observeSelectMutations(input, () => {
if (!updateMode) {
return;
}
input.value = parent.state().controlValue();
}, parent.destroyRef);
}
const bindings = createBindings();
return () => {
const state = parent.state();
for (const name of CONTROL_BINDING_NAMES) {
const value = readFieldStateBindingValue(state, name);
if (bindingUpdated(bindings, name, value)) {
host.setInputOnDirectives(name, value);
if (parent.elementAcceptsNativeProperty(name)) {
const domValue = formatDateForMinMax(name, value, input.type);
_setNativeDomProperty(parent.renderer, input, name, domValue);
}
}
}
const controlValue = state.controlValue();
if (bindingUpdated(bindings, 'controlValue', controlValue)) {
setNativeControlValue(input, controlValue);
}
updateMode = true;
};
}
class InputValidityMonitor {
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: InputValidityMonitor,
deps: [],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: InputValidityMonitor,
providedIn: 'root',
useClass: i0.forwardRef(() => AnimationInputValidityMonitor)
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: InputValidityMonitor,
decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
useClass: forwardRef(() => AnimationInputValidityMonitor)
}]
}]
});
class AnimationInputValidityMonitor extends InputValidityMonitor {
document = inject(DOCUMENT);
cspNonce = inject(CSP_NONCE, {
optional: true
});
injectedStyles = new WeakMap();
watchValidity(destroyRef, element, callback) {
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
return;
}
const rootNode = element.getRootNode();
if (!this.injectedStyles.has(rootNode)) {
this.injectedStyles.set(rootNode, this.createTransitionStyle(rootNode));
}
const onAnimationStart = event => {
const animationEvent = event;
if (animationEvent.animationName === 'ng-valid' || animationEvent.animationName === 'ng-invalid') {
callback();
}
};
element.addEventListener('animationstart', onAnimationStart);
destroyRef.onDestroy(() => {
element.removeEventListener('animationstart', onAnimationStart);
});
}
isBadInput(element) {
return element.validity?.badInput ?? false;
}
createTransitionStyle(rootNode) {
const element = this.document.createElement('style');
if (this.cspNonce) {
element.nonce = this.cspNonce;
}
element.textContent = `
@keyframes ng-valid {}
@keyframes ng-invalid {}
input:valid, textarea:valid {
animation: ng-valid 0.001s;
}
input:invalid, textarea:invalid {
animation: ng-invalid 0.001s;
}
`;
if (rootNode.nodeType === 9) {
rootNode.head?.appendChild(element);
} else {
rootNode.appendChild(element);
}
return element;
}
ngOnDestroy() {
this.injectedStyles.get(this.document)?.remove();
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: AnimationInputValidityMonitor,
deps: null,
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: AnimationInputValidityMonitor
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: AnimationInputValidityMonitor,
decorators: [{
type: Injectable
}]
});
const ɵNgFieldDirective = Symbol();
const FORM_FIELD = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FORM_FIELD' : '');
class FormField {
field = input.required({
...(ngDevMode ? {
debugName: "field"
} : {}),
alias: 'formField'
});
state = computed(() => this.field()(), ...(ngDevMode ? [{
debugName: "state"
}] : []));
renderer = inject(Renderer2);
destroyRef = inject(DestroyRef);
injector = inject(Injector);
element = inject(ElementRef).nativeElement;
elementIsNativeFormElement = _isNativeFormElement(this.element);
elementAcceptsTextualValues = _isTextualFormElement(this.element);
_elementAcceptsMinMax;
nativeFormElement = this.elementIsNativeFormElement ? this.element : undefined;
focuser = options => this.element.focus(options);
controlValueAccessors = inject(NG_VALUE_ACCESSOR, {
optional: true,
self: true
});
config = inject(SIGNAL_FORMS_CONFIG, {
optional: true
});
validityMonitor = inject(InputValidityMonitor);
parseErrorsSource = signal(undefined, ...(ngDevMode ? [{
debugName: "parseErrorsSource"
}] : []));
_interopNgControl;
get interopNgControl() {
return this._interopNgControl ??= new InteropNgControl(this.state);
}
parseErrors = computed(() => this.parseErrorsSource()?.().map(err => ({
...err,
fieldTree: untracked(this.state).fieldTree,
formField: this
})) ?? [], {
...(ngDevMode ? {
debugName: "parseErrors"
} : {}),
equal: shallowArrayEquals
});
errors = computed(() => this.state().errors().filter(err => !err.formField || err.formField === this), {
...(ngDevMode ? {
debugName: "errors"
} : {}),
equal: shallowArrayEquals
});
isFieldBinding = false;
resetter = () => {};
parseErrorsResetCallback;
setParseErrors(source) {
this.parseErrorsSource.set(source);
}
set onReset(callback) {
this.parseErrorsResetCallback = callback;
}
get onReset() {
return this.parseErrorsResetCallback;
}
get controlValueAccessor() {
if (!this.controlValueAccessors || this.controlValueAccessors.length === 0) {
return this.interopNgControl?.valueAccessor ?? undefined;
}
return _selectValueAccessor(this.interopNgControl, this.controlValueAccessors) ?? undefined;
}
installClassBindingEffect() {
const classes = Object.entries(this.config?.classes ?? {}).map(([className, computation]) => [className, computed(() => computation(this))]);
if (classes.length === 0) {
return;
}
const bindings = createBindings();
afterRenderEffect({
write: () => {
for (const [className, computation] of classes) {
const active = computation();
if (bindingUpdated(bindings, className, active)) {
if (active) {
this.renderer.addClass(this.element, className);
} else {
this.renderer.removeClass(this.element, className);
}
}
}
}
}, {
injector: this.injector
});
}
focus(options) {
this.focuser(options);
}
reset() {
this.resetter();
this.parseErrorsResetCallback?.(this.state().value());
}
registerAsBinding(bindingOptions) {
if (this.isFieldBinding) {
throw new _RuntimeError(1913, typeof ngDevMode !== 'undefined' && ngDevMode && 'FormField already registered as a binding');
}
this.isFieldBinding = true;
this.installClassBindingEffect();
if (bindingOptions?.focus) {
this.focuser = focusOptions => bindingOptions.focus(focusOptions);
}
if (bindingOptions?.reset) {
this.resetter = () => bindingOptions.reset();
}
effect(onCleanup => {
const fieldNode = this.state();
fieldNode.nodeState.formFieldBindings.update(controls => [...controls, this]);
onCleanup(() => {
fieldNode.nodeState.formFieldBindings.update(controls => controls.filter(c => c !== this));
});
}, {
injector: this.injector
});
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
effect(() => {
const fieldNode = this.state();
if (fieldNode.hidden()) {
const path = fieldNode.structure.pathKeys().join('.') || '<root>';
console.warn(_formatRuntimeError(1916, `Field '${path}' is hidden but is being rendered. ` + `Hidden fields should be removed from the DOM using @if.`));
}
}, {
injector: this.injector
});
}
}
[ɵNgFieldDirective];
ɵngControlCreate(host) {
if (host.hasPassThrough) {
return;
}
if (this.controlValueAccessor) {
this.ɵngControlUpdate = cvaControlCreate(host, this);
} else if (host.customControl) {
this.ɵngControlUpdate = customControlCreate(host, this);
} else if (this.elementIsNativeFormElement) {
this.ɵngControlUpdate = nativeControlCreate(host, this, this.parseErrorsSource, this.validityMonitor);
} else {
throw new _RuntimeError(1914, typeof ngDevMode !== 'undefined' && ngDevMode && `${host.descriptor} is an invalid [formField] directive host. The host must be a native form control ` + `(such as <input>', '<select>', or '<textarea>') or a custom form control with a 'value' or ` + `'checked' model.`);
}
}
ɵngControlUpdate;
elementAcceptsNativeProperty(key) {
if (!this.elementIsNativeFormElement) {
return false;
}
switch (key) {
case 'min':
case 'max':
return this._elementAcceptsMinMax ??= _elementAcceptsMinMax(this.element);
case 'minLength':
case 'maxLength':
return this.elementAcceptsTextualValues;
case 'disabled':
case 'required':
case 'readonly':
case 'name':
return true;
default:
return false;
}
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: FormField,
deps: [],
target: i0.ɵɵFactoryTarget.Directive
});
static ɵdir = i0.ɵɵngDeclareDirective({
minVersion: "17.1.0",
version: "22.0.2",
type: FormField,
isStandalone: true,
selector: "[formField]",
inputs: {
field: {
classPropertyName: "field",
publicName: "formField",
isSignal: true,
isRequired: true,
transformFunction: null
}
},
providers: [{
provide: FORM_FIELD,
useExisting: FormField
}, {
provide: NgControl,
useFactory: () => inject(FormField).interopNgControl
}, {
provide: _FORM_CONTROL_INTEGRATION,
useFactory: () => inject(FORM_FIELD, {
self: true
})
}],
exportAs: ["formField"],
controlCreate: {
passThroughInput: "formField"
},
ngImport: i0
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: FormField,
decorators: [{
type: Directive,
args: [{
selector: '[formField]',
exportAs: 'formField',
providers: [{
provide: FORM_FIELD,
useExisting: FormField
}, {
provide: NgControl,
useFactory: () => inject(FormField).interopNgControl
}, {
provide: _FORM_CONTROL_INTEGRATION,
useFactory: () => inject(FORM_FIELD, {
self: true
})
}]
}]
}],
propDecorators: {
field: [{
type: i0.Input,
args: [{
isSignal: true,
alias: "formField",
required: true
}]
}]
}
});
class FormRoot {
fieldTree = input.required({
...(ngDevMode ? {
debugName: "fieldTree"
} : {}),
alias: 'formRoot'
});
onSubmit(event) {
event.preventDefault();
untracked(() => {
const fieldTree = this.fieldTree();
const node = fieldTree();
if (node.structure.fieldManager.submitOptions) {
submit(fieldTree);
}
});
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: FormRoot,
deps: [],
target: i0.ɵɵFactoryTarget.Directive
});
static ɵdir = i0.ɵɵngDeclareDirective({
minVersion: "17.1.0",
version: "22.0.2",
type: FormRoot,
isStandalone: true,
selector: "form[formRoot]",
inputs: {
fieldTree: {
classPropertyName: "fieldTree",
publicName: "formRoot",
isSignal: true,
isRequired: true,
transformFunction: null
}
},
host: {
attributes: {
"novalidate": ""
},
listeners: {
"submit": "onSubmit($event)"
}
},
ngImport: i0
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "22.0.2",
ngImport: i0,
type: FormRoot,
decorators: [{
type: Directive,
args: [{
selector: 'form[formRoot]',
host: {
'novalidate': '',
'(submit)': 'onSubmit($event)'
}
}]
}],
propDecorators: {
fieldTree: [{
type: i0.Input,
args: [{
isSignal: true,
alias: "formRoot",
required: true
}]
}]
}
});
const registerWebMcpForm = async (formTree, options) => {
const injector = inject(Injector);
effect(() => {
untracked(() => {
initWebMcpForm(formTree, options, injector);
});
});
};
function initWebMcpForm(formTree, options, injector) {
const node = formTree();
const inputSchema = inferSchemaFromFieldNode(node);
if (!inputSchema) {
throw new Error(`Could not accurately infer WebMCP schema for form "${options.name}". ` + `Ensure that the form model does not contain null, undefined, empty arrays, or unsupported types.`);
}
declareExperimentalWebMcpTool({
name: options.name,
description: options.description,
inputSchema,
execute: async args => {
node.value.set(args);
const success = await submit(formTree);
if (success) {
return {
content: [{
type: 'text',
text: 'Form submitted successfully.'
}]
};
} else {
const errorMessages = node.errorSummary().map(err => {
const fieldName = err.fieldTree().structure.pathKeys().join('.');
return `${fieldName ? `${fieldName}: ` : ''}${err.message || err.kind}`;
}).join('\n');
return {
content: [{
type: 'text',
text: `Form submission failed:\n${errorMessages}`
}]
};
}
}
}, injector);
}
function inferSchemaFromFieldNode(node) {
const value = node.value();
if (typeof value === 'string') return {
type: 'string'
};
if (typeof value === 'number') return {
type: 'number'
};
if (typeof value === 'boolean') return {
type: 'boolean'
};
if (value === null || value === undefined) return undefined;
if (Array.isArray(value)) {
if (value.length === 0) return undefined;
const firstChild = node.structure.getChild('0');
if (!firstChild) return undefined;
const itemSchema = inferSchemaFromFieldNode(firstChild);
if (!itemSchema) return undefined;
return {
type: 'array',
items: itemSchema
};
}
if (typeof value === 'object') {
const properties = {};
const required = [];
const children = node.structure.children();
for (const child of children) {
const key = child.keyInParent();
const childSchema = inferSchemaFromFieldNode(child);
if (!childSchema) return undefined;
properties[key] = childSchema;
if (child.required()) required.push(key.toString());
}
return {
type: 'object',
properties,
required,
additionalProperties: false
};
}
return undefined;
}
function provideExperimentalWebMcpForms() {
return makeEnvironmentProviders([{
provide: REGISTER_WEBMCP_FORM,
useValue: registerWebMcpForm
}]);
}
export { BaseNgValidationError, EmailValidationError, FORM_FIELD, FormField, FormRoot, IS_ASYNC_VALIDATION_RESOURCE, MAX, MAX_DATE, MAX_LENGTH, MAX_NUMBER, MIN, MIN_DATE, MIN_LENGTH, MIN_NUMBER, MaxDateValidationError, MaxLengthValidationError, MaxValidationError, MinDateValidationError, MinLengthValidationError, MinValidationError, NativeInputParseError, NgValidationError, PATTERN, PatternValidationError, REQUIRED, RequiredValidationError, StandardSchemaValidationError, createManagedMetadataKey, createMetadataKey, debounce, disabled, email, emailError, hidden, max, maxDate, maxDateError, maxError, maxLength, maxLengthError, metadata, min, minDate, minDateError, minError, minLength, minLengthError, pattern, patternError, provideExperimentalWebMcpForms, provideSignalFormsConfig, readonly, required, requiredError, standardSchemaError, submit, transformedValue, validate, validateAsync, validateHttp, validateStandardSchema, validateTree, ɵNgFieldDirective };
//# sourceMappingURL=signals.mjs.map