Skip to content

Commit 393b269

Browse files
authored
Merge pull request #466 from CrowdStrike/expose-state-from-headless-form
Expose state from headless form
2 parents 8961b83 + b3b4b80 commit 393b269

File tree

11 files changed

+842
-342
lines changed

11 files changed

+842
-342
lines changed

.changeset/serious-dodos-scream.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@crowdstrike/ember-toucan-form': minor
3+
---
4+
5+
Exposes validationState, submissionState, isInvalid and rawErrors from the HeadlessForm component

docs-app/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"ember-fetch": "^8.1.2",
9494
"ember-headless-form": "1.1.0",
9595
"ember-headless-form-changeset": "1.0.0",
96+
"ember-headless-form-yup": "1.0.0",
9697
"ember-load-initializers": "^2.1.2",
9798
"ember-page-title": "^8.0.0-beta.0",
9899
"ember-qunit": "^8.0.0",
@@ -140,7 +141,8 @@
140141
"ember-velcro": "^2.1.0",
141142
"highlight.js": "^11.6.0",
142143
"highlightjs-glimmer": "^2.0.0",
143-
"tracked-built-ins": "^3.1.0"
144+
"tracked-built-ins": "^3.1.0",
145+
"yup": "^1.0.0"
144146
},
145147
"dependenciesMeta": {
146148
"@crowdstrike/ember-toucan-core": {
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Async state
2+
3+
Submit this form with a valid email, and with the same email again, to see how it disables the submit button, changes its label, and shows error messages coming from the "backend":
4+
5+
```hbs template
6+
<ToucanForm @onSubmit={{this.handleSubmit}} as |form|>
7+
<form.Field @name='email' as |field|>
8+
<div class='my-2 flex flex-col'>
9+
<field.Label>Email</field.Label>
10+
<field.Input
11+
@type='email'
12+
placeholder='Please enter your email'
13+
class='border rounded px-2'
14+
/>
15+
</div>
16+
</form.Field>
17+
18+
<button type='submit' disabled={{form.submissionState.isPending}}>
19+
{{if form.submissionState.isPending 'Submitting...' 'Submit'}}
20+
</button>
21+
22+
{{#if form.submissionState.isResolved}}
23+
<p>We got your data! 🎉</p>
24+
{{else if form.submissionState.isRejected}}
25+
<p>⛔️ {{form.submissionState.error}}</p>
26+
{{/if}}
27+
</ToucanForm>
28+
```
29+
30+
```js component
31+
import Component from '@glimmer/component';
32+
import { action } from '@ember/object';
33+
34+
export default class MyFormComponent extends Component {
35+
saved = [];
36+
37+
@action
38+
async handleSubmit({ email }) {
39+
// pretending something async is happening here
40+
await new Promise((r) => setTimeout(r, 3000));
41+
42+
if (!email) {
43+
throw new Error('No email given');
44+
}
45+
46+
if (this.saved.includes(email)) {
47+
// Throwing this error will cause the form to yield form.submissionState.isRejected as true
48+
throw new Error(`${email} is already taken!`);
49+
}
50+
51+
this.saved.push(email);
52+
}
53+
}
54+
```

docs/toucan-form/async/index.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
title: Async state
3+
order: 5
4+
---
5+
6+
# Managing asynchronous state
7+
8+
toucan-form knows about two events that can be asynchronous:
9+
10+
- **validation** will often be synchronous, but you can also use the ember-headless-form [asynchronous validations](https://ember-headless-form.pages.dev/docs/validation/custom-validation#asynchronous-validation) for e.g. validating data on the server
11+
- **submission** is most often asynchronous when e.g. sending a `POST` request with your form data to the server
12+
13+
To make the form aware of the asynchronous submission process, you just need to return a Promise from the submit callback passed to [`@onSubmit`](https://ember-headless-form.pages.dev/docs/usage/data#getting-data-out).
14+
15+
ember-headless-form will then make the async state of both these events available to you in the template. This allows for use cases like
16+
17+
- disabling the submit button while a submission is ongoing
18+
- showing a loading indicator while submission or validation is pending
19+
- rendering the results of the (either successful or failed) submission, after it is resolved/rejected
20+
21+
To enable these, the form component is yielding `validationState` and `submissionState` objects with these properties:
22+
23+
- `isPending`
24+
- `isResolved`
25+
- `isRejected`
26+
- `value` (when resolved)
27+
- `error` (when rejected)
28+
29+
These derived properties are fully reactive and typed, as these are provided by the excellent [ember-async-data](https://github.com/tracked-tools/ember-async-data) addon. Refer to their documentation for additional details!

docs/toucan-form/native-validation/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Native validation
3-
order: 3
3+
order: 4
44
---
55

66
# Native validation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
```hbs template
2+
<div class='mx-auto max-w-md'>
3+
<ToucanForm
4+
class='space-y-4'
5+
@data={{this.data}}
6+
@onSubmit={{this.handleSubmit}}
7+
@validate={{validate-yup this.schema}}
8+
as |form|
9+
>
10+
<form.Input @label='Name' @name='name' />
11+
<form.Input @label='Email' @name='email' />
12+
13+
<Button class='w-full' type='submit'>Submit</Button>
14+
</ToucanForm>
15+
</div>
16+
```
17+
18+
```js component
19+
import Component from '@glimmer/component';
20+
import { object, string } from 'yup';
21+
22+
export default class extends Component {
23+
data = {
24+
name: '',
25+
email: '',
26+
};
27+
28+
schema = object({
29+
name: string().required(),
30+
email: string().required().email(),
31+
});
32+
33+
handleSubmit(data) {
34+
console.log({ data });
35+
36+
alert(
37+
`Form submitted with:\n${Object.entries(data)
38+
.map(([key, value]) => `${key}: ${value}`)
39+
.join('\n')}`
40+
);
41+
}
42+
}
43+
```
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
title: Yup validation
3+
order: 3
4+
---
5+
6+
# Yup validation
7+
8+
This demo shows how to implement [yup](https://github.com/jquense/yup) validation with ember-toucan-form, powered by [ember-headless-form](https://ember-headless-form.pages.dev/docs/validation/yup).
9+
10+
## Install the adapter package
11+
12+
Before using yup validations with Toucan Form, you'll need to install it as a dependency.
13+
14+
```bash
15+
pnpm add yup ember-headless-form-yup
16+
# or
17+
yarn add yup ember-headless-form-yup
18+
# or
19+
npm install yup ember-headless-form-yup
20+
```

packages/ember-toucan-form/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"@typescript-eslint/parser": "^5.30.5",
9090
"autoprefixer": "^10.0.2",
9191
"concurrently": "^8.0.0",
92+
"ember-async-data": "^1.0.3",
9293
"ember-cli-htmlbars": "^6.1.1",
9394
"ember-headless-form": "^1.0.0-beta.3",
9495
"ember-source": "~5.12.0",

packages/ember-toucan-form/src/components/toucan-form.gts

+30
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import TextareaFieldComponent from '../-private/textarea-field';
1414

1515
import type { HeadlessFormBlock, UserData } from '../-private/types';
1616
import type { WithBoundArgs } from '@glint/template';
17+
import type { TrackedAsyncData } from 'ember-async-data';
18+
import type { ErrorRecord } from 'ember-headless-form';
1719
import type { HeadlessFormComponentSignature } from 'ember-headless-form/components/headless-form';
1820

1921
type HeadlessFormArguments<
@@ -52,6 +54,30 @@ export interface ToucanFormComponentSignature<
5254
>;
5355
Textarea: WithBoundArgs<typeof TextareaFieldComponent<DATA>, 'form'>;
5456

57+
/**
58+
* The (async) validation state as `TrackedAsyncData`.
59+
*
60+
* Use derived state like `.isPending` to render the UI conditionally.
61+
*/
62+
validationState?: TrackedAsyncData<ErrorRecord<DATA>>;
63+
64+
/**
65+
* The (async) submission state as `TrackedAsyncData`.
66+
*
67+
* Use derived state like `.isPending` to render the UI conditionally.
68+
*/
69+
submissionState?: TrackedAsyncData<SUBMISSION_VALUE>;
70+
71+
/**
72+
* Will be true if at least one form field is invalid.
73+
*/
74+
isInvalid: boolean;
75+
76+
/**
77+
* An ErrorRecord, for custom rendering of error output
78+
*/
79+
rawErrors?: ErrorRecord<DATA>;
80+
5581
/**
5682
* Yielded action that will trigger form validation and submission, same as when triggering the native `submit` event on the form.
5783
*
@@ -114,6 +140,10 @@ export default class ToucanFormComponent<
114140
Multiselect=(component this.MultiselectFieldComponent form=form)
115141
RadioGroup=(component this.RadioGroupFieldComponent form=form)
116142
Textarea=(component this.TextareaFieldComponent form=form)
143+
validationState=form.validationState
144+
submissionState=form.submissionState
145+
isInvalid=form.isInvalid
146+
rawErrors=form.rawErrors
117147
reset=form.reset
118148
submit=form.submit
119149
)

0 commit comments

Comments
 (0)