fix(form): dispatch submit ev on native form node and add docs

This commit is contained in:
Joren Broekema 2021-01-28 12:32:22 +01:00
parent 37f975ea48
commit 1a5e353f7f
5 changed files with 116 additions and 16 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/form': patch
---
Dispatch submit event on native form node instead of calling submit() directly, which circumvents lion-form submit logic and will always do a page reload and cannot be stopped by the user.

View file

@ -7,6 +7,7 @@ For usage and installation please see the appropriate packages.
```js script ```js script
import { html } from '@lion/core'; import { html } from '@lion/core';
import '@lion/button/lion-button.js';
import '@lion/checkbox-group/lion-checkbox-group.js'; import '@lion/checkbox-group/lion-checkbox-group.js';
import '@lion/checkbox-group/lion-checkbox.js'; import '@lion/checkbox-group/lion-checkbox.js';
import '@lion/combobox/lion-combobox.js'; import '@lion/combobox/lion-combobox.js';
@ -31,6 +32,8 @@ import '@lion/textarea/lion-textarea.js';
import { MinLength, Required } from '@lion/form-core'; import { MinLength, Required } from '@lion/form-core';
import { loadDefaultFeedbackMessages } from '@lion/validate-messages'; import { loadDefaultFeedbackMessages } from '@lion/validate-messages';
loadDefaultFeedbackMessages();
export default { export default {
title: 'Forms/Features Overview', title: 'Forms/Features Overview',
}; };
@ -40,7 +43,6 @@ export default {
```js story ```js story
export const main = () => { export const main = () => {
loadDefaultFeedbackMessages();
Required.getMessage = () => 'Please enter a value'; Required.getMessage = () => 'Please enter a value';
return html` return html`
<lion-form> <lion-form>
@ -161,3 +163,67 @@ export const main = () => {
`; `;
}; };
``` ```
## Submitting a form
To submit a form, use a regular button (or `LionButton`) with `type="submit"` (which is default) somewhere inside the native `<form>`.
Then, add a `submit` handler on the `lion-form`.
You can use this event to do your own (pre-)submit logic, like getting the serialized form data and sending it to a backend API.
Another example is checking if the form has errors, and focusing the first field with an error.
To fire a submit from JavaScript, select the `lion-form` element and call `.submit()`.
```js preview-story
export const formSubmit = () => {
const submitHandler = ev => {
if (ev.target.hasFeedbackFor.includes('error')) {
const firstFormElWithError = ev.target.formElements.find(el =>
el.hasFeedbackFor.includes('error'),
);
firstFormElWithError.focus();
return;
}
const formData = ev.target.serializedValue;
fetch('/api/foo/', {
method: 'POST',
body: JSON.stringify(formData),
});
};
const submitViaJS = ev => {
// Call submit on the lion-form element, in your own code you should use
// a selector that's not dependent on DOM structure like this one.
ev.target.previousElementSibling.submit();
};
return html`
<lion-form @submit=${submitHandler}>
<form @submit=${ev => ev.preventDefault()}>
<lion-input
name="first_name"
label="First Name"
.validators="${[new Required()]}"
></lion-input>
<lion-input
name="last_name"
label="Last Name"
.validators="${[new Required()]}"
></lion-input>
<div style="display:flex">
<lion-button raised>Submit</lion-button>
<lion-button
type="button"
raised
@click=${ev => ev.currentTarget.parentElement.parentElement.parentElement.resetGroup()}
>Reset</lion-button
>
</div>
</form>
</lion-form>
<button @click=${submitViaJS}>Explicit submit via JavaScript</button>
`;
};
```

View file

@ -3,6 +3,8 @@
`lion-form` is a webcomponent that enhances the functionality of the native `form` component. `lion-form` is a webcomponent that enhances the functionality of the native `form` component.
It is designed to interact with (instances of) the [form controls](?path=/docs/forms-system-overview--page). It is designed to interact with (instances of) the [form controls](?path=/docs/forms-system-overview--page).
> Note: Make sure to explicitly put `<form>` native element as a first child of `<lion-form>`, in order to function properly.
```js script ```js script
export default { export default {
title: 'Forms/Form/Overview', title: 'Forms/Form/Overview',

View file

@ -15,20 +15,8 @@ const throwFormNodeError = () => {
export class LionForm extends LionFieldset { export class LionForm extends LionFieldset {
constructor() { constructor() {
super(); super();
/** @param {Event} ev */ this._submit = this._submit.bind(this);
this._submit = ev => { this._reset = this._reset.bind(this);
ev.preventDefault();
ev.stopPropagation();
this.submitGroup();
this.dispatchEvent(new Event('submit', { bubbles: true }));
};
/** @param {Event} ev */
this._reset = ev => {
ev.preventDefault();
ev.stopPropagation();
this.resetGroup();
this.dispatchEvent(new Event('reset', { bubbles: true }));
};
} }
connectedCallback() { connectedCallback() {
@ -50,12 +38,24 @@ export class LionForm extends LionFieldset {
submit() { submit() {
if (this._formNode) { if (this._formNode) {
this._formNode.submit(); // Firefox requires cancelable flag, otherwise we cannot preventDefault
// Firefox still runs default handlers for untrusted events :\
this._formNode.dispatchEvent(new Event('submit', { cancelable: true }));
} else { } else {
throwFormNodeError(); throwFormNodeError();
} }
} }
/**
* @param {Event} ev
*/
_submit(ev) {
ev.preventDefault();
ev.stopPropagation();
this.submitGroup();
this.dispatchEvent(new Event('submit', { bubbles: true }));
}
reset() { reset() {
if (this._formNode) { if (this._formNode) {
this._formNode.reset(); this._formNode.reset();
@ -64,6 +64,16 @@ export class LionForm extends LionFieldset {
} }
} }
/**
* @param {Event} ev
*/
_reset(ev) {
ev.preventDefault();
ev.stopPropagation();
this.resetGroup();
this.dispatchEvent(new Event('reset', { bubbles: true }));
}
__registerEventsForLionForm() { __registerEventsForLionForm() {
this._formNode.addEventListener('submit', this._submit); this._formNode.addEventListener('submit', this._submit);
this._formNode.addEventListener('reset', this._reset); this._formNode.addEventListener('reset', this._reset);

View file

@ -138,6 +138,23 @@ describe('<lion-form>', () => {
expect(submitEv.composed).to.be.false; expect(submitEv.composed).to.be.false;
}); });
it('redispatches a submit event on the native form node when calling submit() imperatively', async () => {
const nativeFormSubmitEventSpy = spy();
const el = await fixture(html`
<lion-form>
<form @submit=${nativeFormSubmitEventSpy}>
<button type="submit">submit</button>
</form>
</lion-form>
`);
const submitSpy = spy(el, 'submit');
const submitGroupSpy = spy(el, 'submitGroup');
el.submit();
expect(submitSpy.calledOnce).to.be.true;
expect(nativeFormSubmitEventSpy.calledOnce).to.be.true;
expect(submitGroupSpy.calledOnce).to.be.true;
});
it('handles internal submit handler before dispatch', async () => { it('handles internal submit handler before dispatch', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-form> <lion-form>