Skip to content
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation on updating and resetting form state #144

Merged
merged 1 commit into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/new-frogs-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'docs-app': patch
---

Add documentation on updating and resetting form state
2 changes: 1 addition & 1 deletion docs/usage/async/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Async state
order: 6
order: 7
---

# Managing asynchronous state
Expand Down
6 changes: 6 additions & 0 deletions docs/usage/data/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ By default `@data` is immutable, i.e. the addon will only read from it. For hand
But there are use cases where you would want to mutate the data immediately when the user has changed some field. This is especially the case when the data is already an object that has some "buffering" capabilities, shielding its original source data from premature mutations, as with libraries like [ember-changeset](https://github.com/poteto/ember-changeset) or [ember-buffered-proxy](https://github.com/yapplabs/ember-buffered-proxy).

To do so, pass `@dataMode="mutable"` to the form component!

## Updating data

As we learned above, by default the form data passed as `@data` is immutable, and any changes due to the user entering or selecting new data as part of interacting with the form is stored as a separate copy, which we call "dirty" data here (as it deviates from the original data, but is not "persisted" yet). If you change `@data` (either by reassigning a whole new object or updating tracked properties), the form will automatically reflect those changes, i.e. form fields will update their value.

However, this will _not_ happen for any non-pristine fields, i.e. fields that have new user-entered dirty data, as this would mean irrevocably losing that data. But if that is what you want, you need to explicitly wipe out that dirty data by [resetting the form](./reset). So to keep an existing form in place, but just replace all of its data, you would need to do both reassigning new `@data` and resetting the dirty state!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Total nit: I like the link to resetting here, but I would like a code snippet on either this page or the reset page on how to reset.

Reason being, I think folks scan until they see a code sample and likely copy-paste it.

99 changes: 99 additions & 0 deletions docs/usage/reset/demo/reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Resetting dirty form state

The following kitchen-sink like example shows all three ways to reset form state:

- by clicking the native reset button
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think this can be split into multiple demos.

My concern is that folks with copy-paste this and have all three methods (when perhaps they just need the native reset button).

Could be

  • default native reset
  • reset "link"
  • calling reset and a scenario of why this might be desired.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I currently don't have much time, so I decided to merge this as-is, but created this issue to pick up and track your suggestion: #149

- by clicking the link, using the yielded `reset` action
- by calling `reset` using the register pattern described above when selecting a different user

```hbs template
<div class='flex gap-8'>
<div class='flex-initial'>
Select user:
<ul>
{{#each this.users as |user index|}}
<li><a
class='cursor-pointer'
{{on 'click' (fn this.selectUser index)}}
>{{user.firstName}}
{{user.lastName}}</a></li>
{{/each}}
</ul>
</div>

<HeadlessForm
@data={{this.currentUser}}
@onSubmit={{this.handleSubmit}}
class='flex-1'
as |form|
>
{{(this.assignReset form.reset)}}

<form.Field @name='firstName' as |field|>
<div class='my-2 flex flex-col'>
<field.Label>First name</field.Label>
<field.Input required class='border rounded px-2' />
<field.Errors />
</div>
</form.Field>

<form.Field @name='lastName' as |field|>
<div class='my-2 flex flex-col'>
<field.Label>Last name</field.Label>
<field.Input required class='border rounded px-2' />
<field.Errors />
</div>
</form.Field>

<button type='submit'>Submit</button>
<button type='reset'>Reset</button>

<p>Click on the reset button above or
<a {{on 'click' form.reset}}>here</a>
to reset any dirty state</p>
</HeadlessForm>
</div>
```

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

export default class MyFormComponent extends Component {
users = [
{
firstName: 'Jane',
lastName: 'Doe',
},
{
firstName: 'John',
lastName: 'Smith',
},
];

@tracked
selectedIndex = 0;

resetForm;

@action
assignReset(reset) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: maybe there should be a comment here that this is only necessary if you want to do something with the reset callback?

this.resetForm = reset;
}

get currentUser() {
return this.users[this.selectedIndex];
}

@action
selectUser(index) {
this.selectedIndex = index;
this.resetForm?.();
}

handleSubmit({ firstName, lastName }) {
alert(`Form submitted with: ${firstName} ${lastName}`);
}
}
```
22 changes: 22 additions & 0 deletions docs/usage/reset/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: Resetting state
order: 6
---

# Resetting form state

As explained in the chapter for [Updating data](./data#updating-data), form state consists of the original immutable data passed to `@data` and "dirty" changed state. To keep the form component in place but get rid of the dirty state, you need to explicitly call a `reset` action on the form. There are different ways to do that:

- use "the platform" and make the user click on a `<button type="reset">`
- use the yielded `reset` action

For the latter case, this is easy to do when the place where you want to call `reset` is within the block of `<HeadlessForm as |form|>`. For example you can pass it to the `on` modifier, is in `{{on "click form.reset}}`.

However there is another interesting use case where you might want to reset form state from "outside" the component, for example in a controller action. The problem here is that you do not have access to the yielded `reset` action there.
But you can follow the following pattern to solve that:

- create a function/method that receives the `reset` action (it's just a function) and assign it to the context where you will later be able to access it, like the controller for example
- invoke that function as a helper (in modern Ember, helpers are really just functions) within the form template block where you have access to the scope of the yielded `form` API
- call this registered reset function whenever you need to

Note that besides resetting dirty data, the form will also reset any [validation](../validation) state and errors it might had before!