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 onError to createAll #5252

Merged
merged 1 commit into from
Aug 28, 2024
Merged

Conversation

patrickpatrickpatrick
Copy link
Contributor

@patrickpatrickpatrick patrickpatrickpatrick commented Aug 21, 2024

What

  • Add support for onError callback in createAll which is called if error occurs on component initialisation.
  • New parameter for createAll, createAllOptions which allows user to specify a scope, onError or an object that contains both.
  • isSupported will execute onError callback if specified
  • New tests added for onError callback and createAllOptions.

Why

Provide better support for defining custom components by allowing users to execute custom callback if the components fail to initialise.

Fixes #5212

Copy link

github-actions bot commented Aug 21, 2024

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 112.61 KiB
dist/govuk-frontend-development.min.js 42.12 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 89.55 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 84.1 KiB
packages/govuk-frontend/dist/govuk/all.mjs 1.01 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 359 B
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 112.6 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 42.11 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 6.59 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 80.49 KiB 40.06 KiB
accordion.mjs 23.83 KiB 12.39 KiB
button.mjs 6.31 KiB 2.69 KiB
character-count.mjs 22.73 KiB 9.92 KiB
checkboxes.mjs 6.16 KiB 2.83 KiB
error-summary.mjs 8.22 KiB 3.46 KiB
exit-this-page.mjs 17.43 KiB 9.26 KiB
header.mjs 4.79 KiB 2.6 KiB
notification-banner.mjs 6.59 KiB 2.62 KiB
password-input.mjs 15.48 KiB 7.25 KiB
radios.mjs 5.16 KiB 2.38 KiB
skip-link.mjs 4.72 KiB 2.18 KiB
tabs.mjs 10.38 KiB 6.06 KiB

View stats and visualisations on the review app


Action run for b771200

Copy link

github-actions bot commented Aug 21, 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 69492a902..dd7574fa1 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -1104,15 +1104,26 @@ function initAll(e) {
     }))
 }
 
-function createAll(e, t, n = document) {
-    const i = n.querySelectorAll(`[data-module="${e.moduleName}"]`);
-    return isSupported() ? Array.from(i).map((n => {
+function createAll(e, t, n) {
+    let i, s = document;
+    var o;
+    "object" == typeof n && (s = null != (o = n.scope) ? o : s, i = n.onError);
+    "function" == typeof n && (i = n), n instanceof HTMLElement && (s = n);
+    const r = s.querySelectorAll(`[data-module="${e.moduleName}"]`);
+    return isSupported() ? Array.from(r).map((n => {
         try {
             return "defaults" in e && void 0 !== t ? new e(n, t) : new e(n)
-        } catch (i) {
-            return console.log(i), null
+        } catch (s) {
+            return i && s instanceof Error ? i(s, {
+                element: n,
+                component: e,
+                config: t
+            }) : console.log(s), null
         }
-    })).filter(Boolean) : (console.log(new SupportError), [])
+    })).filter(Boolean) : (i ? i(new SupportError, {
+        component: e,
+        config: t
+    }) : console.log(new SupportError), [])
 }
 Tabs.moduleName = "govuk-tabs";
 export {

Action run for b771200

Copy link

github-actions bot commented Aug 21, 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 3b86ffe36..057ec0db2 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -2383,21 +2383,50 @@
    *
    * @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
+   * @param {T["defaults"]} [config] - Config supplied to component
+   * @param {OnErrorCallback<T> | Element | Document | CreateAllOptions<T> } [createAllOptions] - options for createAll including scope of the document to search within and callback function if error throw by component on init
    * @returns {Array<InstanceType<T>>} - array of instantiated components
    */
-  function createAll(Component, config, $scope = document) {
+  function createAll(Component, config, createAllOptions) {
+    let $scope = document;
+    let onError;
+    if (typeof createAllOptions === 'object') {
+      var _createAllOptions$sco;
+      createAllOptions = createAllOptions;
+      $scope = (_createAllOptions$sco = createAllOptions.scope) != null ? _createAllOptions$sco : $scope;
+      onError = createAllOptions.onError;
+    }
+    if (typeof createAllOptions === 'function') {
+      onError = createAllOptions;
+    }
+    if (createAllOptions instanceof HTMLElement) {
+      $scope = createAllOptions;
+    }
     const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
     if (!isSupported()) {
-      console.log(new SupportError());
+      if (onError) {
+        onError(new SupportError(), {
+          component: Component,
+          config
+        });
+      } else {
+        console.log(new SupportError());
+      }
       return [];
     }
     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);
+        if (onError && error instanceof Error) {
+          onError(error, {
+            element: $element,
+            component: Component,
+            config
+          });
+        } else {
+          console.log(error);
+        }
         return null;
       }
     }).filter(Boolean);
@@ -2436,6 +2465,25 @@
    *
    * @typedef {keyof Config} ConfigKey
    */
+  /**
+   * @template {CompatibleClass} T
+   * @typedef {object} ErrorContext
+   * @property {Element} [element] - Element used for component module initialisation
+   * @property {T} component - Class of component
+   * @property {T["defaults"]} config - Config supplied to component
+   */
+  /**
+   * @template {CompatibleClass} T
+   * @callback OnErrorCallback
+   * @param {Error} error - Thrown error
+   * @param {ErrorContext<T>} context - Object containing the element, component class and configuration
+   */
+  /**
+   * @template {CompatibleClass} T
+   * @typedef {object} CreateAllOptions
+   * @property {Element | Document} [scope] - scope of the document to search within
+   * @property {OnErrorCallback<T>} [onError] - callback function if error throw by component on init
+   */
 
   exports.Accordion = Accordion;
   exports.Button = Button;
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index 8725b363d..15a7feada 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -2377,21 +2377,50 @@ function initAll(config) {
  *
  * @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
+ * @param {T["defaults"]} [config] - Config supplied to component
+ * @param {OnErrorCallback<T> | Element | Document | CreateAllOptions<T> } [createAllOptions] - options for createAll including scope of the document to search within and callback function if error throw by component on init
  * @returns {Array<InstanceType<T>>} - array of instantiated components
  */
-function createAll(Component, config, $scope = document) {
+function createAll(Component, config, createAllOptions) {
+  let $scope = document;
+  let onError;
+  if (typeof createAllOptions === 'object') {
+    var _createAllOptions$sco;
+    createAllOptions = createAllOptions;
+    $scope = (_createAllOptions$sco = createAllOptions.scope) != null ? _createAllOptions$sco : $scope;
+    onError = createAllOptions.onError;
+  }
+  if (typeof createAllOptions === 'function') {
+    onError = createAllOptions;
+  }
+  if (createAllOptions instanceof HTMLElement) {
+    $scope = createAllOptions;
+  }
   const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
   if (!isSupported()) {
-    console.log(new SupportError());
+    if (onError) {
+      onError(new SupportError(), {
+        component: Component,
+        config
+      });
+    } else {
+      console.log(new SupportError());
+    }
     return [];
   }
   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);
+      if (onError && error instanceof Error) {
+        onError(error, {
+          element: $element,
+          component: Component,
+          config
+        });
+      } else {
+        console.log(error);
+      }
       return null;
     }
   }).filter(Boolean);
@@ -2430,6 +2459,25 @@ function createAll(Component, config, $scope = document) {
  *
  * @typedef {keyof Config} ConfigKey
  */
+/**
+ * @template {CompatibleClass} T
+ * @typedef {object} ErrorContext
+ * @property {Element} [element] - Element used for component module initialisation
+ * @property {T} component - Class of component
+ * @property {T["defaults"]} config - Config supplied to component
+ */
+/**
+ * @template {CompatibleClass} T
+ * @callback OnErrorCallback
+ * @param {Error} error - Thrown error
+ * @param {ErrorContext<T>} context - Object containing the element, component class and configuration
+ */
+/**
+ * @template {CompatibleClass} T
+ * @typedef {object} CreateAllOptions
+ * @property {Element | Document} [scope] - scope of the document to search within
+ * @property {OnErrorCallback<T>} [onError] - callback function if error throw by component on init
+ */
 
 export { Accordion, Button, CharacterCount, Checkboxes, ErrorSummary, ExitThisPage, Header, NotificationBanner, PasswordInput, Radios, SkipLink, Tabs, createAll, initAll, isSupported, version };
 //# sourceMappingURL=all.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/init.mjs b/packages/govuk-frontend/dist/govuk/init.mjs
index c8b4ab490..2de2329d5 100644
--- a/packages/govuk-frontend/dist/govuk/init.mjs
+++ b/packages/govuk-frontend/dist/govuk/init.mjs
@@ -46,21 +46,50 @@ function initAll(config) {
  *
  * @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
+ * @param {T["defaults"]} [config] - Config supplied to component
+ * @param {OnErrorCallback<T> | Element | Document | CreateAllOptions<T> } [createAllOptions] - options for createAll including scope of the document to search within and callback function if error throw by component on init
  * @returns {Array<InstanceType<T>>} - array of instantiated components
  */
-function createAll(Component, config, $scope = document) {
+function createAll(Component, config, createAllOptions) {
+  let $scope = document;
+  let onError;
+  if (typeof createAllOptions === 'object') {
+    var _createAllOptions$sco;
+    createAllOptions = createAllOptions;
+    $scope = (_createAllOptions$sco = createAllOptions.scope) != null ? _createAllOptions$sco : $scope;
+    onError = createAllOptions.onError;
+  }
+  if (typeof createAllOptions === 'function') {
+    onError = createAllOptions;
+  }
+  if (createAllOptions instanceof HTMLElement) {
+    $scope = createAllOptions;
+  }
   const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
   if (!isSupported()) {
-    console.log(new SupportError());
+    if (onError) {
+      onError(new SupportError(), {
+        component: Component,
+        config
+      });
+    } else {
+      console.log(new SupportError());
+    }
     return [];
   }
   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);
+      if (onError && error instanceof Error) {
+        onError(error, {
+          element: $element,
+          component: Component,
+          config
+        });
+      } else {
+        console.log(error);
+      }
       return null;
     }
   }).filter(Boolean);
@@ -99,6 +128,25 @@ function createAll(Component, config, $scope = document) {
  *
  * @typedef {keyof Config} ConfigKey
  */
+/**
+ * @template {CompatibleClass} T
+ * @typedef {object} ErrorContext
+ * @property {Element} [element] - Element used for component module initialisation
+ * @property {T} component - Class of component
+ * @property {T["defaults"]} config - Config supplied to component
+ */
+/**
+ * @template {CompatibleClass} T
+ * @callback OnErrorCallback
+ * @param {Error} error - Thrown error
+ * @param {ErrorContext<T>} context - Object containing the element, component class and configuration
+ */
+/**
+ * @template {CompatibleClass} T
+ * @typedef {object} CreateAllOptions
+ * @property {Element | Document} [scope] - scope of the document to search within
+ * @property {OnErrorCallback<T>} [onError] - callback function if error throw by component on init
+ */
 
 export { createAll, initAll };
 //# sourceMappingURL=init.mjs.map

Action run for b771200

* @returns {Array<InstanceType<T>>} - array of instantiated components
*/
function createAll(Component, config, $scope = document) {
function createAll(Component, config, $scope = document, errorCallback) {
Copy link
Member

Choose a reason for hiding this comment

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

suggestion As discussed during our call, let's try to limit the number of arguments to 3 and regroup the scope and error callback in a param that can be either:

  • an Element|Document representing the scope
  • a Function representing the error callback
  • an {scope: Element|Document, errorCallback: Function} if both are needed.

Copy link
Member

Choose a reason for hiding this comment

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

Let's also align with the on<NAME> format that's common in the DOM API.

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.

Looking neat! 🙌🏻 Just little remarks about undocumented conventions in the rest of the files for how we do type annotations for TypeScript (which we should probably start documenting in the docs).

Comment on lines 91 to 95
$scope =
/** @type {createAllOptions<Component>} */ (createAllOptions).scope ??
$scope
onError = /** @type {createAllOptions<Component>} */ (createAllOptions)
.onError
Copy link
Member

Choose a reason for hiding this comment

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

suggestion Would it work to do something like the following to put the casting in its own line and make things a little more readable by separating the assignments of the options from the casting?

Suggested change
$scope =
/** @type {createAllOptions<Component>} */ (createAllOptions).scope ??
$scope
onError = /** @type {createAllOptions<Component>} */ (createAllOptions)
.onError
createAllOptions = /** @type {createAllOptions<Component>} */ createAllOptions
$scope = createAllOptions.scope ?? $scope
onError = createAllOptions.onError

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense, I have disable eslint rule "no-self-assign" but it does look neater

Comment on lines 126 to 127
if (onError) {
onError(/** @type {Error} */ (error), {
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've usually ran typeof/instanceof calls rather than casting to make typescript aware of types. Would the following work for that?

Suggested change
if (onError) {
onError(/** @type {Error} */ (error), {
if (onError && error instanceof Error) {
onError(error, {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that seems to work here :)

Comment on lines 57 to 69
/**
* @template {CompatibleClass} T
* @callback onErrorCallback
* @param {Error} error - Thrown error
* @param {ErrorContext<T>} context - Object containing the element, component class and configuration
*/

/**
* @template {CompatibleClass} T
* @typedef {object} createAllOptions
* @property {Element | Document} [scope] - scope of the document to search within
* @property {onErrorCallback<T>} [onError] - callback function if error throw by component on init
*/
Copy link
Member

Choose a reason for hiding this comment

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

question We've tended to put our types at the bottom of the files across the project, is there something forcing these to be early on? If not, would be good to align with the rest of the files for consistency.

suggestion We've generally been PascalCasing the types rather than camelCasing them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mostly just found it easier to keep track of them being nearer where I was typing but I'll move them down again.

@romaricpascal romaricpascal changed the base branch from main to public-js-api August 27, 2024 11:02
@romaricpascal
Copy link
Member

romaricpascal commented Aug 27, 2024

@patrickpatrickpatrick I've changed the PR base to public-js-api on GitHub so we don't merge on main accidentally, but I think it'll need a little rebase to get the check for isSupported inside createAll.

What are your thoughts on whether the check should also trigger a call to onError, as well? For consistency, it feels like we should trigger a call as well, and if people initialise multiple components with multiple createAll calls likely receiving the same error handler, they can use isSupported to not call createAll and avoid the noise 🤔

- Add support for `onError` callback in `createAll` which is called
if error occurs on component initialisation.
- New parameter for `createAll`, `createAllOptions` which allows user to
specify a `scope`, `onError` or an object that contains both.
- `isSupported` will execute `onError` callback if specified
- New tests added for `onError` callback and `createAllOptions`.
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5252 August 27, 2024 13:47 Inactive
@patrickpatrickpatrick
Copy link
Contributor Author

@patrickpatrickpatrick I've changed the PR base to public-js-api on GitHub so we don't merge on main accidentally, but I think it'll need a little rebase to get the check for isSupported inside createAll.

What are your thoughts on whether the check should also trigger a call to onError, as well? For consistency, it feels like we should trigger a call as well, and if people initialise multiple components with multiple createAll calls likely receiving the same error handler, they can use isSupported to not call createAll and avoid the noise 🤔

That makes sense, have added this!

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 making the updates 🙌🏻

@patrickpatrickpatrick patrickpatrickpatrick merged commit ecc74dc into public-js-api Aug 28, 2024
49 checks passed
@patrickpatrickpatrick patrickpatrickpatrick deleted the create-all-error branch August 28, 2024 09:12
@romaricpascal romaricpascal changed the title Add errorCallback to createAll Add onError to createAll Sep 24, 2024
@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.

Add callback to respond to errors when a component does not initialise in createAll
3 participants