Skip to content

Commit

Permalink
Add $module + element instanceof checks
Browse files Browse the repository at this point in the history
Review component JSDoc return types and HTML element checks, update coding standards
  • Loading branch information
colinrotherham committed Dec 15, 2022
1 parent 7a68cde commit a9512f8
Show file tree
Hide file tree
Showing 15 changed files with 460 additions and 158 deletions.
105 changes: 68 additions & 37 deletions docs/contributing/coding-standards/js.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,54 @@
JavaScript files have the same name as the component's folder name. Test files have a `.test` suffix placed before the file extension.

```
checkboxes
├── checkboxes.mjs
└── checkboxes.test.js
component
├── component.mjs
└── component.test.js
```

## Skeleton

```js
import { nodeListForEach } from '../vendor/common.mjs'
import '../../vendor/polyfills/Element.mjs'

function Checkboxes ($module) {
// code goes here
/**
* Component name
*
* @class
* @param {HTMLElement | Element} $module - HTML element to use for component
* @this {Example}
*/
function Example ($module) {
if (!($module instanceof HTMLElement)) {
// Return instance for method chaining
// using `new Example($module).init()`
return this
}

this.$module = $module

// Code goes here
}

Checkboxes.prototype.init = function () {
// code goes here
/**
* Initialise component
*
* @returns {Example} Example component
*/
Example.prototype.init = function () {
// Check that required elements are present
if (!this.$module) {
return this
}

// Code goes here

// Return instance for assignment
// `var myExample = new Example($module).init()`
return this
}

export default Checkboxes
export default Example
```

## Use data attributes to initialise component JavaScript
Expand All @@ -48,15 +77,15 @@ Use `/** ... */` for multi-line comments. Include a description, and specify typ

```js
/**
* Get the nearest ancestor element of a node that matches a given tag name
* @param {object} node element
* @param {string} match tag name (e.g. div)
* @return {object} ancestor element
*/

function (node, match) {
// code goes here
return ancestor
* Get the first descendent (child) of an HTML element that matches a given tag name
*
* @param {HTMLElement} $element - HTML element
* @param {string} tagName - Tag name (for example 'div')
* @returns {Element} Ancestor element
*/
function ($element, tagName) {
// Code goes here
return $element.querySelector(tagName)
}
```

Expand All @@ -73,52 +102,54 @@ Use the prototype design pattern to structure your code.
Create a constructor and define any variables that the object needs.

```js
function Checkboxes ($module) {
// code goes here
function Example ($module) {
// Code goes here
}
```

Assign methods to the prototype object. Do not overwrite the prototype with a new object as this makes inheritance impossible.

```js
// bad
Checkboxes.prototype = {
// Bad
Example.prototype = {
init: function () {
// code goes here
// Code goes here
}
}

// good
Checkboxes.prototype.init = function () {
// code goes here
// Good
Example.prototype.init = function () {
// Code goes here
}
```

When initialising an object, use the `new` keyword.

```js
// bad
var myCheckbox = Checkbox().init()
// Bad
var myExample = Example().init()

// good
var myCheckbox = new Checkbox().init()
// Good
var myExample = new Example().init()
```

## Modules

Use ES6 modules (`import`/`export`) over a non-standard module system. You can always transpile to your preferred module system.
Use ECMAScript modules (`import`/`export`) over CommonJS and other formats. You can always transpile to your preferred module system.

```js
import { nodeListForEach } from '../vendor/common.mjs'
// code goes here
export default Checkboxes
import { closestAttributeValue } from '../common/index.mjs'

// Code goes here
export function exampleHelper1 () {}
export function exampleHelper2 () {}
```

Avoid using wildcard (`import * as nodeListForEach`) imports.
You must specify the file extension when using the import keyword.

You must specify the file extension for a file when importing it.
Avoid using namespace imports (`import * as namespace`) in code transpiled to CommonJS (or AMD) bundled code as this can prevent "tree shaking" optimisations.

Use default export over named export.
Prefer named exports over default exports to avoid compatibility issues with transpiler "synthetic default" as discussed in: https://github.com/alphagov/govuk-frontend/issues/2829

## Polyfilling

Expand Down
32 changes: 8 additions & 24 deletions src/govuk/all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,27 @@ function initAll (config) {

var $accordions = $scope.querySelectorAll('[data-module="govuk-accordion"]')
nodeListForEach($accordions, function ($accordion) {
if ($accordion instanceof HTMLElement) {
new Accordion($accordion, config.accordion).init()
}
new Accordion($accordion, config.accordion).init()
})

var $buttons = $scope.querySelectorAll('[data-module="govuk-button"]')
nodeListForEach($buttons, function ($button) {
if ($button instanceof HTMLElement) {
new Button($button, config.button).init()
}
new Button($button, config.button).init()
})

var $characterCounts = $scope.querySelectorAll('[data-module="govuk-character-count"]')
nodeListForEach($characterCounts, function ($characterCount) {
if ($characterCount instanceof HTMLElement) {
new CharacterCount($characterCount, config.characterCount).init()
}
new CharacterCount($characterCount, config.characterCount).init()
})

var $checkboxes = $scope.querySelectorAll('[data-module="govuk-checkboxes"]')
nodeListForEach($checkboxes, function ($checkbox) {
if ($checkbox instanceof HTMLElement) {
new Checkboxes($checkbox).init()
}
new Checkboxes($checkbox).init()
})

var $details = $scope.querySelectorAll('[data-module="govuk-details"]')
nodeListForEach($details, function ($detail) {
if ($detail instanceof HTMLElement) {
new Details($detail).init()
}
new Details($detail).init()
})

// Find first error summary module to enhance.
Expand All @@ -75,16 +65,12 @@ function initAll (config) {

var $notificationBanners = $scope.querySelectorAll('[data-module="govuk-notification-banner"]')
nodeListForEach($notificationBanners, function ($notificationBanner) {
if ($notificationBanner instanceof HTMLElement) {
new NotificationBanner($notificationBanner, config.notificationBanner).init()
}
new NotificationBanner($notificationBanner, config.notificationBanner).init()
})

var $radios = $scope.querySelectorAll('[data-module="govuk-radios"]')
nodeListForEach($radios, function ($radio) {
if ($radio instanceof HTMLElement) {
new Radios($radio).init()
}
new Radios($radio).init()
})

// Find first skip link module to enhance.
Expand All @@ -95,9 +81,7 @@ function initAll (config) {

var $tabs = $scope.querySelectorAll('[data-module="govuk-tabs"]')
nodeListForEach($tabs, function ($tabs) {
if ($tabs instanceof HTMLElement) {
new Tabs($tabs).init()
}
new Tabs($tabs).init()
})
}

Expand Down
3 changes: 3 additions & 0 deletions src/govuk/common/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,13 @@ export function extractConfigByNamespace (configObject, namespace) {
if (!configObject || typeof configObject !== 'object') {
throw new Error('Provide a `configObject` of type "object".')
}

if (!namespace || typeof namespace !== 'string') {
throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
}

var newObject = {}

for (var key in configObject) {
// Split the key into parts, using . as our namespace separator
var keyParts = key.split('.')
Expand Down
Loading

0 comments on commit a9512f8

Please sign in to comment.