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

Prevent multiple initialisations of a single component instance #5272

Merged
merged 3 commits into from
Sep 16, 2024

Conversation

patrickpatrickpatrick
Copy link
Contributor

@patrickpatrickpatrick patrickpatrickpatrick commented Aug 28, 2024

What

New method for checking if component has already been initialised based on the value of a data attribute on the module of the component instance.

Based on the previous work of @colinrotherham (#4561) and https://github.com/alphagov/govuk_publishing_components/ approach to initialising multiple different modules on a single DOM element.

Why

Prevent multiple initialisations of a single component on single element.

Closes: #1127

@patrickpatrickpatrick patrickpatrickpatrick changed the title init mult Prevent multiple initialisations of a single component instance Aug 28, 2024
Copy link

github-actions bot commented Aug 28, 2024

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 118.52 KiB
dist/govuk-frontend-development.min.js 44.56 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 93.97 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 88.24 KiB
packages/govuk-frontend/dist/govuk/all.mjs 1.1 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 1.1 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 118.5 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 44.55 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 6.9 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 84.37 KiB 42.33 KiB
accordion.mjs 24.81 KiB 12.9 KiB
button.mjs 7.28 KiB 3.2 KiB
character-count.mjs 23.71 KiB 10.43 KiB
checkboxes.mjs 7.14 KiB 3.34 KiB
error-summary.mjs 9.2 KiB 3.96 KiB
exit-this-page.mjs 18.41 KiB 9.77 KiB
header.mjs 5.77 KiB 3.11 KiB
notification-banner.mjs 7.57 KiB 3.13 KiB
password-input.mjs 16.45 KiB 7.76 KiB
radios.mjs 6.14 KiB 2.89 KiB
service-navigation.mjs 5.76 KiB 3.2 KiB
skip-link.mjs 5.69 KiB 2.69 KiB
tabs.mjs 11.36 KiB 6.57 KiB

View stats and visualisations on the review app


Action run for 49f0185

Copy link

github-actions bot commented Aug 28, 2024

JavaScript changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
index b9eeaf5b6..69008e86f 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -116,9 +116,24 @@ class ElementError extends GOVUKFrontendError {
         super(t), this.name = "ElementError"
     }
 }
+class InitError extends GOVUKFrontendError {
+    constructor(e, t) {
+        let n = `moduleName not defined in component (\`${t}\`)`;
+        "string" == typeof e && (n = `Root element (\`$module\`) already initialised (\`${e}\`)`), super(n), this.name = "InitError"
+    }
+}
 class GOVUKFrontendComponent {
-    constructor() {
-        this.checkSupport()
+    constructor(e) {
+        this.checkSupport(), this.checkInitialised(e);
+        const t = this.constructor.moduleName;
+        if ("string" != typeof t) throw new InitError(t);
+        t && (null == e || e.setAttribute(`data-${t}-init`, ""))
+    }
+    checkInitialised(e) {
+        const t = this.constructor.moduleName;
+        if (e && t && function(e, t) {
+                return e instanceof HTMLElement && e.hasAttribute(`data-${t}-init`)
+            }(e, t)) throw new InitError(t)
     }
     checkSupport() {
         if (!isSupported()) throw new SupportError
@@ -208,7 +223,7 @@ I18n.pluralRulesMap = {
 };
 class Accordion extends GOVUKFrontendComponent {
     constructor(e, t = {}) {
-        if (super(), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.controlsClass = "govuk-accordion__controls", this.showAllClass = "govuk-accordion__show-all", this.showAllTextClass = "govuk-accordion__show-all-text", this.sectionClass = "govuk-accordion__section", this.sectionExpandedClass = "govuk-accordion__section--expanded", this.sectionButtonClass = "govuk-accordion__section-button", this.sectionHeaderClass = "govuk-accordion__section-header", this.sectionHeadingClass = "govuk-accordion__section-heading", this.sectionHeadingDividerClass = "govuk-accordion__section-heading-divider", this.sectionHeadingTextClass = "govuk-accordion__section-heading-text", this.sectionHeadingTextFocusClass = "govuk-accordion__section-heading-text-focus", this.sectionShowHideToggleClass = "govuk-accordion__section-toggle", this.sectionShowHideToggleFocusClass = "govuk-accordion__section-toggle-focus", this.sectionShowHideTextClass = "govuk-accordion__section-toggle-text", this.upChevronIconClass = "govuk-accordion-nav__chevron", this.downChevronIconClass = "govuk-accordion-nav__chevron--down", this.sectionSummaryClass = "govuk-accordion__section-summary", this.sectionSummaryFocusClass = "govuk-accordion__section-summary-focus", this.sectionContentClass = "govuk-accordion__section-content", this.$sections = void 0, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, !(e instanceof HTMLElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.controlsClass = "govuk-accordion__controls", this.showAllClass = "govuk-accordion__show-all", this.showAllTextClass = "govuk-accordion__show-all-text", this.sectionClass = "govuk-accordion__section", this.sectionExpandedClass = "govuk-accordion__section--expanded", this.sectionButtonClass = "govuk-accordion__section-button", this.sectionHeaderClass = "govuk-accordion__section-header", this.sectionHeadingClass = "govuk-accordion__section-heading", this.sectionHeadingDividerClass = "govuk-accordion__section-heading-divider", this.sectionHeadingTextClass = "govuk-accordion__section-heading-text", this.sectionHeadingTextFocusClass = "govuk-accordion__section-heading-text-focus", this.sectionShowHideToggleClass = "govuk-accordion__section-toggle", this.sectionShowHideToggleFocusClass = "govuk-accordion__section-toggle-focus", this.sectionShowHideTextClass = "govuk-accordion__section-toggle-text", this.upChevronIconClass = "govuk-accordion-nav__chevron", this.downChevronIconClass = "govuk-accordion-nav__chevron--down", this.sectionSummaryClass = "govuk-accordion__section-summary", this.sectionSummaryFocusClass = "govuk-accordion__section-summary-focus", this.sectionContentClass = "govuk-accordion__section-content", this.$sections = void 0, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, !(e instanceof HTMLElement)) throw new ElementError({
             componentName: "Accordion",
             element: e,
             identifier: "Root element (`$module`)"
@@ -259,9 +274,9 @@ class Accordion extends GOVUKFrontendComponent {
         l.classList.add(this.sectionShowHideToggleClass), l.setAttribute("data-nosnippet", "");
         const c = document.createElement("span");
         c.classList.add(this.sectionShowHideToggleFocusClass), l.appendChild(c);
-        const h = document.createElement("span"),
-            u = document.createElement("span");
-        if (u.classList.add(this.upChevronIconClass), c.appendChild(u), h.classList.add(this.sectionShowHideTextClass), c.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), s) {
+        const u = document.createElement("span"),
+            h = document.createElement("span");
+        if (h.classList.add(this.upChevronIconClass), c.appendChild(h), u.classList.add(this.sectionShowHideTextClass), c.appendChild(u), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), s) {
             const e = document.createElement("span"),
                 t = document.createElement("span");
             t.classList.add(this.sectionSummaryFocusClass), e.appendChild(t);
@@ -303,8 +318,8 @@ class Accordion extends GOVUKFrontendComponent {
         l && a.push(`${l.textContent}`.trim());
         const c = t.querySelector(`.${this.sectionSummaryClass}`);
         c && a.push(`${c.textContent}`.trim());
-        const h = e ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
-        a.push(h), s.setAttribute("aria-label", a.join(" , ")), e ? (o.removeAttribute("hidden"), t.classList.add(this.sectionExpandedClass), n.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), t.classList.remove(this.sectionExpandedClass), n.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
+        const u = e ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
+        a.push(u), s.setAttribute("aria-label", a.join(" , ")), e ? (o.removeAttribute("hidden"), t.classList.add(this.sectionExpandedClass), n.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), t.classList.remove(this.sectionExpandedClass), n.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
     }
     isExpanded(e) {
         return e.classList.contains(this.sectionExpandedClass)
@@ -361,7 +376,7 @@ Accordion.moduleName = "govuk-accordion", Accordion.defaults = Object.freeze({
 });
 class Button extends GOVUKFrontendComponent {
     constructor(e, t = {}) {
-        if (super(), this.$module = void 0, this.config = void 0, this.debounceFormSubmitTimer = null, !(e instanceof HTMLElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, this.config = void 0, this.debounceFormSubmitTimer = null, !(e instanceof HTMLElement)) throw new ElementError({
             componentName: "Button",
             element: e,
             identifier: "Root element (`$module`)"
@@ -395,7 +410,7 @@ Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
 class CharacterCount extends GOVUKFrontendComponent {
     constructor(e, t = {}) {
         var n, i;
-        if (super(), this.$module = void 0, this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.config = void 0, this.i18n = void 0, this.maxLength = void 0, !(e instanceof HTMLElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.config = void 0, this.i18n = void 0, this.maxLength = void 0, !(e instanceof HTMLElement)) throw new ElementError({
             componentName: "Character count",
             element: e,
             identifier: "Root element (`$module`)"
@@ -442,10 +457,10 @@ class CharacterCount extends GOVUKFrontendComponent {
         `${c.textContent}`.match(/^\s*$/) && (c.textContent = this.i18n.t("textareaDescription", {
             count: this.maxLength
         })), this.$textarea.insertAdjacentElement("afterend", c);
-        const h = document.createElement("div");
-        h.className = "govuk-character-count__sr-status govuk-visually-hidden", h.setAttribute("aria-live", "polite"), this.$screenReaderCountMessage = h, c.insertAdjacentElement("afterend", h);
         const u = document.createElement("div");
-        u.className = c.className, u.classList.add("govuk-character-count__status"), u.setAttribute("aria-hidden", "true"), this.$visibleCountMessage = u, c.insertAdjacentElement("afterend", u), c.classList.add("govuk-visually-hidden"), this.$textarea.removeAttribute("maxlength"), this.bindChangeEvents(), window.addEventListener("pageshow", (() => this.updateCountMessage())), this.updateCountMessage()
+        u.className = "govuk-character-count__sr-status govuk-visually-hidden", u.setAttribute("aria-live", "polite"), this.$screenReaderCountMessage = u, c.insertAdjacentElement("afterend", u);
+        const h = document.createElement("div");
+        h.className = c.className, h.classList.add("govuk-character-count__status"), h.setAttribute("aria-hidden", "true"), this.$visibleCountMessage = h, c.insertAdjacentElement("afterend", h), c.classList.add("govuk-visually-hidden"), this.$textarea.removeAttribute("maxlength"), this.bindChangeEvents(), window.addEventListener("pageshow", (() => this.updateCountMessage())), this.updateCountMessage()
     }
     bindChangeEvents() {
         this.$textarea.addEventListener("keyup", (() => this.handleKeyUp())), this.$textarea.addEventListener("focus", (() => this.handleFocus())), this.$textarea.addEventListener("blur", (() => this.handleBlur()))
@@ -549,7 +564,7 @@ CharacterCount.moduleName = "govuk-character-count", CharacterCount.defaults = O
 });
 class Checkboxes extends GOVUKFrontendComponent {
     constructor(e) {
-        if (super(), this.$module = void 0, this.$inputs = void 0, !(e instanceof HTMLElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, this.$inputs = void 0, !(e instanceof HTMLElement)) throw new ElementError({
             componentName: "Checkboxes",
             element: e,
             identifier: "Root element (`$module`)"
@@ -602,7 +617,7 @@ class Checkboxes extends GOVUKFrontendComponent {
 Checkboxes.moduleName = "govuk-checkboxes";
 class ErrorSummary extends GOVUKFrontendComponent {
     constructor(e, t = {}) {
-        if (super(), this.$module = void 0, this.config = void 0, !(e instanceof HTMLElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, this.config = void 0, !(e instanceof HTMLElement)) throw new ElementError({
             componentName: "Error summary",
             element: e,
             identifier: "Root element (`$module`)"
@@ -653,7 +668,7 @@ ErrorSummary.moduleName = "govuk-error-summary", ErrorSummary.defaults = Object.
 });
 class ExitThisPage extends GOVUKFrontendComponent {
     constructor(e, t = {}) {
-        if (super(), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.$button = void 0, this.$skiplinkButton = null, this.$updateSpan = null, this.$indicatorContainer = null, this.$overlay = null, this.keypressCounter = 0, this.lastKeyWasModified = !1, this.timeoutTime = 5e3, this.keypressTimeoutId = null, this.timeoutMessageId = null, !(e instanceof HTMLElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.$button = void 0, this.$skiplinkButton = null, this.$updateSpan = null, this.$indicatorContainer = null, this.$overlay = null, this.keypressCounter = 0, this.lastKeyWasModified = !1, this.timeoutTime = 5e3, this.keypressTimeoutId = null, this.timeoutMessageId = null, !(e instanceof HTMLElement)) throw new ElementError({
             componentName: "Exit this page",
             element: e,
             identifier: "Root element (`$module`)"
@@ -730,7 +745,7 @@ ExitThisPage.moduleName = "govuk-exit-this-page", ExitThisPage.defaults = Object
 });
 class Header extends GOVUKFrontendComponent {
     constructor(e) {
-        if (super(), this.$module = void 0, this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null, !e) throw new ElementError({
+        if (super(e), this.$module = void 0, this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null, !e) throw new ElementError({
             componentName: "Header",
             element: e,
             identifier: "Root element (`$module`)"
@@ -769,7 +784,7 @@ class Header extends GOVUKFrontendComponent {
 Header.moduleName = "govuk-header";
 class NotificationBanner extends GOVUKFrontendComponent {
     constructor(e, t = {}) {
-        if (super(), this.$module = void 0, this.config = void 0, !(e instanceof HTMLElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, this.config = void 0, !(e instanceof HTMLElement)) throw new ElementError({
             componentName: "Notification banner",
             element: e,
             identifier: "Root element (`$module`)"
@@ -853,7 +868,7 @@ PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Obje
 });
 class Radios extends GOVUKFrontendComponent {
     constructor(e) {
-        if (super(), this.$module = void 0, this.$inputs = void 0, !(e instanceof HTMLElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, this.$inputs = void 0, !(e instanceof HTMLElement)) throw new ElementError({
             componentName: "Radios",
             element: e,
             identifier: "Root element (`$module`)"
@@ -941,7 +956,7 @@ ServiceNavigation.moduleName = "govuk-service-navigation";
 class SkipLink extends GOVUKFrontendComponent {
     constructor(e) {
         var t;
-        if (super(), this.$module = void 0, !(e instanceof HTMLAnchorElement)) throw new ElementError({
+        if (super(e), this.$module = void 0, !(e instanceof HTMLAnchorElement)) throw new ElementError({
             componentName: "Skip link",
             element: e,
             expectedType: "HTMLAnchorElement",
@@ -978,7 +993,7 @@ class SkipLink extends GOVUKFrontendComponent {
 SkipLink.moduleName = "govuk-skip-link";
 class Tabs extends GOVUKFrontendComponent {
     constructor(e) {
-        if (super(), this.$module = void 0, this.$tabs = void 0, this.$tabList = void 0, this.$tabListItems = void 0, this.jsHiddenClass = "govuk-tabs__panel--hidden", this.changingHash = !1, this.boundTabClick = void 0, this.boundTabKeydown = void 0, this.boundOnHashChange = void 0, this.mql = null, !e) throw new ElementError({
+        if (super(e), this.$module = void 0, this.$tabs = void 0, this.$tabList = void 0, this.$tabListItems = void 0, this.jsHiddenClass = "govuk-tabs__panel--hidden", this.changingHash = !1, this.boundTabClick = void 0, this.boundTabKeydown = void 0, this.boundOnHashChange = void 0, this.mql = null, !e) throw new ElementError({
             componentName: "Tabs",
             element: e,
             identifier: "Root element (`$module`)"

Action run for 49f0185

Copy link

github-actions bot commented Aug 28, 2024

Other changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.js b/packages/govuk-frontend/dist/govuk/all.bundle.js
index f410a7790..f4c1a5628 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -114,6 +114,9 @@
     (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
     $element.focus();
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -234,10 +237,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -246,6 +272,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
@@ -459,7 +494,7 @@
      * @param {AccordionConfig} [config] - Accordion config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       this.i18n = void 0;
@@ -802,7 +837,7 @@
      * @param {ButtonConfig} [config] - Button config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       this.debounceFormSubmitTimer = null;
@@ -889,7 +924,7 @@
      */
     constructor($module, config = {}) {
       var _ref, _this$config$maxwords;
-      super();
+      super($module);
       this.$module = void 0;
       this.$textarea = void 0;
       this.$visibleCountMessage = void 0;
@@ -1186,7 +1221,7 @@
      * @param {Element | null} $module - HTML element to use for checkboxes
      */
     constructor($module) {
-      super();
+      super($module);
       this.$module = void 0;
       this.$inputs = void 0;
       if (!($module instanceof HTMLElement)) {
@@ -1294,7 +1329,7 @@
      * @param {ErrorSummaryConfig} [config] - Error summary config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       if (!($module instanceof HTMLElement)) {
@@ -1397,7 +1432,7 @@
      * @param {ExitThisPageConfig} [config] - Exit This Page config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       this.i18n = void 0;
@@ -1626,7 +1661,7 @@
      * @param {Element | null} $module - HTML element to use for header
      */
     constructor($module) {
-      super();
+      super($module);
       this.$module = void 0;
       this.$menuButton = void 0;
       this.$menu = void 0;
@@ -1715,7 +1750,7 @@
      * @param {NotificationBannerConfig} [config] - Notification banner config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       if (!($module instanceof HTMLElement)) {
@@ -1933,7 +1968,7 @@
      * @param {Element | null} $module - HTML element to use for radios
      */
     constructor($module) {
-      super();
+      super($module);
       this.$module = void 0;
       this.$inputs = void 0;
       if (!($module instanceof HTMLElement)) {
@@ -2106,7 +2141,7 @@
      */
     constructor($module) {
       var _this$$module$getAttr;
-      super();
+      super($module);
       this.$module = void 0;
       if (!($module instanceof HTMLAnchorElement)) {
         throw new ElementError({
@@ -2162,7 +2197,7 @@
      * @param {Element | null} $module - HTML element to use for tabs
      */
     constructor($module) {
-      super();
+      super($module);
       this.$module = void 0;
       this.$tabs = void 0;
       this.$tabList = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index efa49cc02..8e29d1ad1 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -108,6 +108,9 @@ function setFocus($element, options = {}) {
   (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
   $element.focus();
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -228,10 +231,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -240,6 +266,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
@@ -453,7 +488,7 @@ class Accordion extends GOVUKFrontendComponent {
    * @param {AccordionConfig} [config] - Accordion config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.i18n = void 0;
@@ -796,7 +831,7 @@ class Button extends GOVUKFrontendComponent {
    * @param {ButtonConfig} [config] - Button config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.debounceFormSubmitTimer = null;
@@ -883,7 +918,7 @@ class CharacterCount extends GOVUKFrontendComponent {
    */
   constructor($module, config = {}) {
     var _ref, _this$config$maxwords;
-    super();
+    super($module);
     this.$module = void 0;
     this.$textarea = void 0;
     this.$visibleCountMessage = void 0;
@@ -1180,7 +1215,7 @@ class Checkboxes extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for checkboxes
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$inputs = void 0;
     if (!($module instanceof HTMLElement)) {
@@ -1288,7 +1323,7 @@ class ErrorSummary extends GOVUKFrontendComponent {
    * @param {ErrorSummaryConfig} [config] - Error summary config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     if (!($module instanceof HTMLElement)) {
@@ -1391,7 +1426,7 @@ class ExitThisPage extends GOVUKFrontendComponent {
    * @param {ExitThisPageConfig} [config] - Exit This Page config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.i18n = void 0;
@@ -1620,7 +1655,7 @@ class Header extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for header
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$menuButton = void 0;
     this.$menu = void 0;
@@ -1709,7 +1744,7 @@ class NotificationBanner extends GOVUKFrontendComponent {
    * @param {NotificationBannerConfig} [config] - Notification banner config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     if (!($module instanceof HTMLElement)) {
@@ -1927,7 +1962,7 @@ class Radios extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for radios
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$inputs = void 0;
     if (!($module instanceof HTMLElement)) {
@@ -2100,7 +2135,7 @@ class SkipLink extends GOVUKFrontendComponent {
    */
   constructor($module) {
     var _this$$module$getAttr;
-    super();
+    super($module);
     this.$module = void 0;
     if (!($module instanceof HTMLAnchorElement)) {
       throw new ElementError({
@@ -2156,7 +2191,7 @@ class Tabs extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for tabs
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$tabs = void 0;
     this.$tabList = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/common/index.mjs b/packages/govuk-frontend/dist/govuk/common/index.mjs
index f1259e9dd..e3aafbbb9 100644
--- a/packages/govuk-frontend/dist/govuk/common/index.mjs
+++ b/packages/govuk-frontend/dist/govuk/common/index.mjs
@@ -79,6 +79,9 @@ function setFocus($element, options = {}) {
   (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
   $element.focus();
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -145,5 +148,5 @@ function isObject(option) {
  * @property {string} errorMessage - Error message when required config fields not provided
  */
 
-export { extractConfigByNamespace, getBreakpoint, getFragmentFromUrl, isSupported, mergeConfigs, setFocus, validateConfig };
+export { extractConfigByNamespace, getBreakpoint, getFragmentFromUrl, isInitialised, isSupported, mergeConfigs, setFocus, validateConfig };
 //# sourceMappingURL=index.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
index d9a9f6fb4..1ea530bf9 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
@@ -74,6 +74,9 @@
     }
     return newObject[namespace];
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -168,10 +171,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -180,6 +206,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
@@ -393,7 +428,7 @@
      * @param {AccordionConfig} [config] - Accordion config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       this.i18n = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
index 426470026..941d494c1 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
@@ -68,6 +68,9 @@ function extractConfigByNamespace(Component, dataset, namespace) {
   }
   return newObject[namespace];
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -162,10 +165,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -174,6 +200,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
@@ -387,7 +422,7 @@ class Accordion extends GOVUKFrontendComponent {
    * @param {AccordionConfig} [config] - Accordion config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.i18n = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
index 990abb949..31a0ee243 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
@@ -24,7 +24,7 @@ class Accordion extends GOVUKFrontendComponent {
    * @param {AccordionConfig} [config] - Accordion config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.i18n = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
index 4fa64a9c3..7c75d4d98 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
@@ -74,6 +74,9 @@
     }
     return newObject[namespace];
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -168,10 +171,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -180,6 +206,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
 
   /**
@@ -193,7 +228,7 @@
      * @param {ButtonConfig} [config] - Button config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       this.debounceFormSubmitTimer = null;
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
index 767533419..fc488920a 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
@@ -68,6 +68,9 @@ function extractConfigByNamespace(Component, dataset, namespace) {
   }
   return newObject[namespace];
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -162,10 +165,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -174,6 +200,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
 
 /**
@@ -187,7 +222,7 @@ class Button extends GOVUKFrontendComponent {
    * @param {ButtonConfig} [config] - Button config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.debounceFormSubmitTimer = null;
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.mjs b/packages/govuk-frontend/dist/govuk/components/button/button.mjs
index db2683005..270e90a7a 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.mjs
@@ -16,7 +16,7 @@ class Button extends GOVUKFrontendComponent {
    * @param {ButtonConfig} [config] - Button config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.debounceFormSubmitTimer = null;
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
index 3a0f99791..92e9f8559 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
@@ -79,6 +79,9 @@
     }
     return newObject[namespace];
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -199,10 +202,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -211,6 +237,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
@@ -423,7 +458,7 @@
      */
     constructor($module, config = {}) {
       var _ref, _this$config$maxwords;
-      super();
+      super($module);
       this.$module = void 0;
       this.$textarea = void 0;
       this.$visibleCountMessage = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
index 411789bab..bf3f2865c 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
@@ -73,6 +73,9 @@ function extractConfigByNamespace(Component, dataset, namespace) {
   }
   return newObject[namespace];
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -193,10 +196,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -205,6 +231,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
@@ -417,7 +452,7 @@ class CharacterCount extends GOVUKFrontendComponent {
    */
   constructor($module, config = {}) {
     var _ref, _this$config$maxwords;
-    super();
+    super($module);
     this.$module = void 0;
     this.$textarea = void 0;
     this.$visibleCountMessage = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
index 955e4ef16..7ab241064 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
@@ -24,7 +24,7 @@ class CharacterCount extends GOVUKFrontendComponent {
    */
   constructor($module, config = {}) {
     var _ref, _this$config$maxwords;
-    super();
+    super($module);
     this.$module = void 0;
     this.$textarea = void 0;
     this.$visibleCountMessage = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
index 9750c9804..459b25eda 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
@@ -39,6 +39,20 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
+
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -80,8 +94,21 @@
    */
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -90,6 +117,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   /**
    * Checkboxes component
    *
@@ -111,7 +147,7 @@
      * @param {Element | null} $module - HTML element to use for checkboxes
      */
     constructor($module) {
-      super();
+      super($module);
       this.$module = void 0;
       this.$inputs = void 0;
       if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
index e6bc6213e..49bdeba72 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
@@ -33,6 +33,20 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
+
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -74,8 +88,21 @@ function isSupported($scope = document.body) {
  */
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -84,6 +111,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 /**
  * Checkboxes component
  *
@@ -105,7 +141,7 @@ class Checkboxes extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for checkboxes
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$inputs = void 0;
     if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs
index f9d7d8fbf..5f34b5a12 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs
@@ -22,7 +22,7 @@ class Checkboxes extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for checkboxes
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$inputs = void 0;
     if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
index bf90c6db5..11dde3fc3 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
@@ -104,6 +104,9 @@
     (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
     $element.focus();
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -198,10 +201,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -210,6 +236,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   /**
    * Error summary component
    *
@@ -224,7 +259,7 @@
      * @param {ErrorSummaryConfig} [config] - Error summary config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
index cb21f83ca..32d877a4b 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
@@ -98,6 +98,9 @@ function setFocus($element, options = {}) {
   (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
   $element.focus();
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -192,10 +195,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -204,6 +230,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 /**
  * Error summary component
  *
@@ -218,7 +253,7 @@ class ErrorSummary extends GOVUKFrontendComponent {
    * @param {ErrorSummaryConfig} [config] - Error summary config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
index 1fd0f166e..7be510b70 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
@@ -17,7 +17,7 @@ class ErrorSummary extends GOVUKFrontendComponent {
    * @param {ErrorSummaryConfig} [config] - Error summary config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
index 215fa91f6..db5ac14ff 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
@@ -74,6 +74,9 @@
     }
     return newObject[namespace];
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -168,10 +171,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -180,6 +206,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
@@ -384,7 +419,7 @@
      * @param {ExitThisPageConfig} [config] - Exit This Page config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       this.i18n = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
index 8b9ae0812..bca3e20dd 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
@@ -68,6 +68,9 @@ function extractConfigByNamespace(Component, dataset, namespace) {
   }
   return newObject[namespace];
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -162,10 +165,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -174,6 +200,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
@@ -378,7 +413,7 @@ class ExitThisPage extends GOVUKFrontendComponent {
    * @param {ExitThisPageConfig} [config] - Exit This Page config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.i18n = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
index b4cd38985..7ad1c9360 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
@@ -15,7 +15,7 @@ class ExitThisPage extends GOVUKFrontendComponent {
    * @param {ExitThisPageConfig} [config] - Exit This Page config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     this.i18n = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
index d1d56b9bc..2977098ff 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
@@ -12,6 +12,9 @@
       value: value || undefined
     };
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -87,10 +90,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -99,6 +125,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   /**
    * Header component
    *
@@ -112,7 +147,7 @@
      * @param {Element | null} $module - HTML element to use for header
      */
     constructor($module) {
-      super();
+      super($module);
       this.$module = void 0;
       this.$menuButton = void 0;
       this.$menu = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
index f850fc1f5..81aa86e4b 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
@@ -6,6 +6,9 @@ function getBreakpoint(name) {
     value: value || undefined
   };
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -81,10 +84,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -93,6 +119,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 /**
  * Header component
  *
@@ -106,7 +141,7 @@ class Header extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for header
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$menuButton = void 0;
     this.$menu = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.mjs b/packages/govuk-frontend/dist/govuk/components/header/header.mjs
index cd6832271..a464c6b1f 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.mjs
@@ -15,7 +15,7 @@ class Header extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for header
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$menuButton = void 0;
     this.$menu = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
index e100700c3..9fe1ed201 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
@@ -98,6 +98,9 @@
     (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
     $element.focus();
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -192,10 +195,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -204,6 +230,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   /**
    * Notification Banner component
    *
@@ -215,7 +250,7 @@
      * @param {NotificationBannerConfig} [config] - Notification banner config
      */
     constructor($module, config = {}) {
-      super();
+      super($module);
       this.$module = void 0;
       this.config = void 0;
       if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
index 2016e5f6f..816f0c522 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
@@ -92,6 +92,9 @@ function setFocus($element, options = {}) {
   (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
   $element.focus();
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -186,10 +189,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -198,6 +224,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 /**
  * Notification Banner component
  *
@@ -209,7 +244,7 @@ class NotificationBanner extends GOVUKFrontendComponent {
    * @param {NotificationBannerConfig} [config] - Notification banner config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
index 1ef1621bc..77e2b61d6 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
@@ -14,7 +14,7 @@ class NotificationBanner extends GOVUKFrontendComponent {
    * @param {NotificationBannerConfig} [config] - Notification banner config
    */
   constructor($module, config = {}) {
-    super();
+    super($module);
     this.$module = void 0;
     this.config = void 0;
     if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js
index a72f6fe34..ad6b0bcae 100644
--- a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js
@@ -79,6 +79,9 @@
     }
     return newObject[namespace];
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -173,10 +176,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -185,6 +211,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs
index a05806da4..75dd1d46e 100644
--- a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs
@@ -73,6 +73,9 @@ function extractConfigByNamespace(Component, dataset, namespace) {
   }
   return newObject[namespace];
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -167,10 +170,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -179,6 +205,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
index 493e0e50c..eb9602c40 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
@@ -39,6 +39,20 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
+
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -80,8 +94,21 @@
    */
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -90,6 +117,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   /**
    * Radios component
    *
@@ -111,7 +147,7 @@
      * @param {Element | null} $module - HTML element to use for radios
      */
     constructor($module) {
-      super();
+      super($module);
       this.$module = void 0;
       this.$inputs = void 0;
       if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
index f2f80a00f..61a06be92 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
@@ -33,6 +33,20 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
+
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -74,8 +88,21 @@ function isSupported($scope = document.body) {
  */
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -84,6 +111,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 /**
  * Radios component
  *
@@ -105,7 +141,7 @@ class Radios extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for radios
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$inputs = void 0;
     if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/radios.mjs b/packages/govuk-frontend/dist/govuk/components/radios/radios.mjs
index ed29c7b0a..f74cefb64 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/radios.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/radios/radios.mjs
@@ -22,7 +22,7 @@ class Radios extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for radios
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$inputs = void 0;
     if (!($module instanceof HTMLElement)) {
diff --git a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js
index 0124f7b37..18831fdb6 100644
--- a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js
@@ -12,6 +12,9 @@
       value: value || undefined
     };
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -87,10 +90,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -99,6 +125,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   /**
    * Service Navigation component
    *
diff --git a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs
index 9a0300ddb..99a6ff9f7 100644
--- a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs
@@ -6,6 +6,9 @@ function getBreakpoint(name) {
     value: value || undefined
   };
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -81,10 +84,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -93,6 +119,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 /**
  * Service Navigation component
  *
diff --git a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
index 5e5be27e9..9103ddf5f 100644
--- a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
@@ -34,6 +34,9 @@
     (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
     $element.focus();
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -109,10 +112,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -121,6 +147,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   /**
    * Skip link component
    *
@@ -135,7 +170,7 @@
      */
     constructor($module) {
       var _this$$module$getAttr;
-      super();
+      super($module);
       this.$module = void 0;
       if (!($module instanceof HTMLAnchorElement)) {
         throw new ElementError({
diff --git a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
index 13441668c..49a20e142 100644
--- a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
@@ -28,6 +28,9 @@ function setFocus($element, options = {}) {
   (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
   $element.focus();
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -103,10 +106,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -115,6 +141,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 /**
  * Skip link component
  *
@@ -129,7 +164,7 @@ class SkipLink extends GOVUKFrontendComponent {
    */
   constructor($module) {
     var _this$$module$getAttr;
-    super();
+    super($module);
     this.$module = void 0;
     if (!($module instanceof HTMLAnchorElement)) {
       throw new ElementError({
diff --git a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs
index 03fb0498c..90e6fc436 100644
--- a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs
@@ -16,7 +16,7 @@ class SkipLink extends GOVUKFrontendComponent {
    */
   constructor($module) {
     var _this$$module$getAttr;
-    super();
+    super($module);
     this.$module = void 0;
     if (!($module instanceof HTMLAnchorElement)) {
       throw new ElementError({
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
index cb5d7de9c..64f4c4c80 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
@@ -18,6 +18,9 @@
       value: value || undefined
     };
   }
+  function isInitialised($module, moduleName) {
+    return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+  }
 
   /**
    * Checks if GOV.UK Frontend is supported on this page
@@ -93,10 +96,33 @@
       this.name = 'ElementError';
     }
   }
+  class InitError extends GOVUKFrontendError {
+    constructor(moduleName, className) {
+      let errorText = `moduleName not defined in component (\`${className}\`)`;
+      if (typeof moduleName === 'string') {
+        errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+      }
+      super(errorText);
+      this.name = 'InitError';
+    }
+  }
 
   class GOVUKFrontendComponent {
-    constructor() {
+    constructor($module) {
       this.checkSupport();
+      this.checkInitialised($module);
+      const moduleName = this.constructor.moduleName;
+      if (typeof moduleName === 'string') {
+        moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+      } else {
+        throw new InitError(moduleName);
+      }
+    }
+    checkInitialised($module) {
+      const moduleName = this.constructor.moduleName;
+      if ($module && moduleName && isInitialised($module, moduleName)) {
+        throw new InitError(moduleName);
+      }
     }
     checkSupport() {
       if (!isSupported()) {
@@ -105,6 +131,15 @@
     }
   }
 
+  /**
+   * @typedef ChildClass
+   * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+   */
+
+  /**
+   * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+   */
+
   /**
    * Tabs component
    *
@@ -115,7 +150,7 @@
      * @param {Element | null} $module - HTML element to use for tabs
      */
     constructor($module) {
-      super();
+      super($module);
       this.$module = void 0;
       this.$tabs = void 0;
       this.$tabList = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
index 29d45d0e2..d1da839a3 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
@@ -12,6 +12,9 @@ function getBreakpoint(name) {
     value: value || undefined
   };
 }
+function isInitialised($module, moduleName) {
+  return $module instanceof HTMLElement && $module.hasAttribute(`data-${moduleName}-init`);
+}
 
 /**
  * Checks if GOV.UK Frontend is supported on this page
@@ -87,10 +90,33 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -99,6 +125,15 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 /**
  * Tabs component
  *
@@ -109,7 +144,7 @@ class Tabs extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for tabs
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$tabs = void 0;
     this.$tabList = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
index 82296adc4..50a2cad7a 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
@@ -12,7 +12,7 @@ class Tabs extends GOVUKFrontendComponent {
    * @param {Element | null} $module - HTML element to use for tabs
    */
   constructor($module) {
-    super();
+    super($module);
     this.$module = void 0;
     this.$tabs = void 0;
     this.$tabList = void 0;
diff --git a/packages/govuk-frontend/dist/govuk/errors/index.mjs b/packages/govuk-frontend/dist/govuk/errors/index.mjs
index 9a0f05478..5fdb0858c 100644
--- a/packages/govuk-frontend/dist/govuk/errors/index.mjs
+++ b/packages/govuk-frontend/dist/govuk/errors/index.mjs
@@ -39,6 +39,16 @@ class ElementError extends GOVUKFrontendError {
     this.name = 'ElementError';
   }
 }
+class InitError extends GOVUKFrontendError {
+  constructor(moduleName, className) {
+    let errorText = `moduleName not defined in component (\`${className}\`)`;
+    if (typeof moduleName === 'string') {
+      errorText = `Root element (\`$module\`) already initialised (\`${moduleName}\`)`;
+    }
+    super(errorText);
+    this.name = 'InitError';
+  }
+}
 
-export { ConfigError, ElementError, GOVUKFrontendError, SupportError };
+export { ConfigError, ElementError, GOVUKFrontendError, InitError, SupportError };
 //# sourceMappingURL=index.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs b/packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs
index 79a580f74..dac155c9c 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs
@@ -1,9 +1,22 @@
-import { isSupported } from './common/index.mjs';
-import { SupportError } from './errors/index.mjs';
+import { isInitialised, isSupported } from './common/index.mjs';
+import { InitError, SupportError } from './errors/index.mjs';
 
 class GOVUKFrontendComponent {
-  constructor() {
+  constructor($module) {
     this.checkSupport();
+    this.checkInitialised($module);
+    const moduleName = this.constructor.moduleName;
+    if (typeof moduleName === 'string') {
+      moduleName && ($module == null ? void 0 : $module.setAttribute(`data-${moduleName}-init`, ''));
+    } else {
+      throw new InitError(moduleName);
+    }
+  }
+  checkInitialised($module) {
+    const moduleName = this.constructor.moduleName;
+    if ($module && moduleName && isInitialised($module, moduleName)) {
+      throw new InitError(moduleName);
+    }
   }
   checkSupport() {
     if (!isSupported()) {
@@ -12,5 +25,14 @@ class GOVUKFrontendComponent {
   }
 }
 
+/**
+ * @typedef ChildClass
+ * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+
 export { GOVUKFrontendComponent };
 //# sourceMappingURL=govuk-frontend-component.mjs.map

Action run for 49f0185

export function isInitialised($module, moduleName) {
return (
$module instanceof HTMLElement &&
`${kebabCaseToCamelCase(moduleName)}Init` in $module.dataset
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`${kebabCaseToCamelCase(moduleName)}Init` in $module.dataset
$module.hasAttribute(`data-${moduleName}-init`)

We can avoid the kebabCaseToCamelCase if we look for the attribute. Also wondering if that function wouldn't be better exported by same module as the base component class one, to regroup the concept for computing the name of that attribute in a single file.

*/
constructor() {
constructor($module, moduleName) {
Copy link
Member

Choose a reason for hiding this comment

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

suggestion The moduleName set on a child class can be accessed via this.constructor.moduleName. This would avoid classes inheriting to have to pass the moduleName to the constructor.

TypeScript may have complaints about this, as there's nothing in the types saying that the child class would have that static property. The base class could check for something like if (typeof this.constructor.moduleName !== 'string') which would both set the type and let users know they're inheriting the class properly 🤔

Copy link
Member

Choose a reason for hiding this comment

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

If we added that check in the constructor, we'd then be able to make the InitError more informative:

  • say that the component is already initialised if moduleName is a string
  • say that the moduleName is not the right type if it's anything else

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm still wondering if we shouldn't have moduleName be a mandatory thing to specify. Since it is used in other places also to create and init components.

if (typeof moduleName === 'string') {
moduleName && $module?.setAttribute(`data-${moduleName}-init`, 'true')
} else {
throw new InitError(moduleName, this.constructor.name)
Copy link
Member

Choose a reason for hiding this comment

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

issue this.constructor.name is at risk of getting mangled by Terser and other code minifiers (making class Accordion become class N) if the name is not defined as a static property in the class.

Copy link
Contributor Author

@patrickpatrickpatrick patrickpatrickpatrick Sep 12, 2024

Choose a reason for hiding this comment

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

hmm, would this just be a matter for adding it for our components and then adding to documentation a recommendation to specify the class name?

Copy link
Member

Choose a reason for hiding this comment

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

@patrickpatrickpatrick I think we can do without putting the constructor's name inside the message. The information is already available in the stacktrace, which will have an entry for the component's constructor and let people find which component has an issue.

Comment on lines 13 to 16
/**
* @type {string|undefined}
*/
static moduleName
Copy link
Member

@romaricpascal romaricpascal Sep 12, 2024

Choose a reason for hiding this comment

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

suggestion Rather than adding an empty static property in JavaScript to solve a TypeScript issue, what if we tried in types by defining a type for the shape we expect for the constructor of a child class:

/**
 * @typedef {typeof GOVUKFrontendComponent} ChildClassConstructor 
 * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
 */

Copy link
Member

Choose a reason for hiding this comment

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

Wondering if there's not some overlap with the CompatibleClass type as well, but maybe not as createAll does not necessarily need a child of GOVUKFrontendComponent 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i think I got this working without static moduleName by creating another type and then combining that type and the type of GOVUKFrontendComponent

Copy link

JavaScript changes to GitHub release

No diff changes found.


Action run for 347d37d

Copy link

Stylesheets changes to GitHub release

No diff changes found.


Action run for 347d37d

Copy link
Member

@romaricpascal romaricpascal left a comment

Choose a reason for hiding this comment

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

Just a couple of last thoughts, let me know if it sounds OK.

export function isInitialised($module, moduleName) {
return (
$module instanceof HTMLElement &&
!!$module.getAttribute(`data-${moduleName}-init`)
Copy link
Member

Choose a reason for hiding this comment

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

suggestion Rather than the double negation, we can use $module.hasAttribute as we don't particularly care about the value of the attribute, just its presence.

if (typeof moduleName === 'string') {
moduleName && $module?.setAttribute(`data-${moduleName}-init`, 'true')
} else {
throw new InitError(moduleName, this.constructor.name)
Copy link
Member

Choose a reason for hiding this comment

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

@patrickpatrickpatrick I think we can do without putting the constructor's name inside the message. The information is already available in the stacktrace, which will have an entry for the component's constructor and let people find which component has an issue.

.moduleName

if (typeof moduleName === 'string') {
moduleName && $module?.setAttribute(`data-${moduleName}-init`, 'true')
Copy link
Member

Choose a reason for hiding this comment

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

suggestion We can keep the HTML slightly lighter by setting that attribute to '' rather than true (and checking with hasAttribute which saves some !! casting, as described in the previous comment.).

Suggested change
moduleName && $module?.setAttribute(`data-${moduleName}-init`, 'true')
moduleName && $module?.setAttribute(`data-${moduleName}-init`, '')

Copy link
Member

@romaricpascal romaricpascal left a comment

Choose a reason for hiding this comment

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

Looks good to go! Thanks for these last amends 😊

@patrickpatrickpatrick patrickpatrickpatrick merged commit 5987b57 into public-js-api Sep 16, 2024
48 checks passed
@owenatgov owenatgov mentioned this pull request Oct 10, 2024
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.

Investigate components being initialised more than once
4 participants