mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(forms): add utility to assert that value is a field tree
Adds the `isFieldTree` utility that allows users to assert whether a value is a field tree. This is something that has come up on Material recently and will be useful for users as well. Fixes #69984.
This commit is contained in:
committed by
Alex Rickabaugh
parent
d44b3224d9
commit
2a141847a5
@@ -299,6 +299,9 @@ export type IgnoreUnknownProperties<T> = T extends Record<PropertyKey, unknown>
|
||||
// @public
|
||||
export const IS_ASYNC_VALIDATION_RESOURCE: unique symbol;
|
||||
|
||||
// @public
|
||||
export function isFieldTree(value: unknown): value is FieldTree<unknown>;
|
||||
|
||||
// @public
|
||||
export interface ItemFieldContext<TValue> extends ChildFieldContext<TValue> {
|
||||
readonly index: Signal<number>;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* @description
|
||||
* Entry point for all public APIs of this package.
|
||||
*/
|
||||
export * from './src/api/assertions';
|
||||
export * from './src/api/control';
|
||||
export * from './src/api/di';
|
||||
export * from './src/api/rules';
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {FIELD_TREE} from './symbols';
|
||||
import type {FieldTree} from './types';
|
||||
|
||||
/**
|
||||
* Asserts whether a value is a `FieldTree`.
|
||||
* @param value Value to be checked.
|
||||
*
|
||||
* @publicApi 22.1
|
||||
*/
|
||||
export function isFieldTree(value: unknown): value is FieldTree<unknown> {
|
||||
return typeof value === 'function' && (value as any)[FIELD_TREE] === true;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Symbol used to retain generic type information when it would otherwise be lost.
|
||||
*/
|
||||
export declare const ɵɵTYPE: unique symbol;
|
||||
|
||||
/**
|
||||
* Symbol used to indicate that a value is a field tree.
|
||||
*/
|
||||
export const FIELD_TREE: unique symbol = /* @__PURE__ */ Symbol('FIELD_TREE');
|
||||
@@ -10,11 +10,7 @@ import {Injector, Signal, WritableSignal} from '@angular/core';
|
||||
import {AbstractControl} from '@angular/forms';
|
||||
import type {FormField} from '../directive/form_field';
|
||||
import type {MetadataKey, NgValidationError, ValidationError} from './rules';
|
||||
|
||||
/**
|
||||
* Symbol used to retain generic type information when it would otherwise be lost.
|
||||
*/
|
||||
declare const ɵɵTYPE: unique symbol;
|
||||
import type {ɵɵTYPE} from './symbols';
|
||||
|
||||
/**
|
||||
* Options that can be specified when submitting a form.
|
||||
@@ -177,8 +173,7 @@ export type TreeValidationResult<
|
||||
* @publicApi 22.0
|
||||
*/
|
||||
export type ValidationResult<E extends ValidationError = ValidationError> =
|
||||
| ValidationSuccess
|
||||
| OneOrMany<E>;
|
||||
ValidationSuccess | OneOrMany<E>;
|
||||
|
||||
/**
|
||||
* An asynchronous validation result where all errors explicitly define their target field.
|
||||
@@ -195,8 +190,7 @@ export type ValidationResult<E extends ValidationError = ValidationError> =
|
||||
* @publicApi 22.0
|
||||
*/
|
||||
export type AsyncValidationResult<E extends ValidationError = ValidationError> =
|
||||
| ValidationResult<E>
|
||||
| 'pending';
|
||||
ValidationResult<E> | 'pending';
|
||||
|
||||
/**
|
||||
* A field accessor function that returns the state of the field.
|
||||
@@ -783,8 +777,7 @@ export type SchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> =
|
||||
* @publicApi 22.0
|
||||
*/
|
||||
export type MaybeSchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> =
|
||||
| (TModel & undefined)
|
||||
| SchemaPathTree<Exclude<TModel, undefined>, TPathKind>;
|
||||
(TModel & undefined) | SchemaPathTree<Exclude<TModel, undefined>, TPathKind>;
|
||||
|
||||
/**
|
||||
* A reusable schema that defines behavior and rules for a form.
|
||||
@@ -872,8 +865,7 @@ export type SchemaFn<TModel, TPathKind extends PathKind = PathKind.Root> = (
|
||||
* @publicApi 22.0
|
||||
*/
|
||||
export type SchemaOrSchemaFn<TModel, TPathKind extends PathKind = PathKind.Root> =
|
||||
| Schema<TModel>
|
||||
| SchemaFn<TModel, TPathKind>;
|
||||
Schema<TModel> | SchemaFn<TModel, TPathKind>;
|
||||
|
||||
/**
|
||||
* A function that receives the `FieldContext` for the field the logic is bound to and returns
|
||||
|
||||
@@ -9,16 +9,21 @@
|
||||
import {untracked} from '@angular/core';
|
||||
import {isArray, isObject} from '../util/type_guards';
|
||||
import type {FieldNode} from './node';
|
||||
import {FIELD_TREE} from '../api/symbols';
|
||||
|
||||
/**
|
||||
* Proxy handler which implements `FieldTree<T>` on top of `FieldNode`.
|
||||
*/
|
||||
export const FIELD_PROXY_HANDLER: ProxyHandler<() => FieldNode> = {
|
||||
get(getTgt: () => FieldNode, p: string | symbol, receiver: {[key: string]: unknown}) {
|
||||
get(getTgt: () => FieldNode, property: string | symbol, receiver: Record<string, unknown>) {
|
||||
if (property === FIELD_TREE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tgt = getTgt();
|
||||
|
||||
// First, check whether the requested property is a defined child node of this node.
|
||||
const child = tgt.structure.getChild(p);
|
||||
const child = tgt.structure.getChild(property);
|
||||
if (child !== undefined) {
|
||||
// If so, return the child node's `FieldTree` proxy, allowing the developer to continue
|
||||
// navigating the form structure.
|
||||
@@ -35,12 +40,12 @@ export const FIELD_PROXY_HANDLER: ProxyHandler<() => FieldNode> = {
|
||||
|
||||
if (isArray(value)) {
|
||||
// Allow access to the length for field arrays, it should be the same as the length of the data.
|
||||
if (p === 'length') {
|
||||
if (property === 'length') {
|
||||
return (tgt.value() as Array<unknown>).length;
|
||||
}
|
||||
// Allow access to the iterator. This allows the user to spread the field array into a
|
||||
// standard array in order to call methods like `filter`, `map`, etc.
|
||||
if (p === Symbol.iterator) {
|
||||
if (property === Symbol.iterator) {
|
||||
return () => {
|
||||
// When creating an iterator, we need to account for reactivity. The iterator itself will
|
||||
// read things each time `.next()` is called, but that may happen outside of the context
|
||||
@@ -59,7 +64,7 @@ export const FIELD_PROXY_HANDLER: ProxyHandler<() => FieldNode> = {
|
||||
|
||||
if (isObject(value)) {
|
||||
// For object fields, allow iteration over their entries for convenience of use with `@for`.
|
||||
if (p === Symbol.iterator) {
|
||||
if (property === Symbol.iterator) {
|
||||
return function* () {
|
||||
for (const key in receiver) {
|
||||
yield [key, receiver[key]];
|
||||
@@ -72,7 +77,7 @@ export const FIELD_PROXY_HANDLER: ProxyHandler<() => FieldNode> = {
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(getTgt, prop) {
|
||||
getOwnPropertyDescriptor(getTgt: () => FieldNode, prop: string | symbol) {
|
||||
const value = untracked(getTgt().value) as Object;
|
||||
const desc = Reflect.getOwnPropertyDescriptor(value, prop);
|
||||
// In order for `Object.keys` to function properly, keys must be reported as configurable.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Component, signal} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {form, isFieldTree} from '../../public_api';
|
||||
|
||||
describe('assertions', () => {
|
||||
it('should detect field trees', () => {
|
||||
@Component({template: ''})
|
||||
class App {
|
||||
readonly form = form(
|
||||
signal({
|
||||
firstName: 'Frodo',
|
||||
lastName: 'Baggins',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
const tree = fixture.componentInstance.form;
|
||||
expect(isFieldTree(tree)).toBe(true);
|
||||
expect(isFieldTree(tree().fieldTree)).toBe(true);
|
||||
});
|
||||
|
||||
it('should distinguish non-field-tree values from field trees', () => {
|
||||
expect(isFieldTree(true)).toBe(false);
|
||||
expect(isFieldTree(1)).toBe(false);
|
||||
expect(isFieldTree({})).toBe(false);
|
||||
expect(isFieldTree(() => 123)).toBe(false);
|
||||
expect(isFieldTree(null)).toBe(false);
|
||||
expect(isFieldTree(undefined)).toBe(false);
|
||||
expect(isFieldTree(signal(1))).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user