mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(forms): change FieldState optional properties to non-optional | undefined
This improves compatibility with TypeScript's exactOptionalPropertyTypes. Fixes #67246
This commit is contained in:
parent
37b1c5d0fe
commit
ee8d2098cb
8 changed files with 85 additions and 36 deletions
|
|
@ -79,10 +79,10 @@ export type CompatSchemaPath<TControl extends AbstractControl, TPathKind extends
|
|||
};
|
||||
|
||||
// @public
|
||||
export function createManagedMetadataKey<TRead, TWrite>(create: (s: Signal<TWrite | undefined>) => TRead): MetadataKey<TRead, TWrite, TWrite | undefined>;
|
||||
export function createManagedMetadataKey<TRead, TWrite>(create: (state: FieldState<unknown>, data: Signal<TWrite | undefined>) => TRead): MetadataKey<TRead, TWrite, TWrite | undefined>;
|
||||
|
||||
// @public
|
||||
export function createManagedMetadataKey<TRead, TWrite, TAcc>(create: (s: Signal<TAcc>) => TRead, reducer: MetadataReducer<TAcc, TWrite>): MetadataKey<TRead, TWrite, TAcc>;
|
||||
export function createManagedMetadataKey<TRead, TWrite, TAcc>(create: (state: FieldState<unknown>, data: Signal<TAcc>) => TRead, reducer: MetadataReducer<TAcc, TWrite>): MetadataKey<TRead, TWrite, TAcc>;
|
||||
|
||||
// @public
|
||||
export function createMetadataKey<TWrite>(): MetadataKey<Signal<TWrite | undefined>, TWrite, TWrite | undefined>;
|
||||
|
|
@ -345,9 +345,9 @@ export function metadata<TValue, TKey extends MetadataKey<any, any, any>, TPathK
|
|||
|
||||
// @public
|
||||
export class MetadataKey<TRead, TWrite, TAcc> {
|
||||
protected constructor(reducer: MetadataReducer<TAcc, TWrite>, create: ((s: Signal<TAcc>) => TRead) | undefined);
|
||||
protected constructor(reducer: MetadataReducer<TAcc, TWrite>, create: ((state: FieldState<unknown>, data: Signal<TAcc>) => TRead) | undefined);
|
||||
// (undocumented)
|
||||
readonly create: ((s: Signal<TAcc>) => TRead) | undefined;
|
||||
readonly create: ((state: FieldState<unknown>, data: Signal<TAcc>) => TRead) | undefined;
|
||||
// (undocumented)
|
||||
readonly reducer: MetadataReducer<TAcc, TWrite>;
|
||||
}
|
||||
|
|
@ -509,11 +509,11 @@ export interface ReadonlyFieldState<TValue, TKey extends string | number = strin
|
|||
readonly hidden: Signal<boolean>;
|
||||
readonly invalid: Signal<boolean>;
|
||||
readonly keyInParent: Signal<TKey>;
|
||||
readonly max?: Signal<number | undefined>;
|
||||
readonly maxLength?: Signal<number | undefined>;
|
||||
readonly max: Signal<number | undefined> | undefined;
|
||||
readonly maxLength: Signal<number | undefined> | undefined;
|
||||
metadata<M>(key: MetadataKey<M, any, any>): M | undefined;
|
||||
readonly min?: Signal<number | undefined>;
|
||||
readonly minLength?: Signal<number | undefined>;
|
||||
readonly min: Signal<number | undefined> | undefined;
|
||||
readonly minLength: Signal<number | undefined> | undefined;
|
||||
readonly name: Signal<string>;
|
||||
readonly pattern: Signal<readonly RegExp[]>;
|
||||
readonly pending: Signal<boolean>;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import {type Signal} from '@angular/core';
|
||||
import {FieldPathNode} from '../../schema/path_node';
|
||||
import {assertPathIsCurrent} from '../../schema/schema';
|
||||
import type {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
|
||||
import type {FieldState, LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types';
|
||||
|
||||
/**
|
||||
* Sets a value for the {@link MetadataKey} for this field.
|
||||
|
|
@ -150,7 +150,7 @@ export class MetadataKey<TRead, TWrite, TAcc> {
|
|||
/** Use {@link reducedMetadataKey}. */
|
||||
protected constructor(
|
||||
readonly reducer: MetadataReducer<TAcc, TWrite>,
|
||||
readonly create: ((s: Signal<TAcc>) => TRead) | undefined,
|
||||
readonly create: ((state: FieldState<unknown>, data: Signal<TAcc>) => TRead) | undefined,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ export function createMetadataKey<TWrite, TAcc>(
|
|||
* @experimental 21.0.0
|
||||
*/
|
||||
export function createManagedMetadataKey<TRead, TWrite>(
|
||||
create: (s: Signal<TWrite | undefined>) => TRead,
|
||||
create: (state: FieldState<unknown>, data: Signal<TWrite | undefined>) => TRead,
|
||||
): MetadataKey<TRead, TWrite, TWrite | undefined>;
|
||||
/**
|
||||
* Creates a metadata key that exposes a managed value based on the accumulated result of the values
|
||||
|
|
@ -229,16 +229,16 @@ export function createManagedMetadataKey<TRead, TWrite>(
|
|||
* @experimental 21.0.0
|
||||
*/
|
||||
export function createManagedMetadataKey<TRead, TWrite, TAcc>(
|
||||
create: (s: Signal<TAcc>) => TRead,
|
||||
create: (state: FieldState<unknown>, data: Signal<TAcc>) => TRead,
|
||||
reducer: MetadataReducer<TAcc, TWrite>,
|
||||
): MetadataKey<TRead, TWrite, TAcc>;
|
||||
export function createManagedMetadataKey<TRead, TWrite, TAcc>(
|
||||
create: (s: Signal<TAcc>) => TRead,
|
||||
create: (state: FieldState<unknown>, data: Signal<TAcc>) => TRead,
|
||||
reducer?: MetadataReducer<TAcc, TWrite>,
|
||||
): MetadataKey<TRead, TWrite, TAcc> {
|
||||
return new (MetadataKey as new (
|
||||
reducer: MetadataReducer<TAcc, TWrite>,
|
||||
create: (s: Signal<TAcc>) => TRead,
|
||||
create: (state: FieldState<unknown>, data: Signal<TAcc>) => TRead,
|
||||
) => MetadataKey<TRead, TWrite, TAcc>)(reducer ?? MetadataReducer.override<any>(), create);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export function validateAsync<TValue, TParams, TResult, TPathKind extends PathKi
|
|||
const pathNode = FieldPathNode.unwrapFieldPath(path);
|
||||
|
||||
const RESOURCE = createManagedMetadataKey<ReturnType<typeof opts.factory>, TParams | undefined>(
|
||||
opts.factory,
|
||||
(_state, params) => opts.factory(params),
|
||||
);
|
||||
RESOURCE[IS_ASYNC_VALIDATION_RESOURCE] = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -346,28 +346,28 @@ export interface ReadonlyFieldState<TValue, TKey extends string | number = strin
|
|||
*
|
||||
* Applies to `<input>` with a numeric or date `type` attribute and custom controls.
|
||||
*/
|
||||
readonly max?: Signal<number | undefined>;
|
||||
readonly max: Signal<number | undefined> | undefined;
|
||||
|
||||
/**
|
||||
* A signal indicating the field's maximum string length, if applicable.
|
||||
*
|
||||
* Applies to `<input>`, `<textarea>`, and custom controls.
|
||||
*/
|
||||
readonly maxLength?: Signal<number | undefined>;
|
||||
readonly maxLength: Signal<number | undefined> | undefined;
|
||||
|
||||
/**
|
||||
* A signal indicating the field's minimum value, if applicable.
|
||||
*
|
||||
* Applies to `<input>` with a numeric or date `type` attribute and custom controls.
|
||||
*/
|
||||
readonly min?: Signal<number | undefined>;
|
||||
readonly min: Signal<number | undefined> | undefined;
|
||||
|
||||
/**
|
||||
* A signal indicating the field's minimum string length, if applicable.
|
||||
*
|
||||
* Applies to `<input>`, `<textarea>`, and custom controls.
|
||||
*/
|
||||
readonly minLength?: Signal<number | undefined>;
|
||||
readonly minLength: Signal<number | undefined> | undefined;
|
||||
|
||||
/**
|
||||
* A signal of a unique name for the field, by default based on the name of its parent field.
|
||||
|
|
|
|||
|
|
@ -23,20 +23,31 @@ export class FieldMetadataState {
|
|||
/** A map of all `MetadataKey` that have been defined for this field. */
|
||||
private readonly metadata = new Map<MetadataKey<unknown, unknown, unknown>, unknown>();
|
||||
|
||||
constructor(private readonly node: FieldNode) {
|
||||
// Force eager creation of managed keys,
|
||||
// as managed keys have a `create` function that needs to run during construction.
|
||||
for (const key of this.node.logicNode.logic.getMetadataKeys()) {
|
||||
if (key.create) {
|
||||
const logic = this.node.logicNode.logic.getMetadata(key);
|
||||
const result = untracked(() =>
|
||||
runInInjectionContext(this.node.structure.injector, () =>
|
||||
key.create!(computed(() => logic.compute(this.node.context))),
|
||||
),
|
||||
);
|
||||
this.metadata.set(key, result);
|
||||
}
|
||||
constructor(private readonly node: FieldNode) {}
|
||||
|
||||
/**
|
||||
* Force eager creation of managed keys,
|
||||
* as managed keys have a `create` function that needs to run during construction.
|
||||
*/
|
||||
runMetadataCreateLifecycle(): void {
|
||||
if (!this.node.logicNode.logic.hasMetadataKeys()) {
|
||||
return;
|
||||
}
|
||||
|
||||
untracked(() =>
|
||||
runInInjectionContext(this.node.structure.injector, () => {
|
||||
for (const key of this.node.logicNode.logic.getMetadataKeys()) {
|
||||
if (key.create) {
|
||||
const logic = this.node.logicNode.logic.getMetadata(key);
|
||||
const result = key.create!(
|
||||
this.node,
|
||||
computed(() => logic.compute(this.node.context)),
|
||||
);
|
||||
this.metadata.set(key, result);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Gets the value of an `MetadataKey` for the field. */
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ export class FieldNode implements FieldState<unknown> {
|
|||
this.metadataState = new FieldMetadataState(this);
|
||||
this.submitState = new FieldSubmitState(this);
|
||||
this.controlValue = this.controlValueSignal();
|
||||
// We eagerly create metadata at the end of construction so that the node is fully constructed
|
||||
// before metadata creation logic runs (which may access other states on the node).
|
||||
this.metadataState.runMetadataCreateLifecycle();
|
||||
}
|
||||
|
||||
focusBoundControl(options?: FocusOptions): void {
|
||||
|
|
|
|||
|
|
@ -308,6 +308,10 @@ export class LogicContainer {
|
|||
return this.metadata.has(key);
|
||||
}
|
||||
|
||||
hasMetadataKeys(): boolean {
|
||||
return this.metadata.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an iterable of [metadata key, logic function] pairs.
|
||||
* @returns An iterable of metadata keys.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,14 @@
|
|||
*/
|
||||
import {provideHttpClient} from '@angular/common/http';
|
||||
import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
|
||||
import {ApplicationRef, Injector, resource, signal, type Signal} from '@angular/core';
|
||||
import {
|
||||
ApplicationRef,
|
||||
assertNotInReactiveContext,
|
||||
Injector,
|
||||
resource,
|
||||
signal,
|
||||
type Signal,
|
||||
} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {isNode} from '@angular/private/testing';
|
||||
|
||||
|
|
@ -57,7 +64,7 @@ describe('resources', () => {
|
|||
|
||||
it('Takes a simple resource which reacts to data changes', async () => {
|
||||
const s: SchemaOrSchemaFn<Cat> = function (p) {
|
||||
const RES = createManagedMetadataKey((params: Signal<{x: string} | undefined>) =>
|
||||
const RES = createManagedMetadataKey((_state, params: Signal<{x: string} | undefined>) =>
|
||||
resource({
|
||||
params,
|
||||
loader: async ({params}) => `got: ${params.x}`,
|
||||
|
|
@ -102,7 +109,7 @@ describe('resources', () => {
|
|||
it('should create a resource per entry in an array', async () => {
|
||||
const s: SchemaOrSchemaFn<Cat[]> = function (p) {
|
||||
applyEach(p, (p) => {
|
||||
const RES = createManagedMetadataKey((params: Signal<{x: string} | undefined>) =>
|
||||
const RES = createManagedMetadataKey((_state, params: Signal<{x: string} | undefined>) =>
|
||||
resource({
|
||||
params,
|
||||
loader: async ({params}) => `got: ${params.x}`,
|
||||
|
|
@ -388,7 +395,7 @@ describe('resources', () => {
|
|||
});
|
||||
|
||||
it('should not allow accessing resource metadata on a field that does not define its params', () => {
|
||||
const RES = createManagedMetadataKey((params: Signal<string | undefined>) =>
|
||||
const RES = createManagedMetadataKey((_state, params: Signal<string | undefined>) =>
|
||||
resource({params, loader: async () => 'hi'}),
|
||||
);
|
||||
|
||||
|
|
@ -439,4 +446,28 @@ describe('resources', () => {
|
|||
expect(usernameForm().pending()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow accessing basic field state properties during creation without reading them', () => {
|
||||
let success = false;
|
||||
|
||||
const RES = createManagedMetadataKey((state, _params: Signal<string | undefined>) => {
|
||||
// We shouldn't be captured in the reactive context of node creation here.
|
||||
assertNotInReactiveContext(createManagedMetadataKey);
|
||||
|
||||
state.value();
|
||||
state.disabled();
|
||||
|
||||
success = true;
|
||||
});
|
||||
|
||||
form(
|
||||
signal(''),
|
||||
(p) => {
|
||||
metadata(p, RES, () => 'trigger');
|
||||
},
|
||||
{injector: TestBed.inject(Injector)},
|
||||
);
|
||||
|
||||
expect(success).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue