fix(form): dispatch submit ev on native form node and add docs
This commit is contained in:
parent
37f975ea48
commit
1a5e353f7f
5 changed files with 116 additions and 16 deletions
5
.changeset/strange-cougars-shout.md
Normal file
5
.changeset/strange-cougars-shout.md
Normal 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.
|
||||||
|
|
@ -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>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue