Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9edad01
feat: initial work on FormGroupApi in core
crutchcorn Apr 16, 2026
dbc7ff6
chore: initial (and wrong) per-form-group-validation
crutchcorn Apr 16, 2026
1a22f52
chore: create a common FieldLikeAPI to adopt in form groups shortly
crutchcorn Apr 16, 2026
4775996
chore: implement FieldLike API on FormGroup
crutchcorn Apr 16, 2026
756ddf8
chore: revert changes to FormApi validation logic
crutchcorn Apr 16, 2026
c553439
chore: fix type issues with validation kind
crutchcorn Apr 16, 2026
d2eaa29
chore: minor fixes
crutchcorn Apr 16, 2026
ac22e7b
Revert "chore: revert changes to FormApi validation logic"
crutchcorn Apr 16, 2026
c2d061b
chore: filter fields to validate in formgroup
crutchcorn Apr 16, 2026
433ad6e
chore: improve store
crutchcorn Apr 16, 2026
22e49a7
chore: fix tests
crutchcorn Apr 16, 2026
db88246
chore: fix another test
crutchcorn Apr 16, 2026
4eb9b0f
chore: add submitmeta test
crutchcorn Apr 16, 2026
61d36cd
ci: apply automated fixes and generate docs
autofix-ci[bot] Apr 16, 2026
9b8bcb5
chore: add FormLike methods to FormGroup
crutchcorn Apr 20, 2026
86d9000
Merge branch 'form-group' of https://github.com/TanStack/form into fo…
crutchcorn Apr 20, 2026
d990bc6
chore: add type tests
crutchcorn Apr 22, 2026
66c5db9
chore: fix build
crutchcorn Apr 22, 2026
3c8ff5c
chore: fix type tests
crutchcorn Apr 22, 2026
4e4b272
ci: apply automated fixes and generate docs
autofix-ci[bot] Apr 22, 2026
ce63f8f
Merge branch 'main' into form-group
crutchcorn May 6, 2026
6094b9c
chore: export two missing items
crutchcorn May 6, 2026
1f96984
chore: add new interop FormGroupOptions type
crutchcorn May 6, 2026
ee121e4
chore: initial implement of useFormGroup in React
crutchcorn May 6, 2026
c46e342
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
3b756ae
chore: add formGroup to useForm
crutchcorn May 6, 2026
93ec42f
chore: add initial React implementation tests
crutchcorn May 6, 2026
da467f3
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
d1471f6
chore: add type tests for React adapter
crutchcorn May 6, 2026
e3e81b9
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
23e8066
chore: fix eslint
crutchcorn May 6, 2026
c86b424
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
4b81818
chore: add multi-step wizard
crutchcorn May 6, 2026
40b2758
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
e9ee38c
fix: handle resubmissions
crutchcorn May 6, 2026
bfa9464
fix: validate on resubmit
crutchcorn May 6, 2026
a86f781
feat: add better onDynamic handling to FormGroupApi
crutchcorn May 6, 2026
fec4e73
chore: first attempt to fix onDynamic change handling on groups
crutchcorn May 6, 2026
c3d6176
chore: attempt 2
crutchcorn May 6, 2026
973fab7
chore: attempt 3
crutchcorn May 6, 2026
7546f08
chore: attempt 4
crutchcorn May 6, 2026
c8ec12e
Revert "chore: attempt 4"
crutchcorn May 6, 2026
d499d20
chore: finalize fixing form errors
crutchcorn May 6, 2026
58ed8a2
chore: fix eslint
crutchcorn May 6, 2026
f69771b
chore: regenerate lockfile
crutchcorn May 6, 2026
7d9ca0d
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
9e78281
docs: update the wizard example
crutchcorn May 6, 2026
4837dd9
ci: apply automated fixes and generate docs
autofix-ci[bot] May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/framework/angular/guides/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ This will generate the mapped JSX every time you run `pushValue` on `field`:
Finally, you can use a subfield like so:

```angular-html
<ng-container
<ng-container
[tanstackField]="form"
[name]="getPeopleName($index)"
#person="field"
Expand All @@ -57,9 +57,7 @@ Finally, you can use a subfield like so:
<div>Name for person {{ $index }}</div>
<input
[value]="person.api.state.value"
(input)="
person.api.handleChange($any($event).target.value)
"
(input)="person.api.handleChange($any($event).target.value)"
/>
</label>
</div>
Expand All @@ -81,7 +79,10 @@ export class AppComponent {
> See, if we did the following:
>
> ```angular-html
> <ng-container [tanstackField]="form" [name]="'people[' + $index + '].name'"></ng-container>
> <ng-container
> [tanstackField]="form"
> [name]="'people[' + $index + '].name'"
> ></ng-container>
> ```
>
> We'd be running into a TypeScript issue where `"one" + "two"` is `string` rather than the required `"onetwo"` type
Expand Down Expand Up @@ -145,8 +146,7 @@ export class AppComponent {
},
})


getPeopleName = (idx: number) => `people[${idx}].name` as const;
getPeopleName = (idx: number) => `people[${idx}].name` as const

canSubmit = injectStore(this.form, (state) => state.canSubmit)
isSubmitting = injectStore(this.form, (state) => state.isSubmitting)
Expand Down
21 changes: 10 additions & 11 deletions docs/framework/angular/guides/basic-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,19 @@ Example:
`,
})
export class AppComponent {
firstNameValidator: FieldValidateFn<any, any, string, any> = ({
value,
}) =>
firstNameValidator: FieldValidateFn<any, any, string, any> = ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined

firstNameAsyncValidator: FieldValidateAsyncFn<any, string, any> =
async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error') && 'No "error" allowed in first name'
}
firstNameAsyncValidator: FieldValidateAsyncFn<any, string, any> = async ({
value,
}) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error') && 'No "error" allowed in first name'
}

form = injectForm({
defaultValues: {
Expand Down Expand Up @@ -176,7 +175,7 @@ import { z } from 'zod'
[validators]="{
onChange: z.string().min(3, 'First name must be at least 3 characters'),
onChangeAsyncDebounceMs: 500,
onChangeAsync: firstNameAsyncValidator
onChangeAsync: firstNameAsyncValidator,
}"
#firstName="field"
>
Expand Down Expand Up @@ -343,8 +342,8 @@ export class AppComponent {
yearsOfExperience: 0,
}

getHobbyName = (idx: number) => `hobbies[${idx}].name` as const;
getHobbyDesc = (idx: number) => `hobbies[${idx}].description` as const;
getHobbyName = (idx: number) => `hobbies[${idx}].name` as const
getHobbyDesc = (idx: number) => `hobbies[${idx}].description` as const

form = injectForm({
defaultValues: {
Expand Down
60 changes: 38 additions & 22 deletions docs/framework/angular/guides/dynamic-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ We support this through our `onDynamic` validation function.

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
import {
TanStackField,
injectForm,
revalidateLogic,
} from '@tanstack/angular-form'

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<!-- Your form template here -->
`,
template: ` <!-- Your form template here --> `,
})
export class AppComponent {
form = injectForm({
Expand Down Expand Up @@ -65,9 +67,7 @@ You can, for example, use the following to revalidate on blur after the first su
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<!-- Your form template here -->
`,
template: ` <!-- Your form template here --> `,
})
export class AppComponent {
form = injectForm({
Expand All @@ -87,15 +87,18 @@ Just as you might access errors from an `onChange` or `onBlur` validation, you c

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, injectStore, revalidateLogic } from '@tanstack/angular-form'
import {
TanStackField,
injectForm,
injectStore,
revalidateLogic,
} from '@tanstack/angular-form'

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<p>{{ formErrorMap().onDynamic?.firstName }}</p>
`,
template: ` <p>{{ formErrorMap().onDynamic?.firstName }}</p> `,
})
export class AppComponent {
form = injectForm({
Expand All @@ -121,7 +124,12 @@ You can use `onDynamic` validation alongside other validation logic, such as `on

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, injectStore, revalidateLogic } from '@tanstack/angular-form'
import {
TanStackField,
injectForm,
injectStore,
revalidateLogic,
} from '@tanstack/angular-form'

@Component({
selector: 'app-root',
Expand Down Expand Up @@ -167,7 +175,11 @@ You can also use `onDynamic` validation with fields, just like you would with ot

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
import {
TanStackField,
injectForm,
revalidateLogic,
} from '@tanstack/angular-form'
import type { FieldValidateFn } from '@tanstack/angular-form'

@Component({
Expand All @@ -180,7 +192,7 @@ import type { FieldValidateFn } from '@tanstack/angular-form'
[tanstackField]="form"
name="age"
[validators]="{
onDynamic: ageValidator
onDynamic: ageValidator,
}"
#age="field"
>
Expand Down Expand Up @@ -229,15 +241,17 @@ Async validation can also be used with `onDynamic` just like with other validati

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
import {
TanStackField,
injectForm,
revalidateLogic,
} from '@tanstack/angular-form'

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<!-- Your form template here -->
`,
template: ` <!-- Your form template here --> `,
})
export class AppComponent {
form = injectForm({
Expand Down Expand Up @@ -266,16 +280,18 @@ You can also use standard schema validation libraries like Valibot or Zod with `

```angular-ts
import { Component } from '@angular/core'
import { TanStackField, injectForm, revalidateLogic } from '@tanstack/angular-form'
import {
TanStackField,
injectForm,
revalidateLogic,
} from '@tanstack/angular-form'
import { z } from 'zod'

@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<!-- Your form template here -->
`,
template: ` <!-- Your form template here --> `,
})
export class AppComponent {
schema = z.object({
Expand Down
14 changes: 3 additions & 11 deletions docs/framework/angular/guides/form-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ import { TanStackField, injectForm, injectStore } from '@tanstack/angular-form'
imports: [TanStackField],
template: `
<div>
<ng-container
[tanstackField]="form"
name="firstName"
#firstName="field"
>
<ng-container [tanstackField]="form" name="firstName" #firstName="field">
<label [for]="firstName.api.name">First Name:</label>
<input
[id]="firstName.api.name"
Expand All @@ -47,11 +43,7 @@ import { TanStackField, injectForm, injectStore } from '@tanstack/angular-form'
</ng-container>
</div>
<div>
<ng-container
[tanstackField]="form"
name="lastName"
#lastName="field"
>
<ng-container [tanstackField]="form" name="lastName" #lastName="field">
<label [for]="lastName.api.name">Last Name:</label>
<input
[id]="lastName.api.name"
Expand Down Expand Up @@ -91,7 +83,7 @@ export class AppComponent {
This is functionally correct, but introduces a _lot_ of repeated templating behavior over and over. Instead, let's move the error handling, label to input binding, and other repeated logic into a component:

```angular-ts
import {injectField} from '@tanstack/angular-form'
import { injectField } from '@tanstack/angular-form'

@Component({
selector: 'app-text-field',
Expand Down
3 changes: 1 addition & 2 deletions docs/framework/angular/guides/listeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Events that can be "listened" to are:
[tanstackField]="form"
name="country"
[listeners]="{
onChange: onCountryChange
onChange: onCountryChange,
}"
#country="field"
></ng-container>
Expand All @@ -43,7 +43,6 @@ Events that can be "listened" to are:
></ng-container>
`,
})
export class AppComponent {
form = injectForm({
defaultValues: {
Expand Down
36 changes: 19 additions & 17 deletions docs/framework/angular/guides/submission-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,32 @@ You can accomplish this by specifying the `onSubmitMeta` property. This meta dat
> Note: if `form.handleSubmit()` is called without metadata, it will use the provided default.

```angular-ts
import { Component } from '@angular/core';
import { injectForm } from '@tanstack/angular-form';

import { Component } from '@angular/core'
import { injectForm } from '@tanstack/angular-form'

type FormMeta = {
submitAction: 'continue' | 'backToMenu' | null;
};
submitAction: 'continue' | 'backToMenu' | null
}

// Metadata is not required to call form.handleSubmit().
// Specify what values to use as default if no meta is passed
const defaultMeta: FormMeta = {
submitAction: null,
};
}

@Component({
selector: 'app-root',
template: `
<form (submit)="handleSubmit($event)">
<button type="submit" (click)="
handleClick({ submitAction: 'continue' })
">Submit and continue</button>
<button type="submit" (click)="
handleClick({ submitAction: 'backToMenu' })
">Submit and back to menu</button>
<button type="submit" (click)="handleClick({ submitAction: 'continue' })">
Submit and continue
</button>
<button
type="submit"
(click)="handleClick({ submitAction: 'backToMenu' })"
>
Submit and back to menu
</button>
</form>
`,
})
Expand All @@ -47,18 +49,18 @@ export class AppComponent {
onSubmitMeta: defaultMeta,
onSubmit: async ({ value, meta }) => {
// Do something with the values passed via handleSubmit
console.log(`Selected action - ${meta.submitAction}`, value);
console.log(`Selected action - ${meta.submitAction}`, value)
},
});
})

handleSubmit(event: SubmitEvent) {
event.preventDefault();
event.stopPropagation();
event.preventDefault()
event.stopPropagation()
}

handleClick(meta: FormMeta) {
// Overwrites the default specified in onSubmitMeta
this.form.handleSubmit(meta);
this.form.handleSubmit(meta)
}
}
```
Expand Down
Loading
Loading