Skip to content

feat(signal-forms): Add key & index support in FieldContext #62347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: prototype/signal-forms
Choose a base branch
from

Conversation

mmalerba
Copy link
Contributor

@mmalerba mmalerba commented Jun 27, 2025

TODO: try this and see if its worth the added type complexity:

Can a FieldPath know if it's generated from an array or object? We could encode the type of key() that way

@mmalerba mmalerba requested review from leonsenft, kirjs and alxhub June 27, 2025 19:27
@mmalerba mmalerba added area: forms target: feature This PR is targeted for a feature branch (outside of main and semver branches) labels Jun 27, 2025
@ngbot ngbot bot modified the milestone: Backlog Jun 27, 2025
@angular-robot angular-robot bot added the detected: feature PR contains a feature commit label Jun 27, 2025
@mmalerba mmalerba moved this to In Progress in Experimental Signal Forms Jun 27, 2025
@mmalerba mmalerba self-assigned this Jun 27, 2025
@mmalerba mmalerba marked this pull request as draft June 27, 2025 19:39
? {[K in keyof T]: ChildFieldPath<T[K]>}
: {});
export type ChildFieldPath<T> = {
[ɵɵTYPE]: [T, 'child'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this ɵɵTYPE property?

/**
 * Symbol used to retain generic type information when it would otherwise be lost.
 */
declare const ɵɵTYPE: unique symbol;

The definition makes it clear that T helps retain type information, but how is the 'child' or 'item' used?

const f = form(
cat,
(p) => {
//validate(p, recordKey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line supposed to be tested? I see there's a related comment in the expectation.

const f = form(
pets,
(p) => {
//validate(p, recordIndex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

} & (T extends any[]
? {}
: T extends Record<PropertyKey, any>
? {[K in keyof T]: FieldPath<T[K]>}
? {[K in keyof T]: ChildFieldPath<T[K]>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible or useful to define the elements of T when T extends any[] as ItemFieldPaths?

T extends any[]
  ? {[index: number]: ItemFieldPath<T[number]>}
  : T extends Record<PropertyKey, any>
    ? {[K in keyof T]: ChildFieldPath<T[K]>}
    : {}

Or is this an issue due to the dynamic nature of an array, because you don't know which indices are valid?

path: FieldPath<T> | ChildFieldPath<T> | ItemFieldPath<T>,
logic: NoInfer<Validator<T> | ChildValidator<T> | ItemValidator<T>>,
): void {
assertPathIsCurrent(path as FieldPath<T>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this as cast necessary? Don't all of the types in the union implement this type?

@@ -181,11 +184,14 @@ export function form<T>(...args: any[]): Field<T> {
* element of the array.
* @template T The data type of an element in the array.
*/
export function applyEach<T>(path: FieldPath<T[]>, schema: NoInfer<SchemaOrSchemaFn<T>>): void {
assertPathIsCurrent(path);
export function applyEach<T>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the array() in the comment @example meant to be applyEach()?

export function applyEach<T>(path: FieldPath<T[]>, schema: NoInfer<SchemaOrSchemaFn<T>>): void {
assertPathIsCurrent(path);
export function applyEach<T>(
path: FieldPath<T[]> | ChildFieldPath<T[]> | ItemFieldPath<T[]>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the union actually matter here? I'm guessing it's just meant to accept any type of field path, but can we define ChildFieldPath and ItemFieldPath to just satisfy FieldPath? That way the distinction would only matter in cases where you wish to explicitly read key() or index().

readonly index = computed(() => {
const key = this.key();
if (!Array.isArray(untracked(this.node.structure.parent!.value))) {
throw new Error(`RuntimeError: cannot access index, parent field is not an array`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know core has a system for runtime errors that supports "minifying" them in release mode. Is that something utilized by other Angular packages (e.g. forms, router) or only core?

@@ -199,7 +199,7 @@ export class ChildFieldNodeStructure extends FieldNodeStructure {
}

// Check the parent value at the last known key to avoid a scan.
const data = parentValue[lastKnownKey as number];
const data = parentValue[lastKnownKey as unknown as number];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I understanding correctly that this works because array[1] === array['1']? Is this better than Number(lastKnownKey)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: forms detected: feature PR contains a feature commit target: feature This PR is targeted for a feature branch (outside of main and semver branches)
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

2 participants