mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(forms): add better support for Object.keys and Symbol.iterator
Improves support for using `Object.keys` (and related methods) and
`Symbol.iterator` to work with a `FieldTree` of unknown shape.
(cherry picked from commit e87f423d90)
This commit is contained in:
parent
d1f4c8c18c
commit
f3e2252f4b
4 changed files with 93 additions and 3 deletions
|
|
@ -487,6 +487,8 @@ export class StandardSchemaValidationError extends _NgValidationError {
|
|||
// @public
|
||||
export type Subfields<TValue> = {
|
||||
readonly [K in keyof TValue as TValue[K] extends Function ? never : K]: MaybeFieldTree<TValue[K], string>;
|
||||
} & {
|
||||
[Symbol.iterator](): Iterator<[string, MaybeFieldTree<TValue[keyof TValue], string>]>;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
|
|
|||
|
|
@ -173,6 +173,8 @@ export type Subfields<TValue> = {
|
|||
TValue[K],
|
||||
string
|
||||
>;
|
||||
} & {
|
||||
[Symbol.iterator](): Iterator<[string, MaybeFieldTree<TValue[keyof TValue], string>]>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@
|
|||
*/
|
||||
|
||||
import {untracked} from '@angular/core';
|
||||
import {isArray} from '../util/type_guards';
|
||||
import {isArray, isObject} from '../util/type_guards';
|
||||
import type {FieldNode} from './node';
|
||||
|
||||
/**
|
||||
* Proxy handler which implements `FieldTree<T>` on top of `FieldNode`.
|
||||
*/
|
||||
export const FIELD_PROXY_HANDLER: ProxyHandler<() => FieldNode> = {
|
||||
get(getTgt: () => FieldNode, p: string | symbol) {
|
||||
get(getTgt: () => FieldNode, p: string | symbol, receiver: {[key: string]: unknown}) {
|
||||
const tgt = getTgt();
|
||||
|
||||
// First, check whether the requested property is a defined child node of this node.
|
||||
|
|
@ -41,14 +41,40 @@ export const FIELD_PROXY_HANDLER: ProxyHandler<() => FieldNode> = {
|
|||
// 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) {
|
||||
return (Array.prototype as any)[p];
|
||||
return Array.prototype[p as any];
|
||||
}
|
||||
// Note: We can consider supporting additional array methods if we want in the future,
|
||||
// but they should be thoroughly tested. Just forwarding the method directly from the
|
||||
// `Array` prototype results in broken behavior for some methods like `map`.
|
||||
}
|
||||
|
||||
if (isObject(value)) {
|
||||
// For object fields, allow iteration over their entries for convenience of use with `@for`.
|
||||
if (p === Symbol.iterator) {
|
||||
return function* () {
|
||||
for (const key in receiver) {
|
||||
yield [key, receiver[key]];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, this property doesn't exist.
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(getTgt, prop) {
|
||||
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.
|
||||
if (desc && !desc.configurable) {
|
||||
desc.configurable = true;
|
||||
}
|
||||
return desc;
|
||||
},
|
||||
|
||||
ownKeys(getTgt: () => FieldNode) {
|
||||
const value = untracked(getTgt().value);
|
||||
return typeof value === 'object' && value !== null ? Reflect.ownKeys(value) : [];
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,4 +31,64 @@ describe('FieldTree proxy', () => {
|
|||
// @ts-expect-error
|
||||
f.arr[0] = f.arr[0];
|
||||
});
|
||||
|
||||
it('should get keys and values for object field', () => {
|
||||
const f = form(signal({x: 1, y: 2}), {injector: TestBed.inject(Injector)});
|
||||
expect(Object.keys(f)).toEqual(['x', 'y']);
|
||||
expect(Object.getOwnPropertyNames(f)).toEqual(['x', 'y']);
|
||||
expect(Object.entries(f).map(([key, child]) => [key, child().value()])).toEqual([
|
||||
['x', 1],
|
||||
['y', 2],
|
||||
]);
|
||||
expect(Object.values(f).map((child) => child().value())).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('should get keys and values for array field', () => {
|
||||
const f = form(signal([1, 2]), {injector: TestBed.inject(Injector)});
|
||||
expect(Object.keys(f)).toEqual(['0', '1']);
|
||||
expect(Object.getOwnPropertyNames(f)).toEqual(['0', '1', 'length']);
|
||||
expect(Object.entries(f).map(([key, child]) => [key, child().value()])).toEqual([
|
||||
['0', 1],
|
||||
['1', 2],
|
||||
]);
|
||||
expect(Object.values(f).map((child) => child().value())).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('should get keys and values for primitive field', () => {
|
||||
const f = form(signal(1), {injector: TestBed.inject(Injector)});
|
||||
expect(Object.keys(f)).toEqual([]);
|
||||
expect(Object.getOwnPropertyNames(f)).toEqual([]);
|
||||
expect(Object.entries(f)).toEqual([]);
|
||||
expect(Object.values(f)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should iterate over object field', () => {
|
||||
const f = form(signal({x: 1, y: 2}), {injector: TestBed.inject(Injector)});
|
||||
const result: [string, number][] = [];
|
||||
for (const [key, child] of f) {
|
||||
result.push([key, child().value()]);
|
||||
}
|
||||
expect(result).toEqual([
|
||||
['x', 1],
|
||||
['y', 2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should iterate over array field', () => {
|
||||
const f = form(signal([1, 2]), {injector: TestBed.inject(Injector)});
|
||||
const result: number[] = [];
|
||||
for (const child of f) {
|
||||
result.push(child().value());
|
||||
}
|
||||
expect(result).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('should not iterate over primitive field', () => {
|
||||
const f = form(signal(1), {injector: TestBed.inject(Injector)});
|
||||
expect(() => {
|
||||
// @ts-expect-error - not iterable
|
||||
for (const child of f) {
|
||||
}
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue