Skip to content

Commit 1cf2b0c

Browse files
crutchcornflensrockerautofix-ci[bot]
authored
feat: Add new AppField API for Angular (#1541)
* feat: Add new AppField API for Angular Co-authored-by: Lars Hanisch <blog@flensrocker.de> * ci: apply automated fixes and generate docs * chore: fix issues with bundler This PR breaks the abiltiy to run tests thanks to Testing Library seemingly not supporting Angular 20 * chore: WIP update Vitest setup * chore: fix test infra * ci: apply automated fixes and generate docs * chore: add back Zone.js to Test Setup * chore: fix validation logic * ci: apply automated fixes and generate docs * chore: fixed issues with change detection * chore: fix CI * chore: fix usage of `field.api` for consistency, wrote tests * docs(angular-form): Add docs * ci: apply automated fixes and generate docs --------- Co-authored-by: Lars Hanisch <blog@flensrocker.de> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent a8279d5 commit 1cf2b0c

30 files changed

+2387
-1353
lines changed

docs/config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@
191191
{
192192
"label": "Arrays",
193193
"to": "framework/angular/guides/arrays"
194+
},
195+
{
196+
"label": "Form Composition",
197+
"to": "framework/angular/guides/form-composition"
194198
}
195199
]
196200
},
@@ -568,6 +572,10 @@
568572
{
569573
"label": "Arrays",
570574
"to": "framework/angular/examples/array"
575+
},
576+
{
577+
"label": "Form Composition",
578+
"to": "framework/angular/examples/large-form"
571579
}
572580
]
573581
},
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
id: form-composition
3+
title: Form Composition
4+
---
5+
6+
A common criticism of TanStack Form is its verbosity out-of-the-box. While this _can_ be useful for educational purposes - helping enforce understanding our APIs - it's not ideal in production use cases.
7+
8+
As a result, while basic usage of `[tanstackField]` enables the most powerful and flexible usage of TanStack Form, we provide APIs that wrap it and make your application code less verbose.
9+
10+
## Pre-bound Field Components
11+
12+
If you've ever used TanStack Form in Angular to bind more than one input, you'll have quickly realized how much goes into each input:
13+
14+
```angular-ts
15+
import { Component } from '@angular/core'
16+
import { TanStackField, injectForm, injectStore } from '@tanstack/angular-form'
17+
18+
@Component({
19+
selector: 'app-root',
20+
standalone: true,
21+
imports: [TanStackField],
22+
template: `
23+
<div>
24+
<ng-container
25+
[tanstackField]="form"
26+
name="firstName"
27+
#firstName="field"
28+
>
29+
<label [for]="firstName.api.name">First Name:</label>
30+
<input
31+
[id]="firstName.api.name"
32+
[name]="firstName.api.name"
33+
[value]="firstName.api.state.value"
34+
(blur)="firstName.api.handleBlur()"
35+
(input)="firstName.api.handleChange($any($event).target.value)"
36+
/>
37+
@if (firstName.api.state.meta.isTouched) {
38+
@for (error of firstName.api.state.meta.errors; track $index) {
39+
<div style="color: red">
40+
{{ error }}
41+
</div>
42+
}
43+
}
44+
@if (firstName.api.state.meta.isValidating) {
45+
<p>Validating...</p>
46+
}
47+
</ng-container>
48+
</div>
49+
<div>
50+
<ng-container
51+
[tanstackField]="form"
52+
name="lastName"
53+
#lastName="field"
54+
>
55+
<label [for]="lastName.api.name">Last Name:</label>
56+
<input
57+
[id]="lastName.api.name"
58+
[name]="lastName.api.name"
59+
[value]="lastName.api.state.value"
60+
(blur)="lastName.api.handleBlur()"
61+
(input)="lastName.api.handleChange($any($event).target.value)"
62+
/>
63+
@if (lastName.api.state.meta.isTouched) {
64+
@for (error of lastName.api.state.meta.errors; track $index) {
65+
<div style="color: red">
66+
{{ error }}
67+
</div>
68+
}
69+
}
70+
@if (lastName.api.state.meta.isValidating) {
71+
<p>Validating...</p>
72+
}
73+
</ng-container>
74+
</div>
75+
`,
76+
})
77+
export class AppComponent {
78+
form = injectForm({
79+
defaultValues: {
80+
firstName: '',
81+
lastName: '',
82+
},
83+
onSubmit({ value }) {
84+
// Do something with form data
85+
console.log(value)
86+
},
87+
})
88+
}
89+
```
90+
91+
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:
92+
93+
```angular-ts
94+
import {injectField} from '@tanstack/angular-form'
95+
96+
@Component({
97+
selector: 'app-text-field',
98+
standalone: true,
99+
template: `
100+
<label [for]="field.api.name">{{ label() }}</label>
101+
<input
102+
[id]="field.api.name"
103+
[name]="field.api.name"
104+
[value]="field.api.state.value"
105+
(blur)="field.api.handleBlur()"
106+
(input)="field.api.handleChange($any($event).target.value)"
107+
/>
108+
@if (field.api.state.meta.isTouched) {
109+
@for (error of field.api.state.meta.errors; track $index) {
110+
<div style="color: red">
111+
{{ error }}
112+
</div>
113+
}
114+
}
115+
@if (field.api.state.meta.isValidating) {
116+
<p>Validating...</p>
117+
}
118+
`,
119+
})
120+
export class AppTextField {
121+
label = input.required<string>()
122+
// This API requires another part to it from the parent component
123+
field = injectField<string>()
124+
}
125+
```
126+
127+
> `injectField` accepts a single generic to define the `field.state.value` type.
128+
>
129+
> As a result, a numerical text field would be represented as `injectField<number>`, for example.
130+
131+
Now, we can use the `TanStackAppField` directive (`tanstack-app-field`) to `provide` the expected field associated with this input:
132+
133+
```angular-ts
134+
import { Component } from '@angular/core'
135+
import {
136+
TanStackAppField,
137+
TanStackField,
138+
injectForm,
139+
} from '@tanstack/angular-form'
140+
141+
@Component({
142+
selector: 'app-root',
143+
standalone: true,
144+
imports: [TanStackField, TanStackAppField, AppTextField],
145+
template: `
146+
<div>
147+
<app-text-field
148+
label="First name:"
149+
tanstack-app-field
150+
[tanstackField]="form"
151+
name="firstName"
152+
/>
153+
</div>
154+
<div>
155+
<app-text-field
156+
label="Last name:"
157+
tanstack-app-field
158+
[tanstackField]="form"
159+
name="lastName"
160+
/>
161+
</div>
162+
`,
163+
})
164+
export class AppComponent {
165+
form = injectForm({
166+
defaultValues: {
167+
firstName: '',
168+
lastName: '',
169+
},
170+
onSubmit({ value }) {
171+
// Do something with form data
172+
console.log(value)
173+
},
174+
})
175+
}
176+
```
177+
178+
> Here, the `tanstack-app-field` directive is taking the properties from `[tanstackField]` and `provide`ing them down to the `app-text-field` so that they can be more easily consumed as a component.

examples/angular/array/package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,23 @@
1010
"test": "ng test"
1111
},
1212
"dependencies": {
13-
"@angular/animations": "^19.2.14",
14-
"@angular/common": "^19.2.14",
15-
"@angular/compiler": "^19.2.14",
16-
"@angular/core": "^19.2.14",
17-
"@angular/forms": "^19.2.14",
18-
"@angular/platform-browser": "^19.2.14",
19-
"@angular/platform-browser-dynamic": "^19.2.14",
20-
"@angular/router": "^19.2.14",
13+
"@angular/animations": "^20.0.0",
14+
"@angular/common": "^20.0.0",
15+
"@angular/compiler": "^20.0.0",
16+
"@angular/core": "^20.0.0",
17+
"@angular/forms": "^20.0.0",
18+
"@angular/platform-browser": "^20.0.0",
19+
"@angular/platform-browser-dynamic": "^20.0.0",
20+
"@angular/router": "^20.0.0",
2121
"@tanstack/angular-form": "^1.12.4",
2222
"rxjs": "^7.8.2",
2323
"tslib": "^2.8.1",
24-
"zone.js": "^0.15.1"
24+
"zone.js": "0.15.1"
2525
},
2626
"devDependencies": {
27-
"@angular-devkit/build-angular": "^19.2.15",
28-
"@angular/cli": "^19.2.15",
29-
"@angular/compiler-cli": "^19.2.14",
27+
"@angular-devkit/build-angular": "^20.0.0",
28+
"@angular/cli": "^20.0.0",
29+
"@angular/compiler-cli": "^20.0.0",
3030
"typescript": "5.8.2"
3131
}
3232
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Editor configuration, see https://editorconfig.org
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
indent_style = space
7+
indent_size = 2
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
11+
[*.ts]
12+
quote_type = single
13+
14+
[*.md]
15+
max_line_length = off
16+
trim_trailing_whitespace = false
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# See http://help.github.com/ignore-files/ for more about ignoring files.
2+
3+
# Compiled output
4+
/dist
5+
/tmp
6+
/out-tsc
7+
/bazel-out
8+
9+
# Node
10+
/node_modules
11+
npm-debug.log
12+
yarn-error.log
13+
14+
# IDEs and editors
15+
.idea/
16+
.project
17+
.classpath
18+
.c9/
19+
*.launch
20+
.settings/
21+
*.sublime-workspace
22+
23+
# Visual Studio Code
24+
.vscode/*
25+
!.vscode/settings.json
26+
!.vscode/tasks.json
27+
!.vscode/launch.json
28+
!.vscode/extensions.json
29+
.history/*
30+
31+
# Miscellaneous
32+
/.angular/cache
33+
.sass-cache/
34+
/connect.lock
35+
/coverage
36+
/libpeerconnection.log
37+
testem.log
38+
/typings
39+
40+
# System files
41+
.DS_Store
42+
Thumbs.db

examples/angular/large-form/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Simple
2+
3+
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.0.1.
4+
5+
## Development server
6+
7+
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
8+
9+
## Code scaffolding
10+
11+
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12+
13+
## Build
14+
15+
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
16+
17+
## Running unit tests
18+
19+
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20+
21+
## Running end-to-end tests
22+
23+
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
24+
25+
## Further help
26+
27+
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

0 commit comments

Comments
 (0)