-
Notifications
You must be signed in to change notification settings - Fork 323
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
Allow Button to receive configuration via JavaScript #2867
Conversation
- Create helper functions for tracking the clicks - Group tests ahead of adding JavaScript config ones
This'll be necessary to ensure proper access to data-attribute configuration, in case consuming code initialises the component on their own without checking that the element they pass is an actual element (for ex. passing through the result of `document.querySelector` on a page without the targeted element)
3cb274a
to
01d2304
Compare
} | ||
|
||
/** | ||
* Initialise component | ||
*/ | ||
Button.prototype.init = function () { | ||
if (!this.$module) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Asking for clarification:
At the start of the module we're checking for the existence of $module
, and only if it exists does the code to assign $module
to this.$module
run.
Here we are then checking for the existence of this.$module
, however wouldn't the previous steps ensure that this statement would never evaluate to false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately we need that extra check in init
because the first check only shortcuts the constructor, to safeguard the access ot the module's dataset
. The addEventListener
calls happen inside init()
fail if the component was instantiated with an undefined
/null
element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the constructor was called with an undefined
element, wouldn't we want to immediately halt execution anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's anything we can do from the constructor to stop the init
function from being called – that's happening outside of the object (new govukButton($module).init()
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean letting the JS throw an Error
when trying to access $module.dataset
in the constructor?
We had to set a safety for that in the ErrorSummary
to avoid making a breaking change for users that may have a mis-initialised ErrorSummary
component. For ex. with a blanket new ErrorSummary(document.querySelector('[data-module="error-summary"]'))
that runs on a page without an error summary. With the current version, their code wouldn't break, so we wanted to keep that happening after we added the configuration. I'd be quite keen to remove that safety in the next breaking version as it'll be more informative to users to have an error in their console.
For consistency, I've set that same mechanism on the Button, but need the checks in the two places because of the two step initialisation of components (instantiation + init()
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry Ollie, I was commenting on an old version of the page and hadn't seen you'd replied.
I don't think there's anything we can do from the constructor to stop the
init
function from being called – that's happening outside of the object (new govukButton($module).init()
).
We could always make init()
a no-op in the constructor. That would keep everything in the same place. That'd actually make the thing quite portable from component to component.
if (!$module) {
// Make `init` a no-op to avoid potentially breaking code at initialisation
// from lack of HTML element
this.init = function(){}
return this
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll actually update the PR with that approach as:
- it'll avoid the awkward second check that prompted @querkmachine's question
- it'll be easy to pass from one component to another
I'll open a second PR to amend the components we've already made configurable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure about this from a code readability perspective – appreciate at the minute the init function is near the constructor but I think it'd be easy to miss and I don't think I'd expect a class method to be redefined in this way…
If I was writing this I think I'd stick with the two guard clauses we had before, especially if we plan to get rid of the init
function in the short-to-mid term.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to reset to the previous commit, it's not a solution I was particularly attached to and it'll become irrelevant when we remove the init
function. Just thought it'd make sense to keep everything in a single place instead of the double check, especially as it's for handling an "error" scenario.
eb59149
to
01d2304
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good – just a few minor formatting things that need tidying up
* Wraps the button rendered on the page in a form | ||
* | ||
* Examples don't do this and we need it to have something to submit | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indentation
}) | ||
describe('preventing double clicks', () => { | ||
// Click counting is done through using the button to submit | ||
// a form and counting sumbissions. It requires some bits of recurring |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// a form and counting sumbissions. It requires some bits of recurring | |
// a form and counting submissions. It requires some bits of recurring |
* Gets the number of times the form was submitted | ||
* | ||
* @returns {Number} | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indentation
// Don't refresh the page, which will destroy the context to test against. | ||
event.preventDefault() | ||
}) | ||
const $buttonPrime = $button.cloneNode(true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a while to work out what this meant (and I'm still not 100% sure – guessing this is Is prime as in 'a derivative'?)
Wondering if $secondButton
might be simpler and clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏻
The PRs for adding configuration via JavaScript carries on with the Button. 🎉
Changes are similar to the ErrorSummary and NotificationBanner and the commits paint a step-by-step picture of the changes.
The most notable thing is that it slightly changes when the value for whether to prevent double click is read. On
main
the value is read within theclick
listener but with this update, it'll be read at component instantiation. This has an effect on the following scenario:On
main
theButton
will behave according to the value of the attribute at step 3, while after this PR it'll behave according to the value at step 1.Consistency with the other components has been prioritised here after discussing that updating the data-attribute at runtime was not an expected nor supported use case.
Closes #2836.