- Notifications
You must be signed in to change notification settings - Fork27k
docs: add signal forms schema field configuration guide#66176
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
docs: add signal forms schema field configuration guide#66176
Conversation
Deployed adev-preview for720aa14 to:https://ng-dev-previews-fw--pr-angular-angular-66176-adev-prev-i5w2jvjx.web.app Note: As new commits are pushed to this pull request, this link is updated after the preview is rebuilt. |
| @@ -0,0 +1,827 @@ | |||
| #Schema field configuration | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Can we call this guide "Adding form logic" or something like that, and then say "signal forms allows you to add logic to your form using schemas" and just refer to it as "schemas" from the on instead of "schema field configuration"
| @@ -0,0 +1,827 @@ | |||
| #Schema field configuration | |||
| Signal Forms allow you to configure how fields behave beyond validation through the form schema. You can disable fields conditionally, hide them based on other values, make them readonly, debounce user input, and attach metadata for custom controls. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This sounds like its saying that schemas are for logic other than validation, and you specify validation some other way. I think we could make it more clear by saying that validation logic is covered in [link] and this guide discusses other rules available in schemas
| ##How configuration functions work | ||
| Most configuration functions accept an optional callback function that makes the behavior reactive to form state. This means that the callback function runs automatically whenever the referenced field values change. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Ah I see where the "configuration" stuff is coming from now. We call these rules, e.g. "thedisabled rule". I think a clearer way to say this is "Rules bind reactive logic to specific fields in your form. Most rules accept a reactive logic function as an optional argument. The reactive logic function automatically recomputes whenever the signals it references change, just like acomputed"
Maybe we could try to diagram out the anatomy or something:
disabled(schemaPath.couponCode, ({valueOf}) => valueOf(schemaPath.total) < 50);~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~rule path reactive logic functionbencodezen commentedDec 18, 2025
This is fantastic feedback@mmalerba! I played around with a few different terminology sets and wanted to see how this would land, so appreciate the candid thoughts. I'll make the changes accordingly before marking it ready for review again. |
| label:'Field state management', | ||
| path:'guide/forms/signals/field-state-management', | ||
| contentPath:'guide/forms/signals/field-state-management', | ||
| status:'new', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Shouldn't this new'new' be on the'Schema field configuration' object?
| -`REQUIRED` - Whether the field is required (boolean) | ||
| -`MIN` - Minimum numeric value (number | undefined) | ||
| -`MAX` - Maximum numeric value (number | undefined) | ||
| -`MIN_LENGTH` - Minimum string/array length (number | undefined) | ||
| -`MAX_LENGTH` - Maximum string/array length (number | undefined) | ||
| -`PATTERN` - Regular expression pattern (RegExp[] - array to support multiple patterns) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
nit:
| -`REQUIRED` - Whether the field is required (boolean) | |
| -`MIN` - Minimum numeric value (number | undefined) | |
| -`MAX` - Maximum numeric value (number | undefined) | |
| -`MIN_LENGTH` - Minimum string/array length (number | undefined) | |
| -`MAX_LENGTH` - Maximum string/array length (number | undefined) | |
| -`PATTERN` - Regular expression pattern (RegExp[] - array to support multiple patterns) | |
| -`REQUIRED` - Whether the field is required (`boolean`) | |
| -`MIN` - Minimum numeric value (`number | undefined`) | |
| -`MAX` - Maximum numeric value (`number | undefined`) | |
| -`MIN_LENGTH` - Minimum string/array length (`number | undefined`) | |
| -`MAX_LENGTH` - Maximum string/array length (`number | undefined`) | |
| -`PATTERN` - Regular expression pattern (`RegExp[]` - array to support multiple patterns) |
| const score = value() | ||
| if (score < 0 || score > 100) { | ||
| return {kind: 'range', message: 'Score must be between 0 and 100'} | ||
| } | ||
| return null |
JeanMecheDec 18, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Nit: I would expect the auto-formatting to add those.
| const score = value() | |
| if (score < 0 || score > 100) { | |
| return {kind: 'range', message: 'Score must be between 0 and 100'} | |
| } | |
| return null | |
| const score = value(); | |
| if (score < 0 || score > 100) { | |
| return {kind: 'range', message: 'Score must be between 0 and 100'}; | |
| } | |
| return null; |
Edit: Oh okay, it doesn't because prettier don't understandangular-ts.
Anyway, it's just a nit.
It would be worth asking if prettier could support the angular syntax code block. I openedprettier/prettier#18500 for this.
| This means users can type quickly, tab away, or submit the form without waiting for debounce delays to expire. | ||
| ###Custom debounce logic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This section is inaccurate. From thedurationOrDebouncer parameter documentation:
angular/packages/forms/signals/src/api/rules/debounce.ts
Lines 21 to 22 in5a146b3
| *@param durationOrDebouncer Either a debounce duration in milliseconds, or a custom | |
| * {@link Debouncer} function. |
TheDebouncer function isn't a reactive logic function. It's just a callback that is called every time the control value is updated. It's expected to return nothing (undefined) to immediately synchronize the update, or a promise that will prevent synchronization until it resolves.
For example, thedebounce() rule converts the duration to such a function:
angular/packages/forms/signals/src/api/rules/debounce.ts
Lines 33 to 51 in5a146b3
| constdebouncer= | |
| typeofdurationOrDebouncer==='function' | |
| ?durationOrDebouncer | |
| :durationOrDebouncer>0 | |
| ?debounceForDuration(durationOrDebouncer) | |
| :immediate; | |
| pathNode.builder.addMetadataRule(DEBOUNCER,()=>debouncer); | |
| } | |
| functiondebounceForDuration(durationInMilliseconds:number):Debouncer<unknown>{ | |
| return(_context,abortSignal)=>{ | |
| returnnewPromise((resolve)=>{ | |
| consttimeoutId=setTimeout(resolve,durationInMilliseconds); | |
| abortSignal.addEventListener('abort',()=>clearTimeout(timeoutId)); | |
| }); | |
| }; | |
| } | |
| functionimmediate(){} |
| -`MAX_LENGTH` - Maximum string/array length (number | undefined) | ||
| -`PATTERN` - Regular expression pattern (RegExp[] - array to support multiple patterns) | ||
| When you use validation rules like`required()` or`min()`, they automatically set the corresponding metadata. Custom controls can read this metadata to configure HTML attributes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
"configure HTML attributes" is overly specific and already done automatically for the built-in ones. I think it's sufficient to just saymetadata() provides a way to publish some additional data associated with a field.
IIRC@mmalerba had a good example for labels?
| [required]="isRequired()" | ||
| [min]="minValue()" | ||
| [max]="maxValue()" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
This won't compile. The type checker prevents the user from explicitly binding any of the built-in attributes when the[field] directive is present, because it'll automatically bind these for you.
| }) | ||
| // Manually set metadata for custom controls | ||
| metadata(schemaPath.score, MIN, () => 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I don't think we want to encourage setting built-in metadata this way.min(schemaPath.score, 0) is preferable in this case.
| ###Managed metadata with reducers | ||
| For more complex metadata that needs to accumulate values, use`createManagedMetadataKey()` with a reducer function: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
@mmalerba correct me if I'm wrong, but this isn't the purpose of the "managed" key. All metadata keys support reducers for accumulating values if desired. The "managed" variant is for data that wants to compute a new valuefrom the accumulated/reduced value.
| </label> | ||
| <label> | ||
| Quantity (max: {{ maxQuantity() }}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Great example for a custom metadata key, but sinceMAX is built-in, there's already a signal that exposes it directly:
| Quantity (max: {{maxQuantity() }}) | |
| Quantity (max: {{inventoryForm.quantity().max() }}) |
| <input | ||
| type="number" | ||
| [value]="value()" | ||
| (input)="value.set(($event.target as HTMLInputElement).valueAsNumber)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
value.set won't work here sincevalue is a computed which are readonly.
I'd just go through the field here:
[value]="field().value()"(input)="field().value.set(...)"| [min]="minValue()" | ||
| [max]="maxValue()" | ||
| [required]="isRequired()" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
Again, since these three metadata are built-in, you can just use the built-in properties to access them:
| [min]="minValue()" | |
| [max]="maxValue()" | |
| [required]="isRequired()" | |
| [min]="field().min()" | |
| [max]="field().max()" | |
| [required]="field().required()" |
| (schemaPath) => { | ||
| // Only applied when country is US | ||
| required(schemaPath.zipCode) | ||
| metadata(schemaPath.zipCode, PATTERN, () => /^\d{5}(-\d{4})?$/) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
| metadata(schemaPath.zipCode, PATTERN, () => /^\d{5}(-\d{4})?$/) | |
| pattern(schemaPath.zipCode, /^\d{5}(-\d{4})?$/) |
| function emailFieldConfig(path: SchemaPath<string>) { | ||
| debounce(path, 300) | ||
| metadata(path, PLACEHOLDER, () => 'user@example.com') | ||
| metadata(path, MAX_LENGTH, () => 255) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
| metadata(path, MAX_LENGTH, () => 255) | |
| maxLength(path, 255) |
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: N/A
What is the new behavior?
Does this PR introduce a breaking change?
Other information