Skip to content

Commit

Permalink
Add errorCallback to createAll
Browse files Browse the repository at this point in the history
- 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.
- New tests added for `onError` callback and `createAllOptions`.
  • Loading branch information
patrickpatrickpatrick committed Aug 27, 2024
1 parent e944196 commit 15daa86
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 5 deletions.
86 changes: 85 additions & 1 deletion packages/govuk-frontend/src/govuk/init.jsdom.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ describe('createAll', () => {
})
})

describe('when a $scope is passed', () => {
describe('when a $scope is passed as third parameter', () => {
it('only initialises components within that scope', () => {
document.body.innerHTML = `
<div data-module="mock-component"></div>
Expand All @@ -295,6 +295,28 @@ describe('createAll', () => {
document.querySelector('.my-scope [data-module="mock-component"]')
])
})

it('only initialises components within that scope if scope passed as options attribute', () => {
document.body.innerHTML = `
<div data-module="mock-component"></div>
<div class="not-in-scope">
<div data-module="mock-component"></div>
</div>'
<div class="my-scope">
<div data-module="mock-component"></div>
</div>`

const result = createAll(MockComponent, undefined, {
onError: (e, x) => {},
scope: document.querySelector('.my-scope')
})

expect(result).toStrictEqual([expect.any(MockComponent)])

expect(result[0].args).toStrictEqual([
document.querySelector('.my-scope [data-module="mock-component"]')
])
})
})

describe('when components throw errors', () => {
Expand All @@ -307,6 +329,68 @@ describe('createAll', () => {
}
}

it('executes callback if specified as part of options object', () => {
document.body.innerHTML = `<div data-module="mock-component" data-boom></div>`

const errorCallback = jest.fn((error, context) => {
console.log(error)
console.log(context)
})

// Silence warnings in test output, and allow us to 'expect' them
jest.spyOn(global.console, 'log').mockImplementation()

expect(() => {
createAll(
MockComponentThatErrors,
{ attribute: 'random' },
{ onError: errorCallback }
)
}).not.toThrow()

expect(errorCallback).toHaveBeenCalled()

expect(global.console.log).toHaveBeenCalledWith(expect.any(Error))
expect(global.console.log).toHaveBeenCalledWith(
expect.objectContaining({
component: MockComponentThatErrors,
config: { attribute: 'random' },
element: document.querySelector('[data-module="mock-component"]')
})
)
})

it('executes callback if specified as function', () => {
document.body.innerHTML = `<div data-module="mock-component" data-boom></div>`

const errorCallback = jest.fn((error, context) => {
console.log(error)
console.log(context)
})

// Silence warnings in test output, and allow us to 'expect' them
jest.spyOn(global.console, 'log').mockImplementation()

expect(() => {
createAll(
MockComponentThatErrors,
{ attribute: 'random' },
errorCallback
)
}).not.toThrow()

expect(errorCallback).toHaveBeenCalled()

expect(global.console.log).toHaveBeenCalledWith(expect.any(Error))
expect(global.console.log).toHaveBeenCalledWith(
expect.objectContaining({
component: MockComponentThatErrors,
config: { attribute: 'random' },
element: document.querySelector('[data-module="mock-component"]')
})
)
})

it('catches errors thrown by components and logs them to the console', () => {
document.body.innerHTML = `<div data-module="mock-component" data-boom></div>`

Expand Down
58 changes: 54 additions & 4 deletions packages/govuk-frontend/src/govuk/init.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ function initAll(config) {
})
}

/**
* @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
*/

/**
* Create all instances of a specific component on the page
*
Expand All @@ -65,11 +79,30 @@ 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 /** @type {Element | Document} */ $scope = document
let /** @type {onErrorCallback<Component> | undefined} */ onError

if (typeof createAllOptions === 'object') {
$scope =
/** @type {createAllOptions<Component>} */ (createAllOptions).scope ??
$scope
onError = /** @type {createAllOptions<Component>} */ (createAllOptions)
.onError
}

if (typeof createAllOptions === 'function') {
onError = createAllOptions
}

if (createAllOptions instanceof HTMLElement) {
$scope = createAllOptions
}

const $elements = $scope.querySelectorAll(
`[data-module="${Component.moduleName}"]`
)
Expand All @@ -90,7 +123,16 @@ function createAll(Component, config, $scope = document) {
? new Component($element, config)
: new Component($element)
} catch (error) {
console.log(error)
if (onError) {
onError(/** @type {Error} */ (error), {
element: $element,
component: Component,
config
})
} else {
console.log(error)
}

return null
}
})
Expand Down Expand Up @@ -144,3 +186,11 @@ export { initAll, createAll }
*
* @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
*/

0 comments on commit 15daa86

Please sign in to comment.