From 346885e9aa0c902dbffc8c851fe78e324be18d8a Mon Sep 17 00:00:00 2001 From: Romaric Pascal Date: Tue, 23 Apr 2024 17:30:49 +0100 Subject: [PATCH] Draft example API of how we could initialise components First things first, this code has not even been run, it's only a draft of an API I think could be nice for initialising components, accounting for the nuances found in the different components of the Design System. - Some components run on a specific element, some not. So the API need to allow both. - When initialised on a specific element, the module name could be a static property of the component, however, it's probably useful to let it be set explicitely - Further selection, like finding the for the Copy, should probably be part of the component's responsibility, rather than the initialisation code - Similarly, if a component needs to be initialised only once, this could be part of the component's responsibility, rather thatn the initialisation loop. TypeScript might get in the way of having something so dynamic. Especially, I'm not 100% sure how we can note that a function accepts a 'class' as parameter. Seems it's a bit tricky based on this: https://github.com/Microsoft/TypeScript/issues/17572 --- src/javascripts/application.mjs | 117 ++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/src/javascripts/application.mjs b/src/javascripts/application.mjs index 35703ad0c6..88ec3c0ab0 100644 --- a/src/javascripts/application.mjs +++ b/src/javascripts/application.mjs @@ -20,58 +20,85 @@ import AppTabs from './components/tabs.mjs' // Initialise GOV.UK Frontend initAll() -// Initialise cookie banner -const $cookieBanner = document.querySelector( - '[data-module="govuk-cookie-banner"]' -) -if ($cookieBanner) { - new CookieBanner($cookieBanner) -} +initModules([ + [CookieBanner, 'govuk-cookie-banner'], // Why is this not 'app-cookie-banner' ? + [Analytics, hasUserConsentedToAnalytics], + [Example, 'app-example-frame'], // Why do we not match the module name ? + [AppTabs, 'app-tabs'], + OptionsTable, + [Copy, 'app-copy', {selector: 'pre'}], // Should probably be the responsibility of the Copy module to lookup the `
`
+  Navigation,
+  [Search, 'app-search', {single: true}], // Could error if there's more than one
+  [BackToTop, 'app-back-to-top', {single: true}],
+  [CookiesPage, 'app-cookies-page', {single: true}]
+])
 
-// Initialise analytics if consent is given
-const userConsent = getConsentCookie()
-if (userConsent && isValidConsentCookie(userConsent) && userConsent.analytics) {
-  Analytics()
+/**
+ * Checks if user has consented to run analytics
+ *
+ * @returns {boolean}
+ */
+function hasUserConsentedToAnalytics() {
+  const userConsent = getConsentCookie();
+  return (
+    userConsent && isValidConsentCookie(userConsent) && userConsent.analytics
+  )
 }
 
-// Initialise example frames
-const $examples = document.querySelectorAll('[data-module="app-example-frame"]')
-$examples.forEach(($example) => {
-  new Example($example)
-})
-
-// Initialise tabs
-const $tabs = document.querySelectorAll('[data-module="app-tabs"]')
-$tabs.forEach(($tabs) => {
-  new AppTabs($tabs)
-})
-
-// Do this after initialising tabs
-new OptionsTable()
+/**
+ * Initialises the given list of modules, according to the provided options
+ *
+ * @param {Array} modulesToInitialise
+ */
+function initModules(modulesToInitialise) {
+  for(const moduleToInitialise of modulesToInitialise) {
+    if (Array.isArray(moduleToInitialise)) {
+      initModule(...moduleToInitialise)
+    } else {
+      initModules(moduleToInitialise)
+    }
+  }
+}
 
-// Add copy to clipboard to code blocks inside tab containers
-const $codeBlocks = document.querySelectorAll('[data-module="app-copy"] pre')
-$codeBlocks.forEach(($codeBlock) => {
-  new Copy($codeBlock)
-})
 
-// Initialise mobile navigation
-new Navigation(document)
 
-// Initialise search
-const $searchContainer = document.querySelector('[data-module="app-search"]')
-if ($searchContainer) {
-  new Search($searchContainer)
-}
+/**
+ *
+ * @param {Function} ModuleClass
+ * @param {string} moduleName
+ * @param {{single?: boolean, selector?: string}} options
+ */
+function initModule(ModuleClass, moduleName, options) {
+  if (!moduleName) {
+    runInitialisation(ModuleClass);
+  }
 
-// Initialise back to top
-const $backToTop = document.querySelector('[data-module="app-back-to-top"]')
-if ($backToTop) {
-  new BackToTop($backToTop)
+  const elements = document.querySelectorAll(`[data-module=${moduleName}]`);
+  for(const element of elements) {
+    if (options.selector) {
+      runInitialisation(ModuleClass, element.querySelector(options.selector));
+    } else {
+      runInitialisation(ModuleClass, element)
+    }
+  }
 }
 
-// Initialise cookie page
-const $cookiesPage = document.querySelector('[data-module="app-cookies-page"]')
-if ($cookiesPage) {
-  new CookiesPage($cookiesPage)
+/**
+ * Initialises the given Module, passing it the given `element`
+ * if provided, logging errors if they happen
+ *
+ * @param {Function} ModuleClass
+ * @param {Element} [element]
+ * @returns {object} The initialised instance of the module
+ */
+function runInitialisation(ModuleClass, element) {
+  try {
+    if (element) {
+      return new ModuleClass(element)
+    } else {
+      return new ModuleClass()
+    }
+  } catch (error) {
+    console.error(error);
+  }
 }