Skip to content
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

Merged
merged 7 commits into from
Sep 26, 2022
Merged

Conversation

romaricpascal
Copy link
Member

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 the click listener but with this update, it'll be read at component instantiation. This has an effect on the following scenario:

  1. the attribute could be set when the component is instantiated
  2. some other piece of JS on the page may remove it while the user browses
  3. at the time the use click, the attribute would then not be there, calling for a different behaviour than when the component was initialised

On main the Button 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.

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2867 September 20, 2022 15:57 Inactive
- 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)
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2867 September 20, 2022 16:06 Inactive
}

/**
* Initialise component
*/
Button.prototype.init = function () {
if (!this.$module) {
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Contributor

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()).

Copy link
Member Author

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()).

Copy link
Member Author

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
}

Copy link
Member Author

@romaricpascal romaricpascal Sep 22, 2022

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2867 September 22, 2022 15:41 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2867 September 26, 2022 08:27 Inactive
Copy link
Contributor

@36degrees 36degrees left a 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

Comment on lines 58 to 61
* Wraps the button rendered on the page in a form
*
* Examples don't do this and we need it to have something to submit
*/
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// a form and counting sumbissions. It requires some bits of recurring
// a form and counting submissions. It requires some bits of recurring

Comment on lines 79 to 82
* Gets the number of times the form was submitted
*
* @returns {Number}
*/
Copy link
Contributor

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)
Copy link
Contributor

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?

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2867 September 26, 2022 10:08 Inactive
Copy link
Contributor

@36degrees 36degrees left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

@romaricpascal romaricpascal merged commit a28e6ec into main Sep 26, 2022
@romaricpascal romaricpascal deleted the button-config branch September 26, 2022 10:29
@romaricpascal romaricpascal mentioned this pull request Nov 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow button config set via data attributes to be passed when initialising the component in JavaScript
4 participants