From 3fbea5e0371e96926ef7e1fa06f02a893773bde7 Mon Sep 17 00:00:00 2001 From: rmccar <42928680+rmccar@users.noreply.github.com> Date: Tue, 15 Oct 2019 21:00:21 +0100 Subject: [PATCH] Disable submit button after click to stop multiple submissions (#636) * submit button fix * add javascript to disable and style on click * remove log line * added tests * remove .only from test * refactored * changed readme * change readme * revert refactor * remove button loading from feeback * pr comments and changes to examples * removed test functionality covered by another test --- src/components/button/_macro.njk | 4 +- src/components/button/button.dom.js | 14 ++++ src/components/button/button.js | 11 ++++ .../button/examples/button-loader/index.njk | 5 +- .../button/examples/button/index.njk | 3 +- src/components/button/index.njk | 2 +- src/components/feedback/_macro.njk | 2 +- src/components/feedback/feedback.js | 2 - src/js/index.js | 1 + src/tests/spec/feedback/feedback.spec.js | 4 -- .../spec/submit-button/submit-button.spec.js | 66 +++++++++++++++++++ 11 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 src/components/button/button.dom.js create mode 100644 src/components/button/button.js create mode 100644 src/tests/spec/submit-button/submit-button.spec.js diff --git a/src/components/button/_macro.njk b/src/components/button/_macro.njk index 98f245fa77..dc6a7e5b02 100644 --- a/src/components/button/_macro.njk +++ b/src/components/button/_macro.njk @@ -9,11 +9,11 @@ {% else %} type="{{ params.type if params.type else ('button' if params.print else 'submit') }}" {% endif %} - class="btn{% if params.classes %} {{ params.classes }}{% endif %}{% if params.url %} btn--link{% endif %}{% if params.disabled %} btn--disabled{% endif %}{% if params.print %} btn--print u-d-no js-print-btn{% endif %}" + class="btn{% if params.classes %} {{ params.classes }}{% endif %}{% if params.url %} btn--link{% endif %}{% if params.disabled %} btn--disabled{% endif %}{% if params.print %} btn--print u-d-no js-print-btn{% endif %}{% if params.type == 'submit' %} btn--loader js-button{% endif %}" {% if params.id %}id="{{ params.id }}"{% endif %} {% if params.value %}value="{{ params.value }}"{% endif %} {% if params.name %}name="{{ params.name }}"{% endif %} - {% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %} + {% if params.disabled %} disabled{% endif %} {% if params.url and params.newWindow %}target="_blank"{% endif %} {% if params.attributes %}{% for attribute, value in (params.attributes.items() if params.attributes is mapping and params.attributes.items else params.attributes) %}{{attribute}}="{{value}}" {% endfor %}{% endif %} > diff --git a/src/components/button/button.dom.js b/src/components/button/button.dom.js new file mode 100644 index 0000000000..3a4bd803d4 --- /dev/null +++ b/src/components/button/button.dom.js @@ -0,0 +1,14 @@ +import domready from 'js/domready'; + +async function submitButton() { + const buttons = [...document.querySelectorAll('.js-button')]; + + if (buttons.length) { + const SubmitButton = (await import('./button')).default; + buttons.forEach(button => { + new SubmitButton(button); + }); + } +} + +domready(submitButton); diff --git a/src/components/button/button.js b/src/components/button/button.js new file mode 100644 index 0000000000..94b9a23094 --- /dev/null +++ b/src/components/button/button.js @@ -0,0 +1,11 @@ +export default class SubmitButton { + constructor(component) { + this.button = component; + this.button.addEventListener('click', this.loadingButton.bind(this)); + } + + loadingButton() { + this.button.classList.add('is-loading'); + this.button.setAttribute('disabled', true); + } +} diff --git a/src/components/button/examples/button-loader/index.njk b/src/components/button/examples/button-loader/index.njk index 00a93e30ea..fc7d4aa438 100644 --- a/src/components/button/examples/button-loader/index.njk +++ b/src/components/button/examples/button-loader/index.njk @@ -1,8 +1,7 @@ {% from "components/button/_macro.njk" import onsButton %} {{ onsButton({ - "type": 'button', - "text": 'Start', - "classes": 'btn--loader is-loading' + "type": 'submit', + "text": 'Submit' }) }} diff --git a/src/components/button/examples/button/index.njk b/src/components/button/examples/button/index.njk index 593e9acab3..50e2d95f85 100644 --- a/src/components/button/examples/button/index.njk +++ b/src/components/button/examples/button/index.njk @@ -1,7 +1,6 @@ {% from "components/button/_macro.njk" import onsButton %} {{ onsButton({ - "type": 'button', - "text": 'Submit' + "text": 'Save and continue' }) }} diff --git a/src/components/button/index.njk b/src/components/button/index.njk index fa4fd18eeb..9834cff7e0 100644 --- a/src/components/button/index.njk +++ b/src/components/button/index.njk @@ -70,7 +70,7 @@ Use `btn--disabled` with the `disabled=true` element attribute for buttons that }} ### Loader -The `btn--loader` class can be added to a button when there is a need to indicate to the user that the action taken place is waiting for a response i.e. uploading a file. This helps prevent user’s from clicking buttons multiple times when they are unsure if the action they have carried out has worked. +The `btn--loader` class will be added to a button if the type is explicitly declared as `submit` then when the button is clicked the `is-loading` class is added to start the animation and the button is disabled. This is used when there is a need to indicate to the user that the action taken place is waiting for a response i.e. uploading a file. This helps prevent user’s from clicking buttons multiple times when they are unsure if the action they have carried out has worked. {{ patternlibExample({"path": "components/button/examples/button-loader/index.njk"}) }} diff --git a/src/components/feedback/_macro.njk b/src/components/feedback/_macro.njk index d8bf452348..66e0ba4975 100644 --- a/src/components/feedback/_macro.njk +++ b/src/components/feedback/_macro.njk @@ -121,7 +121,7 @@ {{ onsButton({ "text": params.button.send.text, - "classes": "js-feedback-send btn--loader", + "classes": "js-feedback-send", "attributes": params.button.send.attributes }) }} diff --git a/src/components/feedback/feedback.js b/src/components/feedback/feedback.js index 199df13012..817966d06a 100644 --- a/src/components/feedback/feedback.js +++ b/src/components/feedback/feedback.js @@ -77,7 +77,5 @@ export default class Feedback { element.disabled = !enabled; } }); - - this.button.classList[enabled ? 'remove' : 'add']('is-loading'); } } diff --git a/src/js/index.js b/src/js/index.js index aa824a140b..d728ab148a 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -16,3 +16,4 @@ import 'components/checkboxes/checkboxes.dom'; import 'components/radios/radios.dom'; import 'components/typeahead/typeahead.dom'; import 'components/cookies-banner/cookies-banner.dom'; +import 'components/button/button.dom'; diff --git a/src/tests/spec/feedback/feedback.spec.js b/src/tests/spec/feedback/feedback.spec.js index 7b8c9e4dc3..b6ca448f52 100644 --- a/src/tests/spec/feedback/feedback.spec.js +++ b/src/tests/spec/feedback/feedback.spec.js @@ -82,10 +82,6 @@ describe('Component: Feedback', function() { expect(this.submit.disabled).to.be.true; }); - it('the submit button should be set to loader mode', function() { - expect(this.submit.classList.contains('is-loading')).to.be.true; - }); - it('the form should be posted', function() { expect(this.mockedFetch).to.have.been.called(); }); diff --git a/src/tests/spec/submit-button/submit-button.spec.js b/src/tests/spec/submit-button/submit-button.spec.js new file mode 100644 index 0000000000..9597cb161d --- /dev/null +++ b/src/tests/spec/submit-button/submit-button.spec.js @@ -0,0 +1,66 @@ +import { awaitPolyfills } from 'js/polyfills/await-polyfills'; +import template from 'components/button/_test-template.njk'; +import SubmitButton from 'components/button/button'; + +const params = { + id: 'button', + type: 'submit', + text: 'Submit', +}; + +describe('Function: Submit Button ', function() { + let wrapper, buttonElement; + + before(() => awaitPolyfills); + + beforeEach(() => { + const html = template.render({ params }); + + wrapper = document.createElement('div'); + wrapper.innerHTML = html; + document.body.appendChild(wrapper); + + buttonElement = document.getElementById(params.id); + }); + + afterEach(() => { + if (wrapper) { + wrapper.remove(); + } + }); + + describe('Before the button is initialised', () => { + it('Button should have relevant classes', () => { + expect(buttonElement.classList.contains('btn--loader')).to.be.true; + expect(buttonElement.classList.contains('js-button')).to.be.true; + }); + + it('Button should be type submit', () => { + expect(buttonElement.getAttribute('type')).to.equal('submit'); + }); + }); + + describe('Once the button is initialised', () => { + beforeEach(() => { + new SubmitButton(buttonElement); + }); + + it('Button disabled attribute should not be set', () => { + expect(buttonElement.getAttribute('disabled')).to.not.exist; + }); + + describe('and the button is clicked', () => { + beforeEach(() => { + buttonElement.click(); + }); + + it('Button should have loading style applied', () => { + expect(buttonElement.classList.contains('is-loading')).to.be.true; + }); + + it('Button should be disabled', () => { + expect(buttonElement.getAttribute('disabled')).to.equal('true'); + }); + }); + }); +});