From 1a5e353f7fec0a0e82d1d9d998aae5b8ce615770 Mon Sep 17 00:00:00 2001 From: Joren Broekema Date: Thu, 28 Jan 2021 12:32:22 +0100 Subject: [PATCH] fix(form): dispatch submit ev on native form node and add docs --- .changeset/strange-cougars-shout.md | 5 ++ .../docs/15-features-overview.md | 68 ++++++++++++++++++- packages/form/README.md | 2 + packages/form/src/LionForm.js | 40 +++++++---- packages/form/test/lion-form.test.js | 17 +++++ 5 files changed, 116 insertions(+), 16 deletions(-) create mode 100644 .changeset/strange-cougars-shout.md diff --git a/.changeset/strange-cougars-shout.md b/.changeset/strange-cougars-shout.md new file mode 100644 index 000000000..e0d69a6c6 --- /dev/null +++ b/.changeset/strange-cougars-shout.md @@ -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. diff --git a/packages/form-integrations/docs/15-features-overview.md b/packages/form-integrations/docs/15-features-overview.md index ce7358d1f..ec3992023 100644 --- a/packages/form-integrations/docs/15-features-overview.md +++ b/packages/form-integrations/docs/15-features-overview.md @@ -7,6 +7,7 @@ For usage and installation please see the appropriate packages. ```js script 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.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 { loadDefaultFeedbackMessages } from '@lion/validate-messages'; +loadDefaultFeedbackMessages(); + export default { title: 'Forms/Features Overview', }; @@ -40,7 +43,6 @@ export default { ```js story export const main = () => { - loadDefaultFeedbackMessages(); Required.getMessage = () => 'Please enter a value'; return html` @@ -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 `
`. + +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` + + ev.preventDefault()}> + + +
+ Submit + ev.currentTarget.parentElement.parentElement.parentElement.resetGroup()} + >Reset +
+ +
+ + `; +}; +``` diff --git a/packages/form/README.md b/packages/form/README.md index 0b716b278..6ca0e41ab 100644 --- a/packages/form/README.md +++ b/packages/form/README.md @@ -3,6 +3,8 @@ `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). +> Note: Make sure to explicitly put `
` native element as a first child of ``, in order to function properly. + ```js script export default { title: 'Forms/Form/Overview', diff --git a/packages/form/src/LionForm.js b/packages/form/src/LionForm.js index 226991999..dc57ae805 100644 --- a/packages/form/src/LionForm.js +++ b/packages/form/src/LionForm.js @@ -15,20 +15,8 @@ const throwFormNodeError = () => { export class LionForm extends LionFieldset { constructor() { super(); - /** @param {Event} ev */ - this._submit = ev => { - 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 })); - }; + this._submit = this._submit.bind(this); + this._reset = this._reset.bind(this); } connectedCallback() { @@ -50,12 +38,24 @@ export class LionForm extends LionFieldset { submit() { 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 { throwFormNodeError(); } } + /** + * @param {Event} ev + */ + _submit(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.submitGroup(); + this.dispatchEvent(new Event('submit', { bubbles: true })); + } + reset() { if (this._formNode) { 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() { this._formNode.addEventListener('submit', this._submit); this._formNode.addEventListener('reset', this._reset); diff --git a/packages/form/test/lion-form.test.js b/packages/form/test/lion-form.test.js index f54d6388b..b6f755c4d 100644 --- a/packages/form/test/lion-form.test.js +++ b/packages/form/test/lion-form.test.js @@ -138,6 +138,23 @@ describe('', () => { 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` + + + + + + `); + 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 () => { const el = await fixture(html`