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

Add createAll function to initialise individual components #4975

Merged
merged 7 commits into from May 10, 2024

Conversation

romaricpascal
Copy link
Member

@romaricpascal romaricpascal commented May 9, 2024

Adds a new createAll function that lets users instantiate a given component on all the elements marked with the relevant data-module attribute on the page.

The function allows users to provide a config that will be passed to each instance of the component when created. And can be limited look into only a specific part of the page.

Similarly to initAll (from which the function's code was extracted, and uses that new function under the hood now), the function logs errors to the console rather than throw and break script execution. Users can still call the components' constrcutor themselfs should they want to catch errors.

The feature was initially imagined as a static function, available on each component but ideally defined on our BaseComponent. However:

Copy link

github-actions bot commented May 9, 2024

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 113.32 KiB
dist/govuk-frontend-development.min.js 42.34 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 88.16 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 82.83 KiB
packages/govuk-frontend/dist/govuk/all.mjs 981 B
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 359 B
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 113.3 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 42.33 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 4.86 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 78.45 KiB 40.31 KiB
accordion.mjs 22.71 KiB 12.85 KiB
button.mjs 5.98 KiB 2.69 KiB
character-count.mjs 22.4 KiB 9.92 KiB
checkboxes.mjs 5.83 KiB 2.83 KiB
error-summary.mjs 7.89 KiB 3.46 KiB
exit-this-page.mjs 17.1 KiB 9.26 KiB
header.mjs 4.46 KiB 2.6 KiB
notification-banner.mjs 6.26 KiB 2.62 KiB
password-input.mjs 15.15 KiB 7.25 KiB
radios.mjs 4.83 KiB 2.38 KiB
skip-link.mjs 4.39 KiB 2.18 KiB
tabs.mjs 10.13 KiB 6.11 KiB

View stats and visualisations on the review app


Action run for f4667fd

Copy link

github-actions bot commented May 9, 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 db99e502b..9192c3082 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -1,44 +1,44 @@
 const version = "development";
 
 function normaliseString(e, t) {
-    const s = e ? e.trim() : "";
-    let n, i = null == t ? void 0 : t.type;
-    switch (i || (["true", "false"].includes(s) && (i = "boolean"), s.length > 0 && isFinite(Number(s)) && (i = "number")), i) {
+    const n = e ? e.trim() : "";
+    let s, i = null == t ? void 0 : t.type;
+    switch (i || (["true", "false"].includes(n) && (i = "boolean"), n.length > 0 && isFinite(Number(n)) && (i = "number")), i) {
         case "boolean":
-            n = "true" === s;
+            s = "true" === n;
             break;
         case "number":
-            n = Number(s);
+            s = Number(n);
             break;
         default:
-            n = e
+            s = e
     }
-    return n
+    return s
 }
 
 function mergeConfigs(...e) {
     const t = {};
-    for (const s of e)
-        for (const e of Object.keys(s)) {
-            const n = t[e],
-                i = s[e];
-            isObject(n) && isObject(i) ? t[e] = mergeConfigs(n, i) : t[e] = i
+    for (const n of e)
+        for (const e of Object.keys(n)) {
+            const s = t[e],
+                i = n[e];
+            isObject(s) && isObject(i) ? t[e] = mergeConfigs(s, i) : t[e] = i
         }
     return t
 }
 
-function extractConfigByNamespace(e, t, s) {
-    const n = e.schema.properties[s];
-    if ("object" !== (null == n ? void 0 : n.type)) return;
+function extractConfigByNamespace(e, t, n) {
+    const s = e.schema.properties[n];
+    if ("object" !== (null == s ? void 0 : s.type)) return;
     const i = {
-        [s]: {}
+        [n]: {}
     };
     for (const [o, r] of Object.entries(t)) {
         let e = i;
         const t = o.split(".");
-        for (const [n, i] of t.entries()) "object" == typeof e && (n < t.length - 1 ? (isObject(e[i]) || (e[i] = {}), e = e[i]) : o !== s && (e[i] = normaliseString(r)))
+        for (const [s, i] of t.entries()) "object" == typeof e && (s < t.length - 1 ? (isObject(e[i]) || (e[i] = {}), e = e[i]) : o !== n && (e[i] = normaliseString(r)))
     }
-    return i[s]
+    return i[n]
 }
 
 function getFragmentFromUrl(e) {
@@ -54,20 +54,20 @@ function getBreakpoint(e) {
 }
 
 function setFocus(e, t = {}) {
-    var s;
-    const n = e.getAttribute("tabindex");
+    var n;
+    const s = e.getAttribute("tabindex");
 
     function onBlur() {
-        var s;
-        null == (s = t.onBlur) || s.call(e), n || e.removeAttribute("tabindex")
+        var n;
+        null == (n = t.onBlur) || n.call(e), s || e.removeAttribute("tabindex")
     }
-    n || e.setAttribute("tabindex", "-1"), e.addEventListener("focus", (function() {
+    s || e.setAttribute("tabindex", "-1"), e.addEventListener("focus", (function() {
         e.addEventListener("blur", onBlur, {
             once: !0
         })
     }), {
         once: !0
-    }), null == (s = t.onBeforeFocus) || s.call(e), e.focus()
+    }), null == (n = t.onBeforeFocus) || n.call(e), e.focus()
 }
 
 function isSupported(e = document.body) {
@@ -81,9 +81,9 @@ function isObject(e) {
 }
 
 function normaliseDataset(e, t) {
-    const s = {};
-    for (const [n, i] of Object.entries(e.schema.properties)) n in t && (s[n] = normaliseString(t[n], i)), "object" === (null == i ? void 0 : i.type) && (s[n] = extractConfigByNamespace(e, t, n));
-    return s
+    const n = {};
+    for (const [s, i] of Object.entries(e.schema.properties)) s in t && (n[s] = normaliseString(t[s], i)), "object" === (null == i ? void 0 : i.type) && (n[s] = extractConfigByNamespace(e, t, s));
+    return n
 }
 class GOVUKFrontendError extends Error {
     constructor(...e) {
@@ -106,12 +106,12 @@ class ElementError extends GOVUKFrontendError {
         let t = "string" == typeof e ? e : "";
         if ("object" == typeof e) {
             const {
-                componentName: s,
-                identifier: n,
+                componentName: n,
+                identifier: s,
                 element: i,
                 expectedType: o
             } = e;
-            t = `${s}: ${n}`, t += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found"
+            t = `${n}: ${s}`, t += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found"
         }
         super(t), this.name = "ElementError"
     }
@@ -126,31 +126,31 @@ class GOVUKFrontendComponent {
 }
 class I18n {
     constructor(e = {}, t = {}) {
-        var s;
-        this.translations = void 0, this.locale = void 0, this.translations = e, this.locale = null != (s = t.locale) ? s : document.documentElement.lang || "en"
+        var n;
+        this.translations = void 0, this.locale = void 0, this.translations = e, this.locale = null != (n = t.locale) ? n : document.documentElement.lang || "en"
     }
     t(e, t) {
         if (!e) throw new Error("i18n: lookup key missing");
-        let s = this.translations[e];
-        if ("number" == typeof(null == t ? void 0 : t.count) && "object" == typeof s) {
-            const n = s[this.getPluralSuffix(e, t.count)];
-            n && (s = n)
+        let n = this.translations[e];
+        if ("number" == typeof(null == t ? void 0 : t.count) && "object" == typeof n) {
+            const s = n[this.getPluralSuffix(e, t.count)];
+            s && (n = s)
         }
-        if ("string" == typeof s) {
-            if (s.match(/%{(.\S+)}/)) {
+        if ("string" == typeof n) {
+            if (n.match(/%{(.\S+)}/)) {
                 if (!t) throw new Error("i18n: cannot replace placeholders in string if no option data provided");
-                return this.replacePlaceholders(s, t)
+                return this.replacePlaceholders(n, t)
             }
-            return s
+            return n
         }
         return e
     }
     replacePlaceholders(e, t) {
-        const s = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
-        return e.replace(/%{(.\S+)}/g, (function(e, n) {
-            if (Object.prototype.hasOwnProperty.call(t, n)) {
-                const e = t[n];
-                return !1 === e || "number" != typeof e && "string" != typeof e ? "" : "number" == typeof e ? s ? s.format(e) : `${e}` : e
+        const n = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
+        return e.replace(/%{(.\S+)}/g, (function(e, s) {
+            if (Object.prototype.hasOwnProperty.call(t, s)) {
+                const e = t[s];
+                return !1 === e || "number" != typeof e && "string" != typeof e ? "" : "number" == typeof e ? n ? n.format(e) : `${e}` : e
             }
             throw new Error(`i18n: no data found to replace ${e} placeholder in string`)
         }))
@@ -160,11 +160,11 @@ class I18n {
     }
     getPluralSuffix(e, t) {
         if (t = Number(t), !isFinite(t)) return "other";
-        const s = this.translations[e],
-            n = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(t) : this.selectPluralFormUsingFallbackRules(t);
-        if ("object" == typeof s) {
-            if (n in s) return n;
-            if ("other" in s) return console.warn(`i18n: Missing plural form ".${n}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+        const n = this.translations[e],
+            s = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(t) : this.selectPluralFormUsingFallbackRules(t);
+        if ("object" == typeof n) {
+            if (s in n) return s;
+            if ("other" in n) return console.warn(`i18n: Missing plural form ".${s}" for "${this.locale}" locale. Falling back to ".other".`), "other"
         }
         throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`)
     }
@@ -176,8 +176,8 @@ class I18n {
     getPluralRulesForLocale() {
         const e = this.locale.split("-")[0];
         for (const t in I18n.pluralRulesMap) {
-            const s = I18n.pluralRulesMap[t];
-            if (s.includes(this.locale) || s.includes(e)) return t
+            const n = I18n.pluralRulesMap[t];
+            if (n.includes(this.locale) || n.includes(e)) return t
         }
     }
 }
@@ -199,27 +199,27 @@ I18n.pluralRulesMap = {
     irish: e => 1 === e ? "one" : 2 === e ? "two" : e >= 3 && e <= 6 ? "few" : e >= 7 && e <= 10 ? "many" : "other",
     russian(e) {
         const t = e % 100,
-            s = t % 10;
-        return 1 === s && 11 !== t ? "one" : s >= 2 && s <= 4 && !(t >= 12 && t <= 14) ? "few" : 0 === s || s >= 5 && s <= 9 || t >= 11 && t <= 14 ? "many" : "other"
+            n = t % 10;
+        return 1 === n && 11 !== t ? "one" : n >= 2 && n <= 4 && !(t >= 12 && t <= 14) ? "few" : 0 === n || n >= 5 && n <= 9 || t >= 11 && t <= 14 ? "many" : "other"
     },
     scottish: e => 1 === e || 11 === e ? "one" : 2 === e || 12 === e ? "two" : e >= 3 && e <= 10 || e >= 13 && e <= 19 ? "few" : "other",
     spanish: e => 1 === e ? "one" : e % 1e6 == 0 && 0 !== e ? "many" : "other",
     welsh: e => 0 === e ? "zero" : 1 === e ? "one" : 2 === e ? "two" : 3 === e ? "few" : 6 === e ? "many" : "other"
 };
 class Accordion extends GOVUKFrontendComponent {
-    constructor(t, s = {}) {
+    constructor(t, n = {}) {
         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.browserSupportsSessionStorage = !1, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, !(t instanceof HTMLElement)) throw new ElementError({
             componentName: "Accordion",
             element: t,
             identifier: "Root element (`$module`)"
         });
-        this.$module = t, this.config = mergeConfigs(Accordion.defaults, s, normaliseDataset(Accordion, t.dataset)), this.i18n = new I18n(this.config.i18n);
-        const n = this.$module.querySelectorAll(`.${this.sectionClass}`);
-        if (!n.length) throw new ElementError({
+        this.$module = t, this.config = mergeConfigs(Accordion.defaults, n, normaliseDataset(Accordion, t.dataset)), this.i18n = new I18n(this.config.i18n);
+        const s = this.$module.querySelectorAll(`.${this.sectionClass}`);
+        if (!s.length) throw new ElementError({
             componentName: "Accordion",
             identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
         });
-        this.$sections = n, this.browserSupportsSessionStorage = e.checkForSessionStorage(), this.initControls(), this.initSectionHeaders();
+        this.$sections = s, this.browserSupportsSessionStorage = e.checkForSessionStorage(), this.initControls(), this.initSectionHeaders();
         const i = this.checkIfAllSectionsOpen();
         this.updateShowAllButton(i)
     }
@@ -230,33 +230,33 @@ class Accordion extends GOVUKFrontendComponent {
     }
     initSectionHeaders() {
         this.$sections.forEach(((e, t) => {
-            const s = e.querySelector(`.${this.sectionHeaderClass}`);
-            if (!s) throw new ElementError({
+            const n = e.querySelector(`.${this.sectionHeaderClass}`);
+            if (!n) throw new ElementError({
                 componentName: "Accordion",
                 identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
             });
-            this.constructHeaderMarkup(s, t), this.setExpanded(this.isExpanded(e), e), s.addEventListener("click", (() => this.onSectionToggle(e))), this.setInitialState(e)
+            this.constructHeaderMarkup(n, t), this.setExpanded(this.isExpanded(e), e), n.addEventListener("click", (() => this.onSectionToggle(e))), this.setInitialState(e)
         }))
     }
     constructHeaderMarkup(e, t) {
-        const s = e.querySelector(`.${this.sectionButtonClass}`),
-            n = e.querySelector(`.${this.sectionHeadingClass}`),
+        const n = e.querySelector(`.${this.sectionButtonClass}`),
+            s = e.querySelector(`.${this.sectionHeadingClass}`),
             i = e.querySelector(`.${this.sectionSummaryClass}`);
-        if (!n) throw new ElementError({
+        if (!s) throw new ElementError({
             componentName: "Accordion",
             identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
         });
-        if (!s) throw new ElementError({
+        if (!n) throw new ElementError({
             componentName: "Accordion",
             identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
         });
         const o = document.createElement("button");
         o.setAttribute("type", "button"), o.setAttribute("aria-controls", `${this.$module.id}-content-${t+1}`);
-        for (const d of Array.from(s.attributes)) "id" !== d.nodeName && o.setAttribute(d.nodeName, `${d.nodeValue}`);
+        for (const d of Array.from(n.attributes)) "id" !== d.nodeName && o.setAttribute(d.nodeName, `${d.nodeValue}`);
         const r = document.createElement("span");
-        r.classList.add(this.sectionHeadingTextClass), r.id = s.id;
+        r.classList.add(this.sectionHeadingTextClass), r.id = n.id;
         const a = document.createElement("span");
-        a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), a.innerHTML = s.innerHTML;
+        a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), a.innerHTML = n.innerHTML;
         const l = document.createElement("span");
         l.classList.add(this.sectionShowHideToggleClass), l.setAttribute("data-nosnippet", "");
         const c = document.createElement("span");
@@ -267,16 +267,16 @@ class Accordion extends GOVUKFrontendComponent {
             const e = document.createElement("span"),
                 t = document.createElement("span");
             t.classList.add(this.sectionSummaryFocusClass), e.appendChild(t);
-            for (const s of Array.from(i.attributes)) e.setAttribute(s.nodeName, `${s.nodeValue}`);
+            for (const n of Array.from(i.attributes)) e.setAttribute(n.nodeName, `${n.nodeValue}`);
             t.innerHTML = i.innerHTML, i.parentNode.replaceChild(e, i), o.appendChild(e), o.appendChild(this.getButtonPunctuationEl())
         }
-        o.appendChild(l), n.removeChild(s), n.appendChild(o)
+        o.appendChild(l), s.removeChild(n), s.appendChild(o)
     }
     onBeforeMatch(e) {
         const t = e.target;
         if (!(t instanceof Element)) return;
-        const s = t.closest(`.${this.sectionClass}`);
-        s && this.setExpanded(!0, s)
+        const n = t.closest(`.${this.sectionClass}`);
+        n && this.setExpanded(!0, n)
     }
     onSectionToggle(e) {
         const t = this.isExpanded(e);
@@ -289,24 +289,24 @@ class Accordion extends GOVUKFrontendComponent {
         })), this.updateShowAllButton(e)
     }
     setExpanded(e, t) {
-        const s = t.querySelector(`.${this.upChevronIconClass}`),
-            n = t.querySelector(`.${this.sectionShowHideTextClass}`),
+        const n = t.querySelector(`.${this.upChevronIconClass}`),
+            s = t.querySelector(`.${this.sectionShowHideTextClass}`),
             i = t.querySelector(`.${this.sectionButtonClass}`),
             o = t.querySelector(`.${this.sectionContentClass}`);
         if (!o) throw new ElementError({
             componentName: "Accordion",
             identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
         });
-        if (!s || !n || !i) return;
+        if (!n || !s || !i) return;
         const r = e ? this.i18n.t("hideSection") : this.i18n.t("showSection");
-        n.textContent = r, i.setAttribute("aria-expanded", `${e}`);
+        s.textContent = r, i.setAttribute("aria-expanded", `${e}`);
         const a = [],
             l = t.querySelector(`.${this.sectionHeadingTextClass}`);
         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), i.setAttribute("aria-label", a.join(" , ")), e ? (o.removeAttribute("hidden"), t.classList.add(this.sectionExpandedClass), s.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), t.classList.remove(this.sectionExpandedClass), s.classList.add(this.downChevronIconClass));
+        a.push(h), i.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));
         const u = this.checkIfAllSectionsOpen();
         this.updateShowAllButton(u)
     }
@@ -324,8 +324,8 @@ class Accordion extends GOVUKFrontendComponent {
             const t = e.querySelector(`.${this.sectionButtonClass}`);
             if (t) {
                 const e = t.getAttribute("aria-controls"),
-                    s = t.getAttribute("aria-expanded");
-                e && s && window.sessionStorage.setItem(e, s)
+                    n = t.getAttribute("aria-expanded");
+                e && n && window.sessionStorage.setItem(e, n)
             }
         }
     }
@@ -333,9 +333,9 @@ class Accordion extends GOVUKFrontendComponent {
         if (this.browserSupportsSessionStorage && this.config.rememberExpanded) {
             const t = e.querySelector(`.${this.sectionButtonClass}`);
             if (t) {
-                const s = t.getAttribute("aria-controls"),
-                    n = s ? window.sessionStorage.getItem(s) : null;
-                null !== n && this.setExpanded("true" === n, e)
+                const n = t.getAttribute("aria-controls"),
+                    s = n ? window.sessionStorage.getItem(n) : null;
+                null !== s && this.setExpanded("true" === s, e)
             }
         }
     }
@@ -370,7 +370,7 @@ const e = {
         let t;
         try {
             return window.sessionStorage.setItem(e, e), t = window.sessionStorage.getItem(e) === e.toString(), window.sessionStorage.removeItem(e), t
-        } catch (s) {
+        } catch (n) {
             return !1
         }
     }
@@ -396,8 +396,8 @@ class Button extends GOVUKFrontendComponent {
 }
 
 function closestAttributeValue(e, t) {
-    const s = e.closest(`[${t}]`);
-    return s ? s.getAttribute(t) : null
+    const n = e.closest(`[${t}]`);
+    return n ? n.getAttribute(t) : null
 }
 Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
     preventDoubleClick: !1
@@ -410,7 +410,7 @@ Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
 });
 class CharacterCount extends GOVUKFrontendComponent {
     constructor(e, t = {}) {
-        var s, n;
+        var n, s;
         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({
             componentName: "Character count",
             element: e,
@@ -430,24 +430,24 @@ class CharacterCount extends GOVUKFrontendComponent {
             maxwords: void 0
         }), this.config = mergeConfigs(CharacterCount.defaults, t, r, o);
         const a = function(e, t) {
-            const s = [];
-            for (const [n, i] of Object.entries(e)) {
+            const n = [];
+            for (const [s, i] of Object.entries(e)) {
                 const e = [];
                 if (Array.isArray(i)) {
                     for (const {
-                            required: s,
-                            errorMessage: n
+                            required: n,
+                            errorMessage: s
                         }
-                        of i) s.every((e => !!t[e])) || e.push(n);
-                    "anyOf" !== n || i.length - e.length >= 1 || s.push(...e)
+                        of i) n.every((e => !!t[e])) || e.push(s);
+                    "anyOf" !== s || i.length - e.length >= 1 || n.push(...e)
                 }
             }
-            return s
+            return n
         }(CharacterCount.schema, this.config);
         if (a[0]) throw new ConfigError(`Character count: ${a[0]}`);
         this.i18n = new I18n(this.config.i18n, {
             locale: closestAttributeValue(e, "lang")
-        }), this.maxLength = null != (s = null != (n = this.config.maxwords) ? n : this.config.maxlength) ? s : 1 / 0, this.$module = e, this.$textarea = i;
+        }), this.maxLength = null != (n = null != (s = this.config.maxwords) ? s : this.config.maxlength) ? n : 1 / 0, this.$module = e, this.$textarea = i;
         const l = `${this.$textarea.id}-info`,
             c = document.getElementById(l);
         if (!c) throw new ElementError({
@@ -504,8 +504,8 @@ class CharacterCount extends GOVUKFrontendComponent {
     }
     formatCountMessage(e, t) {
         if (0 === e) return this.i18n.t(`${t}AtLimit`);
-        const s = e < 0 ? "OverLimit" : "UnderLimit";
-        return this.i18n.t(`${t}${s}`, {
+        const n = e < 0 ? "OverLimit" : "UnderLimit";
+        return this.i18n.t(`${t}${n}`, {
             count: Math.abs(e)
         })
     }
@@ -592,10 +592,10 @@ class Checkboxes extends GOVUKFrontendComponent {
     syncConditionalRevealWithInputState(e) {
         const t = e.getAttribute("aria-controls");
         if (!t) return;
-        const s = document.getElementById(t);
-        if (null != s && s.classList.contains("govuk-checkboxes__conditional")) {
+        const n = document.getElementById(t);
+        if (null != n && n.classList.contains("govuk-checkboxes__conditional")) {
             const t = e.checked;
-            e.setAttribute("aria-expanded", t.toString()), s.classList.toggle("govuk-checkboxes__conditional--hidden", !t)
+            e.setAttribute("aria-expanded", t.toString()), n.classList.toggle("govuk-checkboxes__conditional--hidden", !t)
         }
     }
     unCheckAllInputsExcept(e) {
@@ -633,25 +633,25 @@ class ErrorSummary extends GOVUKFrontendComponent {
         if (!(e instanceof HTMLAnchorElement)) return !1;
         const t = getFragmentFromUrl(e.href);
         if (!t) return !1;
-        const s = document.getElementById(t);
-        if (!s) return !1;
-        const n = this.getAssociatedLegendOrLabel(s);
-        return !!n && (n.scrollIntoView(), s.focus({
+        const n = document.getElementById(t);
+        if (!n) return !1;
+        const s = this.getAssociatedLegendOrLabel(n);
+        return !!s && (s.scrollIntoView(), n.focus({
             preventScroll: !0
         }), !0)
     }
     getAssociatedLegendOrLabel(e) {
         var t;
-        const s = e.closest("fieldset");
-        if (s) {
-            const t = s.getElementsByTagName("legend");
+        const n = e.closest("fieldset");
+        if (n) {
+            const t = n.getElementsByTagName("legend");
             if (t.length) {
-                const s = t[0];
-                if (e instanceof HTMLInputElement && ("checkbox" === e.type || "radio" === e.type)) return s;
-                const n = s.getBoundingClientRect().top,
+                const n = t[0];
+                if (e instanceof HTMLInputElement && ("checkbox" === e.type || "radio" === e.type)) return n;
+                const s = n.getBoundingClientRect().top,
                     i = e.getBoundingClientRect();
                 if (i.height && window.innerHeight) {
-                    if (i.top + i.height - n < window.innerHeight / 2) return s
+                    if (i.top + i.height - s < window.innerHeight / 2) return n
                 }
             }
         }
@@ -674,16 +674,16 @@ class ExitThisPage extends GOVUKFrontendComponent {
             element: e,
             identifier: "Root element (`$module`)"
         });
-        const s = e.querySelector(".govuk-exit-this-page__button");
-        if (!(s instanceof HTMLAnchorElement)) throw new ElementError({
+        const n = e.querySelector(".govuk-exit-this-page__button");
+        if (!(n instanceof HTMLAnchorElement)) throw new ElementError({
             componentName: "Exit this page",
-            element: s,
+            element: n,
             expectedType: "HTMLAnchorElement",
             identifier: "Button (`.govuk-exit-this-page__button`)"
         });
-        this.config = mergeConfigs(ExitThisPage.defaults, t, normaliseDataset(ExitThisPage, e.dataset)), this.i18n = new I18n(this.config.i18n), this.$module = e, this.$button = s;
-        const n = document.querySelector(".govuk-js-exit-this-page-skiplink");
-        n instanceof HTMLAnchorElement && (this.$skiplinkButton = n), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
+        this.config = mergeConfigs(ExitThisPage.defaults, t, normaliseDataset(ExitThisPage, e.dataset)), this.i18n = new I18n(this.config.i18n), this.$module = e, this.$button = n;
+        const s = document.querySelector(".govuk-js-exit-this-page-skiplink");
+        s instanceof HTMLAnchorElement && (this.$skiplinkButton = s), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
     }
     initUpdateSpan() {
         this.$updateSpan = document.createElement("span"), this.$updateSpan.setAttribute("role", "status"), this.$updateSpan.className = "govuk-visually-hidden", this.$module.appendChild(this.$updateSpan)
@@ -754,18 +754,18 @@ class Header extends GOVUKFrontendComponent {
         this.$module = e;
         const t = e.querySelector(".govuk-js-header-toggle");
         if (!t) return this;
-        const s = t.getAttribute("aria-controls");
-        if (!s) throw new ElementError({
+        const n = t.getAttribute("aria-controls");
+        if (!n) throw new ElementError({
             componentName: "Header",
             identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
         });
-        const n = document.getElementById(s);
-        if (!n) throw new ElementError({
+        const s = document.getElementById(n);
+        if (!s) throw new ElementError({
             componentName: "Header",
-            element: n,
-            identifier: `Navigation (\`<ul id="${s}">\`)`
+            element: s,
+            identifier: `Navigation (\`<ul id="${n}">\`)`
         });
-        this.$menu = n, this.$menuButton = t, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+        this.$menu = s, this.$menuButton = t, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
     }
     setupResponsiveChecks() {
         const e = getBreakpoint("desktop");
@@ -809,23 +809,23 @@ class PasswordInput extends GOVUKFrontendComponent {
             element: e,
             identifier: "Root element (`$module`)"
         });
-        const s = e.querySelector(".govuk-js-password-input-input");
-        if (!(s instanceof HTMLInputElement)) throw new ElementError({
+        const n = e.querySelector(".govuk-js-password-input-input");
+        if (!(n instanceof HTMLInputElement)) throw new ElementError({
             componentName: "Password input",
-            element: s,
+            element: n,
             expectedType: "HTMLInputElement",
             identifier: "Form field (`.govuk-js-password-input-input`)"
         });
-        if ("password" !== s.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
-        const n = e.querySelector(".govuk-js-password-input-toggle");
-        if (!(n instanceof HTMLButtonElement)) throw new ElementError({
+        if ("password" !== n.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
+        const s = e.querySelector(".govuk-js-password-input-toggle");
+        if (!(s instanceof HTMLButtonElement)) throw new ElementError({
             componentName: "Password input",
-            element: n,
+            element: s,
             expectedType: "HTMLButtonElement",
             identifier: "Button (`.govuk-js-password-input-toggle`)"
         });
-        if ("button" !== n.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
-        this.$module = e, this.$input = s, this.$showHideButton = n, this.config = mergeConfigs(PasswordInput.defaults, t, normaliseDataset(PasswordInput, e.dataset)), this.i18n = new I18n(this.config.i18n, {
+        if ("button" !== s.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
+        this.$module = e, this.$input = n, this.$showHideButton = s, this.config = mergeConfigs(PasswordInput.defaults, t, normaliseDataset(PasswordInput, e.dataset)), this.i18n = new I18n(this.config.i18n, {
             locale: closestAttributeValue(e, "lang")
         }), this.$showHideButton.removeAttribute("hidden");
         const i = document.createElement("div");
@@ -846,9 +846,9 @@ class PasswordInput extends GOVUKFrontendComponent {
         if (e === this.$input.type) return;
         this.$input.setAttribute("type", e);
         const t = "password" === e,
-            s = t ? "show" : "hide",
-            n = t ? "passwordHidden" : "passwordShown";
-        this.$showHideButton.innerText = this.i18n.t(`${s}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${s}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${n}Announcement`)
+            n = t ? "show" : "hide",
+            s = t ? "passwordHidden" : "passwordShown";
+        this.$showHideButton.innerText = this.i18n.t(`${n}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${n}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${s}Announcement`)
     }
 }
 PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Object.freeze({
@@ -896,20 +896,20 @@ class Radios extends GOVUKFrontendComponent {
     syncConditionalRevealWithInputState(e) {
         const t = e.getAttribute("aria-controls");
         if (!t) return;
-        const s = document.getElementById(t);
-        if (null != s && s.classList.contains("govuk-radios__conditional")) {
+        const n = document.getElementById(t);
+        if (null != n && n.classList.contains("govuk-radios__conditional")) {
             const t = e.checked;
-            e.setAttribute("aria-expanded", t.toString()), s.classList.toggle("govuk-radios__conditional--hidden", !t)
+            e.setAttribute("aria-expanded", t.toString()), n.classList.toggle("govuk-radios__conditional--hidden", !t)
         }
     }
     handleClick(e) {
         const t = e.target;
         if (!(t instanceof HTMLInputElement) || "radio" !== t.type) return;
-        const s = document.querySelectorAll('input[type="radio"][aria-controls]'),
-            n = t.form,
+        const n = document.querySelectorAll('input[type="radio"][aria-controls]'),
+            s = t.form,
             i = t.name;
-        s.forEach((e => {
-            const t = e.form === n;
+        n.forEach((e => {
+            const t = e.form === s;
             e.name === i && t && this.syncConditionalRevealWithInputState(e)
         }))
     }
@@ -925,17 +925,17 @@ class SkipLink extends GOVUKFrontendComponent {
             identifier: "Root element (`$module`)"
         });
         this.$module = e;
-        const s = this.$module.hash,
-            n = null != (t = this.$module.getAttribute("href")) ? t : "";
+        const n = this.$module.hash,
+            s = null != (t = this.$module.getAttribute("href")) ? t : "";
         let i;
         try {
             i = new window.URL(this.$module.href)
         } catch (a) {
-            throw new ElementError(`Skip link: Target link (\`href="${n}"\`) is invalid`)
+            throw new ElementError(`Skip link: Target link (\`href="${s}"\`) is invalid`)
         }
         if (i.origin !== window.location.origin || i.pathname !== window.location.pathname) return;
-        const o = getFragmentFromUrl(s);
-        if (!o) throw new ElementError(`Skip link: Target link (\`href="${n}"\`) has no hash fragment`);
+        const o = getFragmentFromUrl(n);
+        if (!o) throw new ElementError(`Skip link: Target link (\`href="${s}"\`) has no hash fragment`);
         const r = document.getElementById(o);
         if (!r) throw new ElementError({
             componentName: "Skip link",
@@ -966,17 +966,17 @@ class Tabs extends GOVUKFrontendComponent {
             identifier: 'Links (`<a class="govuk-tabs__tab">`)'
         });
         this.$module = e, this.$tabs = t, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
-        const s = this.$module.querySelector(".govuk-tabs__list"),
-            n = this.$module.querySelectorAll("li.govuk-tabs__list-item");
-        if (!s) throw new ElementError({
+        const n = this.$module.querySelector(".govuk-tabs__list"),
+            s = this.$module.querySelectorAll("li.govuk-tabs__list-item");
+        if (!n) throw new ElementError({
             componentName: "Tabs",
             identifier: 'List (`<ul class="govuk-tabs__list">`)'
         });
-        if (!n.length) throw new ElementError({
+        if (!s.length) throw new ElementError({
             componentName: "Tabs",
             identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
         });
-        this.$tabList = s, this.$tabListItems = n, this.setupResponsiveChecks()
+        this.$tabList = n, this.$tabListItems = s, this.setupResponsiveChecks()
     }
     setupResponsiveChecks() {
         const e = getBreakpoint("tablet");
@@ -1012,8 +1012,8 @@ class Tabs extends GOVUKFrontendComponent {
             t = this.getTab(e);
         if (!t) return;
         if (this.changingHash) return void(this.changingHash = !1);
-        const s = this.getCurrentTab();
-        s && (this.hideTab(s), this.showTab(t), t.focus())
+        const n = this.getCurrentTab();
+        n && (this.hideTab(n), this.showTab(t), t.focus())
     }
     hideTab(e) {
         this.unhighlightTab(e), this.hidePanel(e)
@@ -1028,8 +1028,8 @@ class Tabs extends GOVUKFrontendComponent {
         const t = getFragmentFromUrl(e.href);
         if (!t) return;
         e.setAttribute("id", `tab_${t}`), e.setAttribute("role", "tab"), e.setAttribute("aria-controls", t), e.setAttribute("aria-selected", "false"), e.setAttribute("tabindex", "-1");
-        const s = this.getPanel(e);
-        s && (s.setAttribute("role", "tabpanel"), s.setAttribute("aria-labelledby", e.id), s.classList.add(this.jsHiddenClass))
+        const n = this.getPanel(e);
+        n && (n.setAttribute("role", "tabpanel"), n.setAttribute("aria-labelledby", e.id), n.classList.add(this.jsHiddenClass))
     }
     unsetAttributes(e) {
         e.removeAttribute("id"), e.removeAttribute("role"), e.removeAttribute("aria-controls"), e.removeAttribute("aria-selected"), e.removeAttribute("tabindex");
@@ -1038,14 +1038,14 @@ class Tabs extends GOVUKFrontendComponent {
     }
     onTabClick(e) {
         const t = this.getCurrentTab(),
-            s = e.currentTarget;
-        t && s instanceof HTMLAnchorElement && (e.preventDefault(), this.hideTab(t), this.showTab(s), this.createHistoryEntry(s))
+            n = e.currentTarget;
+        t && n instanceof HTMLAnchorElement && (e.preventDefault(), this.hideTab(t), this.showTab(n), this.createHistoryEntry(n))
     }
     createHistoryEntry(e) {
         const t = this.getPanel(e);
         if (!t) return;
-        const s = t.id;
-        t.id = "", this.changingHash = !0, window.location.hash = s, t.id = s
+        const n = t.id;
+        t.id = "", this.changingHash = !0, window.location.hash = n, t.id = n
     }
     onTabKeydown(e) {
         switch (e.key) {
@@ -1067,16 +1067,16 @@ class Tabs extends GOVUKFrontendComponent {
         if (null == e || !e.parentElement) return;
         const t = e.parentElement.nextElementSibling;
         if (!t) return;
-        const s = t.querySelector("a.govuk-tabs__tab");
-        s && (this.hideTab(e), this.showTab(s), s.focus(), this.createHistoryEntry(s))
+        const n = t.querySelector("a.govuk-tabs__tab");
+        n && (this.hideTab(e), this.showTab(n), n.focus(), this.createHistoryEntry(n))
     }
     activatePreviousTab() {
         const e = this.getCurrentTab();
         if (null == e || !e.parentElement) return;
         const t = e.parentElement.previousElementSibling;
         if (!t) return;
-        const s = t.querySelector("a.govuk-tabs__tab");
-        s && (this.hideTab(e), this.showTab(s), s.focus(), this.createHistoryEntry(s))
+        const n = t.querySelector("a.govuk-tabs__tab");
+        n && (this.hideTab(e), this.showTab(n), n.focus(), this.createHistoryEntry(n))
     }
     getPanel(e) {
         const t = getFragmentFromUrl(e.href);
@@ -1104,7 +1104,7 @@ class Tabs extends GOVUKFrontendComponent {
 function initAll(e) {
     var t;
     if (e = void 0 !== e ? e : {}, !isSupported()) return void console.log(new SupportError);
-    const s = [
+    const n = [
             [Accordion, e.accordion],
             [Button, e.button],
             [CharacterCount, e.characterCount],
@@ -1118,17 +1118,22 @@ function initAll(e) {
             [SkipLink],
             [Tabs]
         ],
-        n = null != (t = e.scope) ? t : document;
-    s.forEach((([e, t]) => {
-        n.querySelectorAll(`[data-module="${e.moduleName}"]`).forEach((s => {
-            try {
-                "defaults" in e ? new e(s, t) : new e(s)
-            } catch (n) {
-                console.log(n)
-            }
-        }))
+        s = null != (t = e.scope) ? t : document;
+    n.forEach((([e, t]) => {
+        createAll(e, t, s)
     }))
 }
+
+function createAll(e, t, n = document) {
+    const s = n.querySelectorAll(`[data-module="${e.moduleName}"]`);
+    return Array.from(s).map((n => {
+        try {
+            return "defaults" in e && void 0 !== t ? new e(n, t) : new e(n)
+        } catch (s) {
+            return console.log(s), null
+        }
+    })).filter(Boolean)
+}
 Tabs.moduleName = "govuk-tabs";
 export {
     Accordion,
@@ -1143,6 +1148,7 @@ export {
     Radios,
     SkipLink,
     Tabs,
+    createAll,
     initAll,
     version
 }; //# sourceMappingURL=govuk-frontend.min.js.map
\ No newline at end of file

Action run for f4667fd

Copy link

github-actions bot commented May 9, 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 66ef07672..8a123ccac 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -2372,17 +2372,39 @@
     const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [SkipLink], [Tabs]];
     const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
     components.forEach(([Component, config]) => {
-      const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
-      $elements.forEach($element => {
-        try {
-          'defaults' in Component ? new Component($element, config) : new Component($element);
-        } catch (error) {
-          console.log(error);
-        }
-      });
+      createAll(Component, config, $scope);
     });
   }
 
+  /**
+   * Create all instances of a specific component on the page
+   *
+   * Uses the `data-module` attribute to find all elements matching the specified
+   * component on the page, creating instances of the component object for each
+   * of them.
+   *
+   * Any component errors will be caught and logged to the console.
+   *
+   * @template {CompatibleClass} T
+   * @param {T} Component - class of the component to create
+   * @param {T["defaults"]} [config] - config for the component
+   * @param {Element|Document} [$scope] - scope of the document to search within
+   * @returns {Array<InstanceType<T>>} - array of instantiated components
+   */
+  function createAll(Component, config, $scope = document) {
+    const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
+    return Array.from($elements).map($element => {
+      try {
+        return 'defaults' in Component && typeof config !== 'undefined' ? new Component($element, config) : new Component($element);
+      } catch (error) {
+        console.log(error);
+        return null;
+      }
+    }).filter(Boolean);
+  }
+  /**
+   * @typedef {{new (...args: any[]): any, defaults?: object, moduleName: string}} CompatibleClass
+   */
   /**
    * Config for all components via `initAll()`
    *
@@ -2395,7 +2417,6 @@
    * @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
    * @property {PasswordInputConfig} [passwordInput] - Password input config
    */
-
   /**
    * Config for individual components
    *
@@ -2410,7 +2431,6 @@
    * @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
    * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
    */
-
   /**
    * Component config keys, e.g. `accordion` and `characterCount`
    *
@@ -2429,6 +2449,7 @@
   exports.Radios = Radios;
   exports.SkipLink = SkipLink;
   exports.Tabs = Tabs;
+  exports.createAll = createAll;
   exports.initAll = initAll;
   exports.version = version;
 
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index 669d06852..79ce345b0 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -2366,17 +2366,39 @@ function initAll(config) {
   const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [SkipLink], [Tabs]];
   const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
   components.forEach(([Component, config]) => {
-    const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
-    $elements.forEach($element => {
-      try {
-        'defaults' in Component ? new Component($element, config) : new Component($element);
-      } catch (error) {
-        console.log(error);
-      }
-    });
+    createAll(Component, config, $scope);
   });
 }
 
+/**
+ * Create all instances of a specific component on the page
+ *
+ * Uses the `data-module` attribute to find all elements matching the specified
+ * component on the page, creating instances of the component object for each
+ * of them.
+ *
+ * Any component errors will be caught and logged to the console.
+ *
+ * @template {CompatibleClass} T
+ * @param {T} Component - class of the component to create
+ * @param {T["defaults"]} [config] - config for the component
+ * @param {Element|Document} [$scope] - scope of the document to search within
+ * @returns {Array<InstanceType<T>>} - array of instantiated components
+ */
+function createAll(Component, config, $scope = document) {
+  const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
+  return Array.from($elements).map($element => {
+    try {
+      return 'defaults' in Component && typeof config !== 'undefined' ? new Component($element, config) : new Component($element);
+    } catch (error) {
+      console.log(error);
+      return null;
+    }
+  }).filter(Boolean);
+}
+/**
+ * @typedef {{new (...args: any[]): any, defaults?: object, moduleName: string}} CompatibleClass
+ */
 /**
  * Config for all components via `initAll()`
  *
@@ -2389,7 +2411,6 @@ function initAll(config) {
  * @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
  * @property {PasswordInputConfig} [passwordInput] - Password input config
  */
-
 /**
  * Config for individual components
  *
@@ -2404,12 +2425,11 @@ function initAll(config) {
  * @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
  * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
  */
-
 /**
  * Component config keys, e.g. `accordion` and `characterCount`
  *
  * @typedef {keyof Config} ConfigKey
  */
 
-export { Accordion, Button, CharacterCount, Checkboxes, ErrorSummary, ExitThisPage, Header, NotificationBanner, PasswordInput, Radios, SkipLink, Tabs, initAll, version };
+export { Accordion, Button, CharacterCount, Checkboxes, ErrorSummary, ExitThisPage, Header, NotificationBanner, PasswordInput, Radios, SkipLink, Tabs, createAll, initAll, version };
 //# sourceMappingURL=all.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/all.mjs b/packages/govuk-frontend/dist/govuk/all.mjs
index 02cb75e14..e395113b0 100644
--- a/packages/govuk-frontend/dist/govuk/all.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.mjs
@@ -1,81 +1,15 @@
 export { version } from './common/govuk-frontend-version.mjs';
-import { isSupported } from './common/index.mjs';
-import { Accordion } from './components/accordion/accordion.mjs';
-import { Button } from './components/button/button.mjs';
-import { CharacterCount } from './components/character-count/character-count.mjs';
-import { Checkboxes } from './components/checkboxes/checkboxes.mjs';
-import { ErrorSummary } from './components/error-summary/error-summary.mjs';
-import { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs';
-import { Header } from './components/header/header.mjs';
-import { NotificationBanner } from './components/notification-banner/notification-banner.mjs';
-import { PasswordInput } from './components/password-input/password-input.mjs';
-import { Radios } from './components/radios/radios.mjs';
-import { SkipLink } from './components/skip-link/skip-link.mjs';
-import { Tabs } from './components/tabs/tabs.mjs';
-import { SupportError } from './errors/index.mjs';
-
-/**
- * Initialise all components
- *
- * Use the `data-module` attributes to find, instantiate and init all of the
- * components provided as part of GOV.UK Frontend.
- *
- * @param {Config & { scope?: Element }} [config] - Config for all components (with optional scope)
- */
-function initAll(config) {
-  var _config$scope;
-  config = typeof config !== 'undefined' ? config : {};
-  if (!isSupported()) {
-    console.log(new SupportError());
-    return;
-  }
-  const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [SkipLink], [Tabs]];
-  const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
-  components.forEach(([Component, config]) => {
-    const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
-    $elements.forEach($element => {
-      try {
-        'defaults' in Component ? new Component($element, config) : new Component($element);
-      } catch (error) {
-        console.log(error);
-      }
-    });
-  });
-}
-
-/**
- * Config for all components via `initAll()`
- *
- * @typedef {object} Config
- * @property {AccordionConfig} [accordion] - Accordion config
- * @property {ButtonConfig} [button] - Button config
- * @property {CharacterCountConfig} [characterCount] - Character Count config
- * @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
- * @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
- * @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
- * @property {PasswordInputConfig} [passwordInput] - Password input config
- */
-
-/**
- * Config for individual components
- *
- * @typedef {import('./components/accordion/accordion.mjs').AccordionConfig} AccordionConfig
- * @typedef {import('./components/accordion/accordion.mjs').AccordionTranslations} AccordionTranslations
- * @typedef {import('./components/button/button.mjs').ButtonConfig} ButtonConfig
- * @typedef {import('./components/character-count/character-count.mjs').CharacterCountConfig} CharacterCountConfig
- * @typedef {import('./components/character-count/character-count.mjs').CharacterCountTranslations} CharacterCountTranslations
- * @typedef {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} ErrorSummaryConfig
- * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageConfig} ExitThisPageConfig
- * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageTranslations} ExitThisPageTranslations
- * @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
- * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
- */
-
-/**
- * Component config keys, e.g. `accordion` and `characterCount`
- *
- * @typedef {keyof Config} ConfigKey
- */
-
-export { Accordion, Button, CharacterCount, Checkboxes, ErrorSummary, ExitThisPage, Header, NotificationBanner, PasswordInput, Radios, SkipLink, Tabs, initAll };
+export { Accordion } from './components/accordion/accordion.mjs';
+export { Button } from './components/button/button.mjs';
+export { CharacterCount } from './components/character-count/character-count.mjs';
+export { Checkboxes } from './components/checkboxes/checkboxes.mjs';
+export { ErrorSummary } from './components/error-summary/error-summary.mjs';
+export { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs';
+export { Header } from './components/header/header.mjs';
+export { NotificationBanner } from './components/notification-banner/notification-banner.mjs';
+export { PasswordInput } from './components/password-input/password-input.mjs';
+export { Radios } from './components/radios/radios.mjs';
+export { SkipLink } from './components/skip-link/skip-link.mjs';
+export { Tabs } from './components/tabs/tabs.mjs';
+export { createAll, initAll } from './init.mjs';
 //# sourceMappingURL=all.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/init.mjs b/packages/govuk-frontend/dist/govuk/init.mjs
new file mode 100644
index 000000000..f985eaffb
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/init.mjs
@@ -0,0 +1,100 @@
+import { isSupported } from './common/index.mjs';
+import { Accordion } from './components/accordion/accordion.mjs';
+import { Button } from './components/button/button.mjs';
+import { CharacterCount } from './components/character-count/character-count.mjs';
+import { Checkboxes } from './components/checkboxes/checkboxes.mjs';
+import { ErrorSummary } from './components/error-summary/error-summary.mjs';
+import { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs';
+import { Header } from './components/header/header.mjs';
+import { NotificationBanner } from './components/notification-banner/notification-banner.mjs';
+import { PasswordInput } from './components/password-input/password-input.mjs';
+import { Radios } from './components/radios/radios.mjs';
+import { SkipLink } from './components/skip-link/skip-link.mjs';
+import { Tabs } from './components/tabs/tabs.mjs';
+import { SupportError } from './errors/index.mjs';
+
+/**
+ * Initialise all components
+ *
+ * Use the `data-module` attributes to find, instantiate and init all of the
+ * components provided as part of GOV.UK Frontend.
+ *
+ * @param {Config & { scope?: Element }} [config] - Config for all components (with optional scope)
+ */
+function initAll(config) {
+  var _config$scope;
+  config = typeof config !== 'undefined' ? config : {};
+  if (!isSupported()) {
+    console.log(new SupportError());
+    return;
+  }
+  const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [SkipLink], [Tabs]];
+  const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
+  components.forEach(([Component, config]) => {
+    createAll(Component, config, $scope);
+  });
+}
+
+/**
+ * Create all instances of a specific component on the page
+ *
+ * Uses the `data-module` attribute to find all elements matching the specified
+ * component on the page, creating instances of the component object for each
+ * of them.
+ *
+ * Any component errors will be caught and logged to the console.
+ *
+ * @template {CompatibleClass} T
+ * @param {T} Component - class of the component to create
+ * @param {T["defaults"]} [config] - config for the component
+ * @param {Element|Document} [$scope] - scope of the document to search within
+ * @returns {Array<InstanceType<T>>} - array of instantiated components
+ */
+function createAll(Component, config, $scope = document) {
+  const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
+  return Array.from($elements).map($element => {
+    try {
+      return 'defaults' in Component && typeof config !== 'undefined' ? new Component($element, config) : new Component($element);
+    } catch (error) {
+      console.log(error);
+      return null;
+    }
+  }).filter(Boolean);
+}
+/**
+ * @typedef {{new (...args: any[]): any, defaults?: object, moduleName: string}} CompatibleClass
+ */
+/**
+ * Config for all components via `initAll()`
+ *
+ * @typedef {object} Config
+ * @property {AccordionConfig} [accordion] - Accordion config
+ * @property {ButtonConfig} [button] - Button config
+ * @property {CharacterCountConfig} [characterCount] - Character Count config
+ * @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
+ * @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
+ * @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
+ * @property {PasswordInputConfig} [passwordInput] - Password input config
+ */
+/**
+ * Config for individual components
+ *
+ * @typedef {import('./components/accordion/accordion.mjs').AccordionConfig} AccordionConfig
+ * @typedef {import('./components/accordion/accordion.mjs').AccordionTranslations} AccordionTranslations
+ * @typedef {import('./components/button/button.mjs').ButtonConfig} ButtonConfig
+ * @typedef {import('./components/character-count/character-count.mjs').CharacterCountConfig} CharacterCountConfig
+ * @typedef {import('./components/character-count/character-count.mjs').CharacterCountTranslations} CharacterCountTranslations
+ * @typedef {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} ErrorSummaryConfig
+ * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageConfig} ExitThisPageConfig
+ * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageTranslations} ExitThisPageTranslations
+ * @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
+ * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
+ */
+/**
+ * Component config keys, e.g. `accordion` and `characterCount`
+ *
+ * @typedef {keyof Config} ConfigKey
+ */
+
+export { createAll, initAll };
+//# sourceMappingURL=init.mjs.map

Action run for f4667fd

36degrees and others added 7 commits May 10, 2024 11:55
Co-authored-by:  Romaric Pascal <romaric.pascal@digital.cabinet-office.gov.uk>
Co-authored-by:  Owen Jones <owen.jones@digital.cabinet-office.gov.uk>
TypeScript doesn’t really have the concept of passing a class to a function, so we have to fake it by defining a type that is constructable and has the properties we need to be able to instantiate it (a `moduleName` and optionally a set of `defaults` from which we can infer the config type).

This is based on approaches from:

- https://stackoverflow.com/questions/71086547/build-a-function-that-accepts-a-class-in-typescript
- microsoft/TypeScript#17572

Co-authored-by: Owen Jones <owen.jones@digital.cabinet-office.gov.uk>
Co-authored-by: Owen Jones <owen.jones@digital.cabinet-office.gov.uk>
Co-authored-by: Oliver Byford <oliver.byford@digital.cabinet-office.gov.uk>
Co-authored-by: Owen Jones <owen.jones@digital.cabinet-office.gov.uk>
Now that `createAll` returns the component objects,
we can `expect` on their properties rather than use spies on the constructor.

Also added a couple of missing test cases and grouped tests in `describe`.

Co-authored-by: Oliver Byford <oliver.byford@digital.cabinet-office.gov.uk>
Move the `components/global.puppeteer.test.js` file up to `all.puppeteer.test.js`
as it's more related to the `all.mjs` file than the components themselves.

Co-authored-by: Oliver Byford <oliver.byford@digital.cabinet-office.gov.uk>
Co-authored-by: Oliver Byford <oliver.byford@digital.cabinet-office.gov.uk>
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4975 May 10, 2024 10:59 Inactive
@romaricpascal romaricpascal marked this pull request as ready for review May 10, 2024 11:12
Comment on lines +1 to +14
export { version } from './common/govuk-frontend-version.mjs'
export { Accordion } from './components/accordion/accordion.mjs'
export { Button } from './components/button/button.mjs'
export { CharacterCount } from './components/character-count/character-count.mjs'
export { Checkboxes } from './components/checkboxes/checkboxes.mjs'
export { ErrorSummary } from './components/error-summary/error-summary.mjs'
export { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs'
export { Header } from './components/header/header.mjs'
export { NotificationBanner } from './components/notification-banner/notification-banner.mjs'
export { PasswordInput } from './components/password-input/password-input.mjs'
export { Radios } from './components/radios/radios.mjs'
export { SkipLink } from './components/skip-link/skip-link.mjs'
export { Tabs } from './components/tabs/tabs.mjs'
export { initAll, createAll } from './init.mjs'
Copy link
Member Author

Choose a reason for hiding this comment

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

note With the removal of initAll from all.mjs, all the file does is re-exporting the difference pieces of our library that constitute our public API, so we can switch to the export { NAMED_IMPORT } from 'module' syntax rather than have a bunch of imports followed by an export.

Comment on lines +184 to +196
export { Accordion } from './components/accordion/accordion.mjs';
export { Button } from './components/button/button.mjs';
export { CharacterCount } from './components/character-count/character-count.mjs';
export { Checkboxes } from './components/checkboxes/checkboxes.mjs';
export { ErrorSummary } from './components/error-summary/error-summary.mjs';
export { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs';
export { Header } from './components/header/header.mjs';
export { NotificationBanner } from './components/notification-banner/notification-banner.mjs';
export { PasswordInput } from './components/password-input/password-input.mjs';
export { Radios } from './components/radios/radios.mjs';
export { SkipLink } from './components/skip-link/skip-link.mjs';
export { Tabs } from './components/tabs/tabs.mjs';
export { createAll, initAll } from './init.mjs';
Copy link
Member Author

Choose a reason for hiding this comment

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

note This change is unrelated to our move to using export in all.mjs. Even keeping imports + a final export, Rollup would rewrite to the export { NAMED_EXPORT } from 'module' syntax.

@@ -126,7 +126,7 @@ describe('dist/', () => {

it('should contain relative paths to sources', () => {
expect(sourcemap.sources).toContain(
'../packages/govuk-frontend/src/govuk/all.mjs'
'../packages/govuk-frontend/src/govuk/init.mjs'
Copy link
Member Author

Choose a reason for hiding this comment

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

note sourcemap.sources lists many other modules, which was already the case alongside all.mjs, so we can just swap to another one of our files being imported.

@36degrees 36degrees merged commit 6c9d8d7 into main May 10, 2024
49 checks passed
@36degrees 36degrees deleted the easier-js-init branch May 10, 2024 12:18
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.

Move responsibilities from initAll to the components
3 participants