Skip to content

Commit

Permalink
Merge pull request #466 from CrowdStrike/expose-state-from-headless-form
Browse files Browse the repository at this point in the history
Expose state from headless form
  • Loading branch information
simonihmig authored Nov 27, 2024
2 parents 8961b83 + b3b4b80 commit 393b269
Show file tree
Hide file tree
Showing 11 changed files with 842 additions and 342 deletions.
5 changes: 5 additions & 0 deletions .changeset/serious-dodos-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@crowdstrike/ember-toucan-form': minor
---

Exposes validationState, submissionState, isInvalid and rawErrors from the HeadlessForm component
4 changes: 3 additions & 1 deletion docs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"ember-fetch": "^8.1.2",
"ember-headless-form": "1.1.0",
"ember-headless-form-changeset": "1.0.0",
"ember-headless-form-yup": "1.0.0",
"ember-load-initializers": "^2.1.2",
"ember-page-title": "^8.0.0-beta.0",
"ember-qunit": "^8.0.0",
Expand Down Expand Up @@ -140,7 +141,8 @@
"ember-velcro": "^2.1.0",
"highlight.js": "^11.6.0",
"highlightjs-glimmer": "^2.0.0",
"tracked-built-ins": "^3.1.0"
"tracked-built-ins": "^3.1.0",
"yup": "^1.0.0"
},
"dependenciesMeta": {
"@crowdstrike/ember-toucan-core": {
Expand Down
54 changes: 54 additions & 0 deletions docs/toucan-form/async/demo/base-demo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Async state

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":

```hbs template
<ToucanForm @onSubmit={{this.handleSubmit}} as |form|>
<form.Field @name='email' as |field|>
<div class='my-2 flex flex-col'>
<field.Label>Email</field.Label>
<field.Input
@type='email'
placeholder='Please enter your email'
class='border rounded px-2'
/>
</div>
</form.Field>
<button type='submit' disabled={{form.submissionState.isPending}}>
{{if form.submissionState.isPending 'Submitting...' 'Submit'}}
</button>
{{#if form.submissionState.isResolved}}
<p>We got your data! 🎉</p>
{{else if form.submissionState.isRejected}}
<p>⛔️ {{form.submissionState.error}}</p>
{{/if}}
</ToucanForm>
```

```js component
import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class MyFormComponent extends Component {
saved = [];

@action
async handleSubmit({ email }) {
// pretending something async is happening here
await new Promise((r) => setTimeout(r, 3000));

if (!email) {
throw new Error('No email given');
}

if (this.saved.includes(email)) {
// Throwing this error will cause the form to yield form.submissionState.isRejected as true
throw new Error(`${email} is already taken!`);
}

this.saved.push(email);
}
}
```
29 changes: 29 additions & 0 deletions docs/toucan-form/async/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: Async state
order: 5
---

# Managing asynchronous state

toucan-form knows about two events that can be asynchronous:

- **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
- **submission** is most often asynchronous when e.g. sending a `POST` request with your form data to the server

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).

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

- disabling the submit button while a submission is ongoing
- showing a loading indicator while submission or validation is pending
- rendering the results of the (either successful or failed) submission, after it is resolved/rejected

To enable these, the form component is yielding `validationState` and `submissionState` objects with these properties:

- `isPending`
- `isResolved`
- `isRejected`
- `value` (when resolved)
- `error` (when rejected)

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!
2 changes: 1 addition & 1 deletion docs/toucan-form/native-validation/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Native validation
order: 3
order: 4
---

# Native validation
Expand Down
43 changes: 43 additions & 0 deletions docs/toucan-form/yup-validation/demo/base-demo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
```hbs template
<div class='mx-auto max-w-md'>
<ToucanForm
class='space-y-4'
@data={{this.data}}
@onSubmit={{this.handleSubmit}}
@validate={{validate-yup this.schema}}
as |form|
>
<form.Input @label='Name' @name='name' />
<form.Input @label='Email' @name='email' />
<Button class='w-full' type='submit'>Submit</Button>
</ToucanForm>
</div>
```

```js component
import Component from '@glimmer/component';
import { object, string } from 'yup';

export default class extends Component {
data = {
name: '',
email: '',
};

schema = object({
name: string().required(),
email: string().required().email(),
});

handleSubmit(data) {
console.log({ data });

alert(
`Form submitted with:\n${Object.entries(data)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')}`
);
}
}
```
20 changes: 20 additions & 0 deletions docs/toucan-form/yup-validation/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Yup validation
order: 3
---

# Yup validation

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).

## Install the adapter package

Before using yup validations with Toucan Form, you'll need to install it as a dependency.

```bash
pnpm add yup ember-headless-form-yup
# or
yarn add yup ember-headless-form-yup
# or
npm install yup ember-headless-form-yup
```
1 change: 1 addition & 0 deletions packages/ember-toucan-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@typescript-eslint/parser": "^5.30.5",
"autoprefixer": "^10.0.2",
"concurrently": "^8.0.0",
"ember-async-data": "^1.0.3",
"ember-cli-htmlbars": "^6.1.1",
"ember-headless-form": "^1.0.0-beta.3",
"ember-source": "~5.12.0",
Expand Down
30 changes: 30 additions & 0 deletions packages/ember-toucan-form/src/components/toucan-form.gts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import TextareaFieldComponent from '../-private/textarea-field';

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

type HeadlessFormArguments<
Expand Down Expand Up @@ -52,6 +54,30 @@ export interface ToucanFormComponentSignature<
>;
Textarea: WithBoundArgs<typeof TextareaFieldComponent<DATA>, 'form'>;

/**
* The (async) validation state as `TrackedAsyncData`.
*
* Use derived state like `.isPending` to render the UI conditionally.
*/
validationState?: TrackedAsyncData<ErrorRecord<DATA>>;

/**
* The (async) submission state as `TrackedAsyncData`.
*
* Use derived state like `.isPending` to render the UI conditionally.
*/
submissionState?: TrackedAsyncData<SUBMISSION_VALUE>;

/**
* Will be true if at least one form field is invalid.
*/
isInvalid: boolean;

/**
* An ErrorRecord, for custom rendering of error output
*/
rawErrors?: ErrorRecord<DATA>;

/**
* Yielded action that will trigger form validation and submission, same as when triggering the native `submit` event on the form.
*
Expand Down Expand Up @@ -114,6 +140,10 @@ export default class ToucanFormComponent<
Multiselect=(component this.MultiselectFieldComponent form=form)
RadioGroup=(component this.RadioGroupFieldComponent form=form)
Textarea=(component this.TextareaFieldComponent form=form)
validationState=form.validationState
submissionState=form.submissionState
isInvalid=form.isInvalid
rawErrors=form.rawErrors
reset=form.reset
submit=form.submit
)
Expand Down
Loading

0 comments on commit 393b269

Please sign in to comment.