Skip to content

Commit 5880fbc

Browse files
mmalerbaalxhub
authored andcommitted
feat(forms): redo the signal forms metadata API
This PR makes a number of changes to the metadata API to address design flaws in the previous API. Some of the changes include: - Replaces the previous `MetadataKey` and `AggregateMetadataKey` with a single unified `MetadataKey` that is used for all metadata. - The new `MetadataKey` is only defined for fields that explicitly set it in their schema logic - All metadata now has reducer / aggregate behavior - The new `MetadataKey` has an option to create a managed key which wraps the result of its computed aggregate into some other structure such as a `Resource` or `linkedSignal` - There are now two APIs to create metadata keys - `createMetadataKey` for pure computed metadata - `createManagedMetadataKey` for metadata that manages its computation internally (cherry picked from commit ebc5c2b)
1 parent 53f752e commit 5880fbc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+675
-582
lines changed

goldens/public-api/forms/signals/index.api.md

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,6 @@ import { ɵFieldState } from '@angular/core';
3333
import { ɵInteropControl } from '@angular/core';
3434
import { ɵɵcontrolCreate } from '@angular/core';
3535

36-
// @public
37-
export function aggregateMetadata<TValue, TMetadataItem, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, key: AggregateMetadataKey<any, TMetadataItem>, logic: NoInfer<LogicFn<TValue, TMetadataItem, TPathKind>>): void;
38-
39-
// @public
40-
export class AggregateMetadataKey<TAcc, TItem> {
41-
// (undocumented)
42-
readonly getInitial: () => TAcc;
43-
// (undocumented)
44-
readonly reduce: (acc: TAcc, item: TItem) => TAcc;
45-
}
46-
47-
// @public
48-
export function andMetadataKey(): AggregateMetadataKey<boolean, boolean>;
49-
5036
// @public
5137
export function apply<TValue>(path: SchemaPath<TValue>, schema: NoInfer<SchemaOrSchemaFn<TValue>>): void;
5238

@@ -94,7 +80,16 @@ export type CompatSchemaPath<TControl extends AbstractControl, TPathKind extends
9480
};
9581

9682
// @public
97-
export function createMetadataKey<TValue>(): MetadataKey<TValue>;
83+
export function createManagedMetadataKey<TRead, TWrite>(create: (s: Signal<TWrite | undefined>) => TRead): MetadataKey<TRead, TWrite, TWrite | undefined>;
84+
85+
// @public
86+
export function createManagedMetadataKey<TRead, TWrite, TAcc>(create: (s: Signal<TAcc>) => TRead, reducer: MetadataReducer<TAcc, TWrite>): MetadataKey<TRead, TWrite, TAcc>;
87+
88+
// @public
89+
export function createMetadataKey<TWrite>(): MetadataKey<Signal<TWrite | undefined>, TWrite, TWrite | undefined>;
90+
91+
// @public
92+
export function createMetadataKey<TWrite, TAcc>(reducer: MetadataReducer<TAcc, TWrite>): MetadataKey<Signal<TAcc>, TWrite, TAcc>;
9893

9994
// @public
10095
export function customError<E extends Partial<ValidationError.WithField>>(obj: WithField<E>): CustomValidationError;
@@ -179,12 +174,10 @@ export interface FieldState<TValue, TKey extends string | number = string | numb
179174
readonly errors: Signal<ValidationError.WithField[]>;
180175
readonly errorSummary: Signal<ValidationError.WithField[]>;
181176
readonly fieldBindings: Signal<readonly Field<unknown>[]>;
182-
hasMetadata(key: MetadataKey<any> | AggregateMetadataKey<any, any>): boolean;
183177
readonly hidden: Signal<boolean>;
184178
readonly invalid: Signal<boolean>;
185179
readonly keyInParent: Signal<TKey>;
186-
metadata<M>(key: AggregateMetadataKey<M, any>): Signal<M>;
187-
metadata<M>(key: MetadataKey<M>): M | undefined;
180+
metadata<M>(key: MetadataKey<M, any, any>): M | undefined;
188181
readonly pending: Signal<boolean>;
189182
reset(value?: TValue): void;
190183
readonly submitting: Signal<boolean>;
@@ -270,23 +263,20 @@ export interface ItemFieldContext<TValue> extends ChildFieldContext<TValue> {
270263
// @public
271264
export type ItemType<T extends Object> = T extends ReadonlyArray<any> ? T[number] : T[keyof T];
272265

273-
// @public
274-
export function listMetadataKey<TItem>(): AggregateMetadataKey<TItem[], TItem | undefined>;
275-
276266
// @public
277267
export type LogicFn<TValue, TReturn, TPathKind extends PathKind = PathKind.Root> = (ctx: FieldContext<TValue, TPathKind>) => TReturn;
278268

279269
// @public
280270
export type MapToErrorsFn<TValue, TResult, TPathKind extends PathKind = PathKind.Root> = (result: TResult, ctx: FieldContext<TValue, TPathKind>) => TreeValidationResult;
281271

282272
// @public
283-
export const MAX: AggregateMetadataKey<number | undefined, number | undefined>;
273+
export const MAX: MetadataKey<Signal<number | undefined>, number | undefined, number | undefined>;
284274

285275
// @public
286276
export function max<TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<number | string | null, SchemaPathRules.Supported, TPathKind>, maxValue: number | LogicFn<number | string | null, number | undefined, TPathKind>, config?: BaseValidatorConfig<number | string | null, TPathKind>): void;
287277

288278
// @public
289-
export const MAX_LENGTH: AggregateMetadataKey<number | undefined, number | undefined>;
279+
export const MAX_LENGTH: MetadataKey<Signal<number | undefined>, number | undefined, number | undefined>;
290280

291281
// @public
292282
export function maxError(max: number, options: WithField<ValidationErrorOptions>): MaxValidationError;
@@ -312,9 +302,6 @@ export class MaxLengthValidationError extends _NgValidationError {
312302
readonly maxLength: number;
313303
}
314304

315-
// @public
316-
export function maxMetadataKey(): AggregateMetadataKey<number | undefined, number | undefined>;
317-
318305
// @public
319306
export class MaxValidationError extends _NgValidationError {
320307
constructor(max: number, options?: ValidationErrorOptions);
@@ -331,23 +318,44 @@ export type MaybeFieldTree<TModel, TKey extends string | number = string | numbe
331318
export type MaybeSchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> = (TModel & undefined) | SchemaPathTree<Exclude<TModel, undefined>, TPathKind>;
332319

333320
// @public
334-
export function metadata<TValue, TData, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, factory: (ctx: FieldContext<TValue, TPathKind>) => TData): MetadataKey<TData>;
321+
export function metadata<TValue, TKey extends MetadataKey<any, any, any>, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, key: TKey, logic: NoInfer<LogicFn<TValue, MetadataSetterType<TKey>, TPathKind>>): TKey;
335322

336323
// @public
337-
export function metadata<TValue, TData, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, key: MetadataKey<TData>, factory: (ctx: FieldContext<TValue, TPathKind>) => TData): MetadataKey<TData>;
324+
export class MetadataKey<TRead, TWrite, TAcc> {
325+
protected constructor(reducer: MetadataReducer<TAcc, TWrite>, create: ((s: Signal<TAcc>) => TRead) | undefined);
326+
// (undocumented)
327+
readonly create: ((s: Signal<TAcc>) => TRead) | undefined;
328+
// (undocumented)
329+
readonly reducer: MetadataReducer<TAcc, TWrite>;
330+
}
338331

339332
// @public
340-
export class MetadataKey<TValue> {
333+
export interface MetadataReducer<TAcc, TItem> {
334+
getInitial: () => TAcc;
335+
reduce: (acc: TAcc, item: TItem) => TAcc;
341336
}
342337

338+
// @public (undocumented)
339+
export const MetadataReducer: {
340+
readonly list: <TItem>() => MetadataReducer<TItem[], TItem | undefined>;
341+
readonly min: () => MetadataReducer<number | undefined, number | undefined>;
342+
readonly max: () => MetadataReducer<number | undefined, number | undefined>;
343+
readonly or: () => MetadataReducer<boolean, boolean>;
344+
readonly and: () => MetadataReducer<boolean, boolean>;
345+
readonly override: typeof override;
346+
};
347+
343348
// @public
344-
export const MIN: AggregateMetadataKey<number | undefined, number | undefined>;
349+
export type MetadataSetterType<TKey> = TKey extends MetadataKey<any, infer TWrite, any> ? TWrite : never;
350+
351+
// @public
352+
export const MIN: MetadataKey<Signal<number | undefined>, number | undefined, number | undefined>;
345353

346354
// @public
347355
export function min<TValue extends number | string | null, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, minValue: number | LogicFn<TValue, number | undefined, TPathKind>, config?: BaseValidatorConfig<TValue, TPathKind>): void;
348356

349357
// @public
350-
export const MIN_LENGTH: AggregateMetadataKey<number | undefined, number | undefined>;
358+
export const MIN_LENGTH: MetadataKey<Signal<number | undefined>, number | undefined, number | undefined>;
351359

352360
// @public
353361
export function minError(min: number, options: WithField<ValidationErrorOptions>): MinValidationError;
@@ -373,9 +381,6 @@ export class MinLengthValidationError extends _NgValidationError {
373381
readonly minLength: number;
374382
}
375383

376-
// @public
377-
export function minMetadataKey(): AggregateMetadataKey<number | undefined, number | undefined>;
378-
379384
// @public
380385
export class MinValidationError extends _NgValidationError {
381386
constructor(min: number, options?: ValidationErrorOptions);
@@ -394,9 +399,6 @@ export type NgValidationError = RequiredValidationError | MinValidationError | M
394399
// @public
395400
export type OneOrMany<T> = T | readonly T[];
396401

397-
// @public
398-
export function orMetadataKey(): AggregateMetadataKey<boolean, boolean>;
399-
400402
// @public
401403
export type PathKind = PathKind.Root | PathKind.Child | PathKind.Item;
402404

@@ -416,7 +418,7 @@ export namespace PathKind {
416418
}
417419

418420
// @public
419-
export const PATTERN: AggregateMetadataKey<RegExp[], RegExp | undefined>;
421+
export const PATTERN: MetadataKey<Signal<RegExp[]>, RegExp | undefined, RegExp[]>;
420422

421423
// @public
422424
export function pattern<TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<string, SchemaPathRules.Supported, TPathKind>, pattern: RegExp | LogicFn<string | undefined, RegExp | undefined, TPathKind>, config?: BaseValidatorConfig<string, TPathKind>): void;
@@ -445,14 +447,11 @@ export function readonly<TValue, TPathKind extends PathKind = PathKind.Root>(pat
445447
// @public
446448
export type ReadonlyArrayLike<T> = Pick<ReadonlyArray<T>, number | 'length' | typeof Symbol.iterator>;
447449

448-
// @public
449-
export function reducedMetadataKey<TAcc, TItem>(reduce: (acc: TAcc, item: TItem) => TAcc, getInitial: NoInfer<() => TAcc>): AggregateMetadataKey<TAcc, TItem>;
450-
451450
// @public
452451
export type RemoveStringIndexUnknownKey<K, V> = string extends K ? unknown extends V ? never : K : K;
453452

454453
// @public
455-
export const REQUIRED: AggregateMetadataKey<boolean, boolean>;
454+
export const REQUIRED: MetadataKey<Signal<boolean>, boolean, boolean>;
456455

457456
// @public
458457
export function required<TValue, TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>, config?: BaseValidatorConfig<TValue, TPathKind> & {

packages/forms/signals/compat/src/api/compat_validation_error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import {AbstractControl} from '@angular/forms';
10-
import {FieldTree} from '../../../src/api/types';
1110
import {ValidationError} from '../../../src/api/rules/validation/validation_errors';
11+
import {FieldTree} from '../../../src/api/types';
1212

1313
/**
1414
* An error used for compat errors.

packages/forms/signals/public_api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
* Entry point for all public APIs of this package.
1313
*/
1414
export * from './src/api/control';
15-
export * from './src/api/rules/debounce';
1615
export * from './src/api/di';
1716
export * from './src/api/field_directive';
1817
export * from './src/api/rules';
18+
export * from './src/api/rules/debounce';
1919
export * from './src/api/rules/metadata';
20+
export * from './src/api/rules/validation/validation_errors';
2021
export * from './src/api/structure';
2122
export * from './src/api/types';
22-
export * from './src/api/rules/validation/validation_errors';

packages/forms/signals/src/api/control.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import {InputSignal, InputSignalWithTransform, ModelSignal, OutputRef} from '@angular/core';
10-
import type {DisabledReason} from './types';
1110
import {ValidationError, type WithOptionalField} from './rules/validation/validation_errors';
11+
import type {DisabledReason} from './types';
1212

1313
/**
1414
* The base set of properties shared by all form control contracts.

packages/forms/signals/src/api/rules/aggregate_metadata.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

packages/forms/signals/src/api/rules/debounce.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function debounce<TValue, TPathKind extends PathKind = PathKind.Root>(
3636
: durationOrDebouncer > 0
3737
? debounceForDuration(durationOrDebouncer)
3838
: immediate;
39-
pathNode.builder.addAggregateMetadataRule(DEBOUNCER, () => debouncer);
39+
pathNode.builder.addMetadataRule(DEBOUNCER, () => debouncer);
4040
}
4141

4242
function debounceForDuration(durationInMilliseconds: number): Debouncer<unknown> {

packages/forms/signals/src/api/rules/disabled.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {FieldPathNode} from '../../schema/path_node';
1010
import {assertPathIsCurrent} from '../../schema/schema';
11-
import type {FieldContext, SchemaPath, LogicFn, PathKind, SchemaPathRules} from '../types';
11+
import type {FieldContext, LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
1212

1313
/**
1414
* Adds logic to a field to conditionally disable it. A disabled field does not contribute to the

packages/forms/signals/src/api/rules/hidden.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {FieldPathNode} from '../../schema/path_node';
1010
import {assertPathIsCurrent} from '../../schema/schema';
11-
import type {SchemaPath, LogicFn, PathKind, SchemaPathRules} from '../types';
11+
import type {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
1212

1313
/**
1414
* Adds logic to a field to conditionally hide it. A hidden field does not contribute to the

packages/forms/signals/src/api/rules/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export * from './aggregate_metadata';
109
export * from './disabled';
1110
export * from './hidden';
11+
export * from './metadata';
1212
export * from './readonly';
1313
export * from './validation';

0 commit comments

Comments
 (0)