diff --git a/CHANGELOG.md b/CHANGELOG.md index e2293f2f1a..893718a88b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,22 @@ This change was made in [pull request #3819: Add linked image focus style](https You must make the following changes when you migrate to this release, or your service might break. +#### Update component initialisation + +Remove `.init()` from individually instantiated components as initialisation now happens automatically: + +```mjs +new Radios($radio).init() +``` + +```mjs +new Radios($radio) +``` + +If you import the JavaScript using `window.GOVUKFrontend.initAll()`, you will not need to make any changes. + +This change was introduced in [pull request #4011: Remove component init() methods and initialise in constructor](https://github.com/alphagov/govuk-frontend/pull/4011). + #### Check that details components work as expected The Details component no longer uses JavaScript, and is no longer polyfilled in older browsers. diff --git a/docs/contributing/coding-standards/js.md b/docs/contributing/coding-standards/js.md index de29e1aef2..d23e75428f 100644 --- a/docs/contributing/coding-standards/js.md +++ b/docs/contributing/coding-standards/js.md @@ -27,18 +27,6 @@ export class Example { this.$module = $module - // Code goes here - } - - /** - * Initialise component - */ - init () { - // Check that required elements are present - if (!this.$module) { - return - } - // Code goes here this.$module.addEventListener('click', () => { // ... @@ -104,14 +92,14 @@ Add methods to the class. ```mjs // Good class Example { - init () { + doSomething () { // Code goes here } } // Bad Example.prototype = { - init: function () { + doSomething: function () { // Code goes here } } diff --git a/docs/examples/webpack/src/javascripts/app.mjs b/docs/examples/webpack/src/javascripts/app.mjs index 307e08fb96..9a45823853 100644 --- a/docs/examples/webpack/src/javascripts/app.mjs +++ b/docs/examples/webpack/src/javascripts/app.mjs @@ -3,5 +3,6 @@ import { Button } from 'govuk-frontend' const $buttons = document.querySelectorAll('[data-module="govuk-button"]') $buttons.forEach(($button) => { - new Button($button).init() + /* eslint-disable-next-line no-new */ + new Button($button) }) diff --git a/packages/govuk-frontend/src/govuk/all.mjs b/packages/govuk-frontend/src/govuk/all.mjs index 0f2b93a370..264db6f4a7 100644 --- a/packages/govuk-frontend/src/govuk/all.mjs +++ b/packages/govuk-frontend/src/govuk/all.mjs @@ -1,3 +1,5 @@ +/* eslint-disable no-new */ + import { version } from './common/govuk-frontend-version.mjs' import { Accordion } from './components/accordion/accordion.mjs' import { Button } from './components/button/button.mjs' @@ -33,60 +35,60 @@ function initAll (config) { const $accordions = $scope.querySelectorAll('[data-module="govuk-accordion"]') $accordions.forEach(($accordion) => { - new Accordion($accordion, config.accordion).init() + new Accordion($accordion, config.accordion) }) const $buttons = $scope.querySelectorAll('[data-module="govuk-button"]') $buttons.forEach(($button) => { - new Button($button, config.button).init() + new Button($button, config.button) }) const $characterCounts = $scope.querySelectorAll('[data-module="govuk-character-count"]') $characterCounts.forEach(($characterCount) => { - new CharacterCount($characterCount, config.characterCount).init() + new CharacterCount($characterCount, config.characterCount) }) const $checkboxes = $scope.querySelectorAll('[data-module="govuk-checkboxes"]') $checkboxes.forEach(($checkbox) => { - new Checkboxes($checkbox).init() + new Checkboxes($checkbox) }) // Find first error summary module to enhance. const $errorSummary = $scope.querySelector('[data-module="govuk-error-summary"]') if ($errorSummary) { - new ErrorSummary($errorSummary, config.errorSummary).init() + new ErrorSummary($errorSummary, config.errorSummary) } const $exitThisPageButtons = $scope.querySelectorAll('[data-module="govuk-exit-this-page"]') $exitThisPageButtons.forEach(($button) => { - new ExitThisPage($button, config.exitThisPage).init() + new ExitThisPage($button, config.exitThisPage) }) // Find first header module to enhance. const $header = $scope.querySelector('[data-module="govuk-header"]') if ($header) { - new Header($header).init() + new Header($header) } const $notificationBanners = $scope.querySelectorAll('[data-module="govuk-notification-banner"]') $notificationBanners.forEach(($notificationBanner) => { - new NotificationBanner($notificationBanner, config.notificationBanner).init() + new NotificationBanner($notificationBanner, config.notificationBanner) }) const $radios = $scope.querySelectorAll('[data-module="govuk-radios"]') $radios.forEach(($radio) => { - new Radios($radio).init() + new Radios($radio) }) // Find first skip link module to enhance. const $skipLink = $scope.querySelector('[data-module="govuk-skip-link"]') if ($skipLink) { - new SkipLink($skipLink).init() + new SkipLink($skipLink) } const $tabs = $scope.querySelectorAll('[data-module="govuk-tabs"]') $tabs.forEach(($tabs) => { - new Tabs($tabs).init() + new Tabs($tabs) }) } diff --git a/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs index 49b52be968..3a50fcaee4 100644 --- a/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs +++ b/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs @@ -134,16 +134,6 @@ export class Accordion { this.$sections = $sections this.browserSupportsSessionStorage = helper.checkForSessionStorage() - } - - /** - * Initialise component - */ - init () { - // Check that required elements are present - if (!this.$module || !this.$sections) { - return - } this.initControls() this.initSectionHeaders() diff --git a/packages/govuk-frontend/src/govuk/components/button/button.mjs b/packages/govuk-frontend/src/govuk/components/button/button.mjs index 99af1fbef9..a2304c9a88 100644 --- a/packages/govuk-frontend/src/govuk/components/button/button.mjs +++ b/packages/govuk-frontend/src/govuk/components/button/button.mjs @@ -40,16 +40,6 @@ export class Button { config || {}, normaliseDataset($module.dataset) ) - } - - /** - * Initialise component - */ - init () { - // Check that required elements are present - if (!this.$module) { - return - } this.$module.addEventListener('keydown', (event) => this.handleKeyDown(event)) this.$module.addEventListener('click', (event) => this.debounce(event)) diff --git a/packages/govuk-frontend/src/govuk/components/button/button.test.js b/packages/govuk-frontend/src/govuk/components/button/button.test.js index f0c507380c..d06737b5f5 100644 --- a/packages/govuk-frontend/src/govuk/components/button/button.test.js +++ b/packages/govuk-frontend/src/govuk/components/button/button.test.js @@ -23,7 +23,8 @@ describe('/components/button', () => { // `undefined` simulates the element being missing, // from an unchecked `document.querySelector` for example - new namespace[exportName](undefined).init() + /* eslint-disable-next-line no-new */ + new namespace[exportName](undefined) // If our component initialisation breaks, this won't run return true diff --git a/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs b/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs index 5df1f0d195..54edb24dc0 100644 --- a/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs +++ b/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs @@ -119,16 +119,6 @@ export class CharacterCount { this.$module = $module this.$textarea = $textarea - } - - /** - * Initialise component - */ - init () { - // Check that required elements are present - if (!this.$module || !this.$textarea) { - return - } const $textareaDescription = document.getElementById(`${this.$textarea.id}-info`) if (!$textareaDescription) { diff --git a/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs index f49cfb671c..cddcb5dc9f 100644 --- a/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs +++ b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs @@ -9,6 +9,17 @@ export class Checkboxes { $inputs /** + * Checkboxes can be associated with a 'conditionally revealed' content block – + * for example, a checkbox for 'Phone' could reveal an additional form field for + * the user to enter their phone number. + * + * These associations are made using a `data-aria-controls` attribute, which is + * promoted to an aria-controls attribute during initialisation. + * + * We also need to restore the state of any conditional reveals on the page (for + * example if the user has navigated back), and set up event handlers to keep + * the reveal in sync with the checkbox state. + * * @param {Element} $module - HTML element to use for checkboxes */ constructor ($module) { @@ -24,27 +35,6 @@ export class Checkboxes { this.$module = $module this.$inputs = $inputs - } - - /** - * Initialise component - * - * Checkboxes can be associated with a 'conditionally revealed' content block – - * for example, a checkbox for 'Phone' could reveal an additional form field for - * the user to enter their phone number. - * - * These associations are made using a `data-aria-controls` attribute, which is - * promoted to an aria-controls attribute during initialisation. - * - * We also need to restore the state of any conditional reveals on the page (for - * example if the user has navigated back), and set up event handlers to keep - * the reveal in sync with the checkbox state. - */ - init () { - // Check that required elements are present - if (!this.$module || !this.$inputs) { - return - } this.$inputs.forEach(($input) => { const targetId = $input.getAttribute('data-aria-controls') diff --git a/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.mjs b/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.mjs index 56459579d3..1d4204ae60 100644 --- a/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.mjs +++ b/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.mjs @@ -40,16 +40,6 @@ export class ErrorSummary { config || {}, normaliseDataset($module.dataset) ) - } - - /** - * Initialise component - */ - init () { - // Check that required elements are present - if (!this.$module) { - return - } this.setFocus() this.$module.addEventListener('click', (event) => this.handleClick(event)) diff --git a/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.test.js b/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.test.js index 9321e1a1e1..6c779bd1d3 100644 --- a/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.test.js +++ b/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.test.js @@ -89,7 +89,8 @@ describe('Error Summary', () => { // `undefined` simulates the element being missing, // from an unchecked `document.querySelector` for example - new namespace[exportName](undefined).init() + /* eslint-disable-next-line no-new */ + new namespace[exportName](undefined) // If our component initialisation breaks, this won't run return true diff --git a/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs b/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs index c53f6f1edc..3f5a2d4c8b 100644 --- a/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs +++ b/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs @@ -98,12 +98,7 @@ export class ExitThisPage { if ($skiplinkButton instanceof HTMLAnchorElement) { this.$skiplinkButton = $skiplinkButton } - } - /** - * Initialise component - */ - init () { this.buildIndicator() this.initUpdateSpan() this.initButtonClickHandler() diff --git a/packages/govuk-frontend/src/govuk/components/globals.test.js b/packages/govuk-frontend/src/govuk/components/globals.test.js index 2267ab63d1..f412f8bf90 100644 --- a/packages/govuk-frontend/src/govuk/components/globals.test.js +++ b/packages/govuk-frontend/src/govuk/components/globals.test.js @@ -45,22 +45,6 @@ describe('GOV.UK Frontend', () => { ]) }) - it('exported Components have an init function', async () => { - const components = exported - .filter(method => !['initAll', 'version'].includes(method)) - - const componentsWithoutInitFunctions = await page.evaluate(async (components) => { - const namespace = await import('govuk-frontend') - - return components.filter(component => { - const prototype = namespace[component].prototype - return typeof prototype.init !== 'function' - }) - }, components) - - expect(componentsWithoutInitFunctions).toEqual([]) - }) - it('can be initialised scoped to certain sections of the page', async () => { await goToExample(page, 'scoped-initialisation') diff --git a/packages/govuk-frontend/src/govuk/components/header/header.mjs b/packages/govuk-frontend/src/govuk/components/header/header.mjs index 803b028c74..44e1e9e319 100644 --- a/packages/govuk-frontend/src/govuk/components/header/header.mjs +++ b/packages/govuk-frontend/src/govuk/components/header/header.mjs @@ -22,9 +22,8 @@ export class Header { /** * A global const for storing a matchMedia instance which we'll use to - * detect when a screen size change happens. We set this later during the - * init function and rely on it being null if the feature isn't available - * to initially apply hidden attributes + * detect when a screen size change happens. We rely on it being null if the + * feature isn't available to initially apply hidden attributes * * @private * @type {MediaQueryList | null} @@ -32,6 +31,9 @@ export class Header { mql = null /** + * Apply a matchMedia for desktop which will trigger a state sync if the browser + * viewport moves between states. + * * @param {Element} $module - HTML element to use for header */ constructor ($module) { @@ -44,20 +46,14 @@ export class Header { this.$menu = this.$menuButton && $module.querySelector( `#${this.$menuButton.getAttribute('aria-controls')}` ) - } - /** - * Initialise component - * - * Check for the presence of the header, menu and menu button – if any are - * missing then there's nothing to do so return early. - * Apply a matchMedia for desktop which will trigger a state sync if the browser - * viewport moves between states. - */ - init () { - // Check that required elements are present - if (!this.$module || !this.$menuButton || !this.$menu) { - return + if ( + !( + this.$menuButton instanceof HTMLElement || + this.$menu instanceof HTMLElement + ) + ) { + return this } // Set the matchMedia to the govuk-frontend desktop breakpoint diff --git a/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.mjs b/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.mjs index 7fdee3ac74..9dcfc52c35 100644 --- a/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.mjs +++ b/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.mjs @@ -30,16 +30,6 @@ export class NotificationBanner { config || {}, normaliseDataset($module.dataset) ) - } - - /** - * Initialise component - */ - init () { - // Check that required elements are present - if (!this.$module) { - return - } this.setFocus() } diff --git a/packages/govuk-frontend/src/govuk/components/radios/radios.mjs b/packages/govuk-frontend/src/govuk/components/radios/radios.mjs index 52576d0364..360374e035 100644 --- a/packages/govuk-frontend/src/govuk/components/radios/radios.mjs +++ b/packages/govuk-frontend/src/govuk/components/radios/radios.mjs @@ -9,6 +9,17 @@ export class Radios { $inputs /** + * Radios can be associated with a 'conditionally revealed' content block – for + * example, a radio for 'Phone' could reveal an additional form field for the + * user to enter their phone number. + * + * These associations are made using a `data-aria-controls` attribute, which is + * promoted to an aria-controls attribute during initialisation. + * + * We also need to restore the state of any conditional reveals on the page (for + * example if the user has navigated back), and set up event handlers to keep + * the reveal in sync with the radio state. + * * @param {Element} $module - HTML element to use for radios */ constructor ($module) { @@ -24,27 +35,6 @@ export class Radios { this.$module = $module this.$inputs = $inputs - } - - /** - * Initialise component - * - * Radios can be associated with a 'conditionally revealed' content block – for - * example, a radio for 'Phone' could reveal an additional form field for the - * user to enter their phone number. - * - * These associations are made using a `data-aria-controls` attribute, which is - * promoted to an aria-controls attribute during initialisation. - * - * We also need to restore the state of any conditional reveals on the page (for - * example if the user has navigated back), and set up event handlers to keep - * the reveal in sync with the radio state. - */ - init () { - // Check that required elements are present - if (!this.$module || !this.$inputs) { - return - } this.$inputs.forEach(($input) => { const targetId = $input.getAttribute('data-aria-controls') diff --git a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs index 0fe255c633..91b549fa46 100644 --- a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs +++ b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs @@ -24,16 +24,6 @@ export class SkipLink { } this.$module = $module - } - - /** - * Initialise component - */ - init () { - // Check that required elements are present - if (!this.$module) { - return - } // Check for linked element const $linkedElement = this.getLinkedElement() diff --git a/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs b/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs index 23e726affe..f25d2e02ba 100644 --- a/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs +++ b/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs @@ -53,16 +53,6 @@ export class Tabs { this.boundTabClick = this.onTabClick.bind(this) this.boundTabKeydown = this.onTabKeydown.bind(this) this.boundOnHashChange = this.onHashChange.bind(this) - } - - /** - * Initialise component - */ - init () { - // Check that required elements are present - if (!this.$module || !this.$tabs) { - return - } this.setupResponsiveChecks() } diff --git a/shared/helpers/puppeteer.js b/shared/helpers/puppeteer.js index f44298bc03..6d8aab17ea 100644 --- a/shared/helpers/puppeteer.js +++ b/shared/helpers/puppeteer.js @@ -67,7 +67,7 @@ async function axe (page, overrides = {}) { * the test boilerplate page, then either: * * - instantiates the component class, passing the provided JavaScript - * configuration, and calls the init function + * configuration * - runs the passed initialiser function inside the browser * (which lets you instantiate it a different way, like using `initAll`, * or run arbitrary code) @@ -100,7 +100,8 @@ async function renderAndInitialise (page, componentName, options) { } const namespace = await import('govuk-frontend') - new namespace[exportName]($module, options.config).init() + /* eslint-disable-next-line no-new */ + new namespace[exportName]($module, options.config) }, componentNameToClassName(componentName), options) return page