Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ea58962

Browse filesBrowse files
authored
Drop resolve from everywhere (#61614)
* forms: Drop resolve, and use the new API everywhere * fixup: Minor cleanup --------- Co-authored-by: kirjs <kirjs@users.noreply.github.com>
1 parent e221067 commit ea58962
Copy full SHA for ea58962

File tree

Expand file treeCollapse file tree

8 files changed

+131
-108
lines changed
Filter options
Expand file treeCollapse file tree

8 files changed

+131
-108
lines changed

‎packages/forms/experimental/docs/signal-forms.md

Copy file name to clipboardExpand all lines: packages/forms/experimental/docs/signal-forms.md
+16-14Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -264,21 +264,23 @@ passwordForm.password.$state.errors(); // []
264264
passwordForm.confirm.$state.errors(); // []
265265
```
266266

267-
##### Approach #2: Use `resolve` to access other fields' values
267+
##### Approach #2: Use helper functions to access other fields' state or values
268268

269-
If you need to access other fields' values but don't want to move the logic to a common parent node, you can define the logic on the desired field and then use the `resolve` function provided in the `FieldContext` to access the state of _other_ fields.
269+
If you need to access other fields' values but don't want to move the logic to a common parent node, you can define the logic on the desired field. The `FieldContext` provides helper functions to access the state or value of _other_ fields:
270270

271-
The `resolve()` function takes another `FieldPath` from anywhere in your field structure and returns the corresponding `Field` instance for that path. You can then access its `$state` to get its value, state, etc.
271+
- **`valueOf(otherPath: FieldPath<U>): U`**: Directly retrieves the current value of the field at `otherPath`.
272+
- **`stateOf(otherPath: FieldPath<U>): FieldState<U>`**: Retrieves the `FieldState` instance for the field at `otherPath` (e.g., `stateOf(otherPath).disabled()`).
273+
- - **`fieldOf(otherPath: FieldPath<U>): Field<U>`**: Retrieves the `Field` instance for the field at `otherPath`. This is useful when you want to access its decendants or specific array items.
272274

273-
Here's the same password matching validation, but associating the error with the `confirm` field instead:
275+
Here's the same password matching validation, but associating the error with the `confirm` field instead, using `valueOf`:
274276

275277
```typescript
276278
const passwordSchema: Schema<ConfirmedPassword> = (path: FieldPath<ConfirmedPassword>) => {
277279
// Add validation on the `confirm` field that considers both the
278280
// `password` and `confirm` values.
279-
validate(path.confirm, ({value, resolve}: FieldContext<string>) => {
280-
// Get the `Field` for `path.password` and read its value.
281-
const password = resolve(path.password).$state.value();
281+
validate(path.confirm, ({value, valueOf}: FieldContext<string>) => {
282+
// Get the value of `path.password`.
283+
const password = valueOf(path.password);
282284
// Compare the password and confirm values, return an error if they don't match.
283285
if (password !== value()) {
284286
return {kind: 'non-matching', message: 'Password and confirm must match'};
@@ -295,7 +297,7 @@ passwordForm.password.$state.errors(); // []
295297
passwordForm.confirm.$state.errors(); // [{kind: 'non-matching', message: 'Password and confirm must match'}]
296298
```
297299

298-
Note that because `value()` in `resolve(path.password).$state.value()` is a signal read, this establishes a reactive dependency on the value of `password` as well as the value of `confirm`, ensuring that the validation is recomputed if either one changes.
300+
Note that because `valueOf(path.password)` reads a signal internally (as does `value()` for the current field), this establishes a reactive dependency on the value of `password` as well as the value of `confirm`, ensuring that the validation is recomputed if either one changes.
299301

300302
### Composing logic from multiple schemas
301303

@@ -360,9 +362,9 @@ const tripSchema: Schema<Trip> = (tripPath: FieldPath<Trip>) => {
360362
apply(tripPath.end, dateSchema);
361363

362364
// More trip-specific date logic, will be merged with standard date logic above.
363-
error(tripPath.end, ({value, resolve}: FieldContext<SimpleDate>) => {
364-
const startField = resolve(tripPath.start);
365-
return compareTo(value(), startField.$state.value()) < 0;
365+
error(tripPath.end, ({value, valueOf}: FieldContext<SimpleDate>) => {
366+
const startValue = valueOf(tripPath.start);
367+
return compareTo(value(), startValue) < 0;
366368
}, 'Trip must end after it starts');
367369
};
368370
```
@@ -505,8 +507,8 @@ Angular Signal Forms provides a `submit()` helper function to manage this workfl
505507

506508
1. **`field`**: The `Field` instance to submit. This can be the root field or any sub-field node.
507509
2. **`action`**: An asynchronous function that performs the submission action. It receives the `field` being submitted as an argument and returns a `Promise`.
508-
- The returned `Promise` resolves with `void` (or `undefined`, or `[]`) if the action completes successfully without server-side validation errors.
509-
- It resolves with an array of `ServerError` if the submission fails due to server-side validation or other issues that need to be reported back onto the form fields. The `ServerError` structure is detailed in the next section.
510+
- The returned `Promise` resolves with `void` (or `undefined`, or `[]`) if the action completes successfully without server-side validation errors.
511+
- It resolves with an array of `ServerError` if the submission fails due to server-side validation or other issues that need to be reported back onto the form fields. The `ServerError` structure is detailed in the next section.
510512

511513
All `FieldState` objects have a `submittedStatus` signal that indicates their current submit state. The status can be `'unsubmitted'`, `'submitting'`, or `'submitted'`. There is no status to indicate that the submit errored because errors are reported through the `errors()` state the same way as client validation errors. (This is discussed more in the next section). `FieldState` objects also have a `resetSubmittedStatus()` method which sets the `submittedStatus` back to `'unsubmitted'`.
512514

@@ -627,4 +629,4 @@ The `[field]` directive works out-of-the-box with standard HTML form elements li
627629

628630
It can also integrate with custom form components (including those from libraries like Angular Material - e.g., `<mat-select>`, `<mat-radio>`) provided they correctly implement Angular's `ControlValueAccessor` interface. This is the standard mechanism in Angular for components to participate in forms.
629631

630-
<!-- TODO: add a more in depth section on how to integrate your own custom UI controls -->
632+
<!-- TODO: add a more in-depth section on how to integrate your own custom UI controls -->

‎packages/forms/experimental/docs/tutorial.md

Copy file name to clipboardExpand all lines: packages/forms/experimental/docs/tutorial.md
+48-48Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ We're going to build a feedback form with the following fields:
2525
* text [feedback] disabled if rating is 5, otherwise required
2626
* checkbox [recommendToFriends]
2727
* array [friends] only displayed/validated when recommendToFriends is true
28-
* text [name] required
29-
* text [email] required, must have @
28+
* text [name] required
29+
* text [email] required, must have @
3030
```
3131

3232
## Initial setup
@@ -84,7 +84,7 @@ So first, we need to create a signal with initial values:
8484

8585
```typescript
8686
// feedback.ts
87-
import {ChangeDetectionStrategy, Component} from '@angular/core';
87+
import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; // Added signal import
8888

8989
@Component({/*...*/})
9090
export class FeedbackComponent {
@@ -96,7 +96,6 @@ export class FeedbackComponent {
9696
confirmationPassword: '',
9797
feedback: '',
9898
recommendToFriends: false,
99-
recommendationText: '',
10099
friends: [],
101100
});
102101
}
@@ -130,6 +129,9 @@ import {
130129
form,
131130
FieldDirective,
132131
} from 'google3/experimental/angularsignalforms';
132+
import { MatFormFieldModule } from '@angular/material/form-field';
133+
import { MatInputModule } from '@angular/material/input';
134+
133135

134136
@Component({/*...*/
135137
imports: [
@@ -345,9 +347,9 @@ export class FeedbackComponent {
345347

346348
Now let's write a custom validator to ensure that the password and confirmation password match.
347349

348-
To do this, we will use the special `resolve` function provided to the validator. `resolve` takes a path segment and returns the corresponding form field instance.
350+
To do this, we will use the special `valueOf` function provided to the validator. `valueOf` takes a path segment and returns the corresponding form field's value.
349351

350-
> `resolve` can be used for cross-field validation.
352+
> Besides `valueOf`, you can also use `stateOf` and `fieldOf` can be used for cross-field validation.
351353
352354
```typescript
353355
// feedback.ts
@@ -359,8 +361,8 @@ export class FeedbackComponent {
359361
required(path.password);
360362
required(path.confirmationPassword);
361363

362-
validate(path.confirmationPassword, ({value, resolve}) => {
363-
return value() === resolve(path.password).$state.value()
364+
validate(path.confirmationPassword, ({value, valueOf}) => {
365+
return value() === valueOf(path.password)
364366
? undefined
365367
: {kind: 'confirmationPassword'};
366368
});
@@ -397,8 +399,8 @@ To do this we'd create a constructor function, which would take a path with the
397399
export function confirmationPasswordValidator(
398400
path: FieldPath<{password: string}>,
399401
): Validator<string> {
400-
return ({value, resolve}) => {
401-
return value() === resolve(path.password).$state.value()
402+
return ({value, valueOf}) => {
403+
return value() === valueOf(path.password)
402404
? undefined
403405
: {kind: 'confirmationPassword'};
404406
};
@@ -446,7 +448,7 @@ export class RatingComponent implements FormUiControl<number> {
446448

447449
### Displaying the stars
448450
This is unrelated to Forms.
449-
you can see full implementation [here](http://google3/experimental/users/kirjs/forms/app/feedback/rating.ts)
451+
you can see full implementation [here](http://google3/experimental/users/kirjs/forms/app/feedback/rating.ts)
450452

451453
### Using rating in the Feedback component template
452454

@@ -477,8 +479,8 @@ export class FeedbackComponent {
477479
/* ... */
478480
readonly form = form(this.data, (path) => {
479481
/* ... */
480-
disabled(path.feedback, ({resolve}) => {
481-
return resolve(path.rating).$state.value() > 4;
482+
disabled(path.feedback, ({valueOf}) => {
483+
return valueOf(path.rating) > 4;
482484
});
483485

484486
// When a field is disabled, validation doesn't run
@@ -531,10 +533,8 @@ This code should look familiar: Both fields are required, and the email has its
531533
```typescript
532534
// friend.ts
533535
import {
534-
Field,
535536
Schema,
536537
required,
537-
FieldDirective,
538538
validate,
539539
} from 'google3/experimental/angularsignalforms';
540540

@@ -562,23 +562,21 @@ import {
562562

563563
export const emailValidator: Validator<string> =
564564
({value}) => {
565-
return !value().includes('@') ? undefined : {kind: 'email'};
565+
return !value().includes('@') ? undefined : {kind: 'email'};
566566
};
567567
```
568568
Now we can use it in the schema (don't forget to use in feedback component as well).
569569

570570
```typescript
571571
// friend.ts
572572
import {
573-
Field,
574573
Schema,
575574
required,
576-
FieldDirective,
577575
validate,
578576
} from 'google3/experimental/angularsignalforms';
579577
import {emailValidator} from './validators';
580578

581-
// Schema is not used in this file.
579+
// Schema is not used in this file.
582580
export const friendSchema: Schema<Friend> = (friend) => {
583581
required(friend.name);
584582
required(friend.email);
@@ -591,13 +589,8 @@ This component will display the form fields for a single friend and used in arra
591589

592590
```typescript
593591
// friend.ts
594-
import {
595-
Field,
596-
Schema,
597-
required,
598-
FieldDirective,
599-
validate,
600-
} from 'google3/experimental/angularsignalforms';
592+
import { Component, ChangeDetectionStrategy, input } from '@angular/core';
593+
import { Field } from 'google3/experimental/angularsignalforms';
601594

602595

603596
@Component({
@@ -694,10 +687,9 @@ Then, we'll display the list of friends, but only when the checkbox is checked.
694687
<app-friend [friend]="friend"></app-friend>
695688
}
696689
}
697-
</fieldset>
698690
```
699691

700-
### Hiding
692+
### Hiding
701693
The current setup works, but there's a small issue.
702694
If we create a friend with an error, and then hide it, the validation would still run, and the form would be marked as invalid.
703695

@@ -706,11 +698,12 @@ We can solve it by using `hidden` with a predicate to disabled the validation.
706698
```typescript
707699
// feedback.ts
708700
import {
709-
/* ... */
701+
/* ... */
710702
applyEach,
703+
hidden
711704
} from 'google3/experimental/angularsignalforms';
712705

713-
import { friendSchema } from './friend';
706+
import { friendSchema } from './friend';
714707

715708
/* ... */
716709
export class FeedbackComponent {
@@ -719,13 +712,13 @@ export class FeedbackComponent {
719712
/* ... */
720713
applyEach(path.friends, friendSchema);
721714
// Doesn't actually hide anything in the UI.
722-
hidden(path.friends, ({resolve}) => {
723-
return resolve(path.recommendToFriends).$state.value() === false ;
715+
hidden(path.friends, ({valueOf}) => {
716+
return valueOf(path.recommendToFriends) === false ;
724717
});
725718
});
726719
}
727720
```
728-
> it's important to note, that hidden doesn't actually hide fields in the template, just disables validation.
721+
> it's important to note, that `hidden` doesn't actually hide fields in the template, just disables validation.
729722
730723
### Conditionally enabling/disabling validation with applyWhen
731724
Sometimes we want to apply multiple rules based only if certain condition is true.
@@ -736,28 +729,28 @@ Let's look at an unrelated example, where we want to apply different rules depen
736729

737730
```typescript
738731
// unrelated-form.ts
739-
form(this.pet, (pet) => {
732+
form(this.pet, (pet: FieldPath<any>) => {
740733
// Applies for all pets
741734
required(pet.cute);
742735

743736
// Rules that only apply for dogs.
744737
applyWhen(
745-
path,
738+
pet,
746739
({value}) => value().type === 'dog',
747740
(pathWhenTrue) => {
748741
// Only required for dogs, but can be entered for cats
749-
requred(pathWhenTrue.walksPerDay);
742+
required(pathWhenTrue.walksPerDay);
750743
// Doesn't apply for dogs
751744
hidden(pathWhenTrue.purringIntensity);
752745
}
753746
);
754747

755748
applyWhen(
756-
path,
749+
pet,
757750
({value}) => value().type === 'cat',
758751
(pathWhenTrue) => {
759752
// Those rules only apply for cats.
760-
requred(pathWhenTrue.a);
753+
required(pathWhenTrue.a);
761754
validate(pathWhenTrue.b, /* validation rules */);
762755
applyEach(pathWhenTrue, /* array rules */);
763756
applyWhen(/* we can even have nested apply whens. */);
@@ -776,6 +769,7 @@ It's also important to not use closured path, but use the one provided by the fu
776769
export class FeedbackComponent {
777770
/* ... */
778771
readonly form = form(this.data, (path) => {
772+
/* ... other rules ... */
779773
applyWhen(
780774
path,
781775
({value}) => value().recommendToFriends,
@@ -876,27 +870,33 @@ This marks the end of the tutorial. Let's take a look at the complete form defin
876870
/* ... */
877871
export class FeedbackComponent {
878872
/* ... */
879-
readonly form = form(this.data, (path) => {
873+
readonly form = form(this.data, (path: FieldPath<Feedback>) => {
880874
required(path.name);
875+
876+
required(path.email);
877+
validate(path.email, emailValidator);
881878

882-
disabled(path.feedback, ({resolve}) => {
883-
return resolve(path.rating).$state.value() > 4;
879+
required(path.password);
880+
required(path.confirmationPassword);
881+
882+
disabled(path.feedback, ({valueOf}) => {
883+
return valueOf(path.rating) > 4;
884884
});
885885
required(path.feedback);
886886

887-
validate(path.confirmationPassword, ({value, resolve}) => {
888-
return value() === resolve(path.password).$state.value()
887+
validate(path.confirmationPassword, ({value, valueOf}) => {
888+
return value() === valueOf(path.password)
889889
? undefined
890890
: {kind: 'confirmationPassword'};
891891
})
892892

893893
applyWhen(
894-
path,
895-
(f) => f.value().recommendToFriends,
896-
(path) => {
897-
applyEach(path.friends, friendSchema);
894+
path,
895+
({value}) => value().recommendToFriends,
896+
(pathWhenTrue) => {
897+
applyEach(pathWhenTrue.friends, friendSchema);
898898
},
899899
);
900900
});
901901
}
902-
```
902+
```

‎packages/forms/experimental/src/api/async.ts

Copy file name to clipboardExpand all lines: packages/forms/experimental/src/api/async.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function validateAsync<TValue, TRequest, TData>(
3131

3232
const dataKey = defineResource(path, {
3333
params: (ctx) => {
34-
const node = ctx.resolve(path).$state as FieldNode;
34+
const node = ctx.stateOf(path) as FieldNode;
3535
if (node.shouldSkipValidation() || !node.syncValid()) {
3636
return undefined;
3737
}

‎packages/forms/experimental/src/api/types.ts

Copy file name to clipboardExpand all lines: packages/forms/experimental/src/api/types.ts
-5Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,5 @@ export interface FieldContext<T> {
204204
readonly valueOf: <P>(p: FieldPath<P>) => P;
205205
readonly stateOf: <P>(p: FieldPath<P>) => FieldState<P>;
206206
readonly fieldOf: <P>(p: FieldPath<P>) => Field<P>;
207-
/**
208-
* A function that gets the `Field` for a given `FieldPath`.
209-
* This can be used by the `LogicFunction` to implement cross-field logic.
210-
*/
211-
resolve: <U>(path: FieldPath<U>) => Field<U>;
212207
data: <D>(path: DataKey<D>) => D;
213208
}

‎packages/forms/experimental/src/field_node.ts

Copy file name to clipboardExpand all lines: packages/forms/experimental/src/field_node.ts
-2Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,6 @@ export class FieldNode implements FieldState<unknown> {
182182
};
183183
return (this._fieldContext ??= {
184184
value: this.value,
185-
// TODO: Drop resolve and data
186-
resolve,
187185
state: this,
188186
field: this.fieldProxy,
189187
stateOf<P>(p: FieldPath<P>) {

‎packages/forms/experimental/src/logic_node.ts

Copy file name to clipboardExpand all lines: packages/forms/experimental/src/logic_node.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ function wrapWithPredicate<TValue, TReturn>(
201201
return logicFn;
202202
}
203203
return (arg: FieldContext<any>): TReturn => {
204-
const predicateField = arg.resolve(predicate.path).$state as FieldNode;
204+
const predicateField = arg.stateOf(predicate.path) as FieldNode;
205205
if (!predicate.fn(predicateField.fieldContext)) {
206206
// don't actually run the user function
207207
return defaultValue;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.