From 2577602839a47c04fe77c97451a0ab6690fa6820 Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Mon, 18 Nov 2024 14:42:10 +0000 Subject: [PATCH] move the component --- .../src/govuk/component.jsdom.test.mjs | 45 +++++++ .../govuk-frontend/src/govuk/component.mjs | 113 +++++++++++++++++ .../src/govuk/govuk-frontend-component.mjs | 114 +----------------- 3 files changed, 159 insertions(+), 113 deletions(-) create mode 100644 packages/govuk-frontend/src/govuk/component.jsdom.test.mjs create mode 100644 packages/govuk-frontend/src/govuk/component.mjs diff --git a/packages/govuk-frontend/src/govuk/component.jsdom.test.mjs b/packages/govuk-frontend/src/govuk/component.jsdom.test.mjs new file mode 100644 index 0000000000..491981373a --- /dev/null +++ b/packages/govuk-frontend/src/govuk/component.jsdom.test.mjs @@ -0,0 +1,45 @@ +import { SupportError } from './errors/index.mjs' +import { GOVUKFrontendComponent } from './govuk-frontend-component.mjs' + +describe('GOVUKFrontendComponent', () => { + describe('checkSupport()', () => { + beforeEach(() => { + // Jest does not tidy the JSDOM document between tests + // so we need to take care of that ourselves + document.documentElement.innerHTML = '' + }) + + describe('default implementation', () => { + class ServiceComponent extends GOVUKFrontendComponent { + static moduleName = 'app-service-component' + } + + it('Makes initialisation throw if GOV.UK Frontend is not supported', () => { + expect(() => new ServiceComponent(document.body)).toThrow(SupportError) + }) + + it('Allows initialisation if GOV.UK Frontend is supported', () => { + document.body.classList.add('govuk-frontend-supported') + + expect(() => new ServiceComponent(document.body)).not.toThrow() + }) + }) + + describe('when overriden', () => { + it('Allows child classes to define their own condition for support', () => { + class ServiceComponent extends GOVUKFrontendComponent { + static moduleName = 'app-service-component' + + static checkSupport() { + throw new Error('Custom error') + } + } + + // Use the message rather than the class as `SupportError` extends `Error` + expect(() => new ServiceComponent(document.body)).toThrow( + 'Custom error' + ) + }) + }) + }) +}) diff --git a/packages/govuk-frontend/src/govuk/component.mjs b/packages/govuk-frontend/src/govuk/component.mjs new file mode 100644 index 0000000000..07f0aac274 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/component.mjs @@ -0,0 +1,113 @@ +import { isInitialised, isSupported } from './common/index.mjs' +import { ElementError, InitError, SupportError } from './errors/index.mjs' + +/** + * Base Component class + * + * Centralises the behaviours shared by our components + * + * @virtual + * @template {Element} [RootElementType=HTMLElement] + */ +export class Component { + /** + * @type {typeof Element} + */ + static elementType = HTMLElement + + // allows Typescript user to work around the lack of types + // in GOVUKFrontend package, Typescript is not aware of $root + // in components that extend GOVUKFrontendComponent + /** + * Returns the root element of the component + * + * @protected + * @returns {RootElementType} - the root element of component + */ + get $root() { + return this._$root + } + + /** + * @protected + * @type {RootElementType} + */ + _$root + + /** + * Constructs a new component, validating that GOV.UK Frontend is supported + * + * @internal + * @param {Element | null} [$root] - HTML element to use for component + */ + constructor($root) { + const childConstructor = /** @type {ChildClassConstructor} */ ( + this.constructor + ) + + // TypeScript does not enforce that inheriting classes will define a `moduleName` + // (even if we add a `@virtual` `static moduleName` property to this class). + // While we trust users to do this correctly, we do a little check to provide them + // a helpful error message. + // + // After this, we'll be sure that `childConstructor` has a `moduleName` + // as expected of the `ChildClassConstructor` we've cast `this.constructor` to. + if (typeof childConstructor.moduleName !== 'string') { + throw new InitError(`\`moduleName\` not defined in component`) + } + + if (!($root instanceof childConstructor.elementType)) { + throw new ElementError({ + element: $root, + component: childConstructor, + identifier: 'Root element (`$root`)', + expectedType: childConstructor.elementType.name + }) + } else { + this._$root = /** @type {RootElementType} */ ($root) + } + + childConstructor.checkSupport() + + this.checkInitialised() + + const moduleName = childConstructor.moduleName + + this.$root.setAttribute(`data-${moduleName}-init`, '') + } + + /** + * Validates whether component is already initialised + * + * @private + * @throws {InitError} when component is already initialised + */ + checkInitialised() { + const constructor = /** @type {ChildClassConstructor} */ (this.constructor) + const moduleName = constructor.moduleName + + if (moduleName && isInitialised(this.$root, moduleName)) { + throw new InitError(constructor) + } + } + + /** + * Validates whether components are supported + * + * @throws {SupportError} when the components are not supported + */ + static checkSupport() { + if (!isSupported()) { + throw new SupportError() + } + } +} + +/** + * @typedef ChildClass + * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component + */ + +/** + * @typedef {typeof Component & ChildClass} ChildClassConstructor + */ diff --git a/packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs b/packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs index 2e389ddd9a..0abfd6c6f5 100644 --- a/packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs +++ b/packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs @@ -1,113 +1 @@ -import { isInitialised, isSupported } from './common/index.mjs' -import { ElementError, InitError, SupportError } from './errors/index.mjs' - -/** - * Base Component class - * - * Centralises the behaviours shared by our components - * - * @virtual - * @template {Element} [RootElementType=HTMLElement] - */ -export class GOVUKFrontendComponent { - /** - * @type {typeof Element} - */ - static elementType = HTMLElement - - // allows Typescript user to work around the lack of types - // in GOVUKFrontend package, Typescript is not aware of $root - // in components that extend GOVUKFrontendComponent - /** - * Returns the root element of the component - * - * @protected - * @returns {RootElementType} - the root element of component - */ - get $root() { - return this._$root - } - - /** - * @protected - * @type {RootElementType} - */ - _$root - - /** - * Constructs a new component, validating that GOV.UK Frontend is supported - * - * @internal - * @param {Element | null} [$root] - HTML element to use for component - */ - constructor($root) { - const childConstructor = /** @type {ChildClassConstructor} */ ( - this.constructor - ) - - // TypeScript does not enforce that inheriting classes will define a `moduleName` - // (even if we add a `@virtual` `static moduleName` property to this class). - // While we trust users to do this correctly, we do a little check to provide them - // a helpful error message. - // - // After this, we'll be sure that `childConstructor` has a `moduleName` - // as expected of the `ChildClassConstructor` we've cast `this.constructor` to. - if (typeof childConstructor.moduleName !== 'string') { - throw new InitError(`\`moduleName\` not defined in component`) - } - - if (!($root instanceof childConstructor.elementType)) { - throw new ElementError({ - element: $root, - component: childConstructor, - identifier: 'Root element (`$root`)', - expectedType: childConstructor.elementType.name - }) - } else { - this._$root = /** @type {RootElementType} */ ($root) - } - - childConstructor.checkSupport() - - this.checkInitialised() - - const moduleName = childConstructor.moduleName - - this.$root.setAttribute(`data-${moduleName}-init`, '') - } - - /** - * Validates whether component is already initialised - * - * @private - * @throws {InitError} when component is already initialised - */ - checkInitialised() { - const constructor = /** @type {ChildClassConstructor} */ (this.constructor) - const moduleName = constructor.moduleName - - if (moduleName && isInitialised(this.$root, moduleName)) { - throw new InitError(constructor) - } - } - - /** - * Validates whether components are supported - * - * @throws {SupportError} when the components are not supported - */ - static checkSupport() { - if (!isSupported()) { - throw new SupportError() - } - } -} - -/** - * @typedef ChildClass - * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component - */ - -/** - * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor - */ +export { Component as GOVUKFrontendComponent } from './component.mjs'