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

move the component #5515

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/govuk-frontend/src/govuk/component.jsdom.test.mjs
Original file line number Diff line number Diff line change
@@ -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'
)
})
})
})
})
113 changes: 113 additions & 0 deletions packages/govuk-frontend/src/govuk/component.mjs
Original file line number Diff line number Diff line change
@@ -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
*/
114 changes: 1 addition & 113 deletions packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs
Original file line number Diff line number Diff line change
@@ -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'
Loading