Skip to content

Commit 0718c67

Browse files
Update coding standards with JSDoc changes etc
Also includes: 1. Checks for `$module` on instantiation 2. Checks for selectors on init We prefer `{Element}` type to `{HTMLElement}` etc to maintain compatibility with `querySelector`
1 parent 478f642 commit 0718c67

File tree

16 files changed

+314
-113
lines changed

16 files changed

+314
-113
lines changed

docs/contributing/coding-standards/js.md

+59-37
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,45 @@
55
JavaScript files have the same name as the component's folder name. Test files have a `.test` suffix placed before the file extension.
66

77
```
8-
checkboxes
9-
├── checkboxes.mjs
10-
└── checkboxes.test.js
8+
component
9+
├── component.mjs
10+
└── component.test.js
1111
```
1212

1313
## Skeleton
1414

1515
```js
16-
import { nodeListForEach } from '../vendor/common.mjs'
16+
import '../../vendor/polyfills/Element.mjs'
1717

18-
function Checkboxes ($module) {
19-
// code goes here
18+
/**
19+
* Component name
20+
*
21+
* @class
22+
* @param {Element} $module - HTML element to use for component
23+
*/
24+
function Example ($module) {
25+
if (!$module) {
26+
return this
27+
}
28+
29+
this.$module = $module
30+
31+
// Code goes here
2032
}
2133

22-
Checkboxes.prototype.init = function () {
23-
// code goes here
34+
/**
35+
* Initialise component
36+
*/
37+
Example.prototype.init = function () {
38+
// Check that required elements are present
39+
if (!this.$module) {
40+
return
41+
}
42+
43+
// Code goes here
2444
}
2545

26-
export default Checkboxes
46+
export default Example
2747
```
2848

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

4969
```js
5070
/**
51-
* Get the nearest ancestor element of a node that matches a given tag name
52-
* @param {object} node element
53-
* @param {string} match tag name (e.g. div)
54-
* @return {object} ancestor element
55-
*/
56-
57-
function (node, match) {
58-
// code goes here
59-
return ancestor
71+
* Get the first descendent (child) of an HTML element that matches a given tag name
72+
*
73+
* @param {Element} $element - HTML element
74+
* @param {string} tagName - Tag name (for example 'div')
75+
* @returns {Element} Ancestor element
76+
*/
77+
function ($element, tagName) {
78+
// Code goes here
79+
return $element.querySelector(tagName)
6080
}
6181
```
6282

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

7595
```js
76-
function Checkboxes ($module) {
77-
// code goes here
96+
function Example ($module) {
97+
// Code goes here
7898
}
7999
```
80100

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

83103
```js
84-
// bad
85-
Checkboxes.prototype = {
104+
// Bad
105+
Example.prototype = {
86106
init: function () {
87-
// code goes here
107+
// Code goes here
88108
}
89109
}
90110

91-
// good
92-
Checkboxes.prototype.init = function () {
93-
// code goes here
111+
// Good
112+
Example.prototype.init = function () {
113+
// Code goes here
94114
}
95115
```
96116

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

99119
```js
100-
// bad
101-
var myCheckbox = Checkbox().init()
120+
// Bad
121+
var myExample = Example()
102122

103-
// good
104-
var myCheckbox = new Checkbox().init()
123+
// Good
124+
var myExample = new Example()
105125
```
106126

107127
## Modules
108128

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

111131
```js
112-
import { nodeListForEach } from '../vendor/common.mjs'
113-
// code goes here
114-
export default Checkboxes
132+
import { closestAttributeValue } from '../common/index.mjs'
133+
134+
// Code goes here
135+
export function exampleHelper1 () {}
136+
export function exampleHelper2 () {}
115137
```
116138

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

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

121-
Use default export over named export.
143+
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
122144

123145
## Polyfilling
124146

src/govuk/all.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export {
104104
* Config for all components
105105
*
106106
* @typedef {object} Config
107-
* @property {HTMLElement} [scope=document] - Scope to query for components
107+
* @property {Element} [scope=document] - Scope to query for components
108108
* @property {import('./components/accordion/accordion.mjs').AccordionConfig} [accordion] - Accordion config
109109
* @property {import('./components/button/button.mjs').ButtonConfig} [button] - Button config
110110
* @property {import('./components/character-count/character-count.mjs').CharacterCountConfig} [characterCount] - Character Count config

src/govuk/common/closest-attribute-value.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import '../vendor/polyfills/Element/prototype/closest.mjs'
33
/**
44
* Returns the value of the given attribute closest to the given element (including itself)
55
*
6-
* @param {HTMLElement} $element - The element to start walking the DOM tree up
6+
* @param {Element} $element - The element to start walking the DOM tree up
77
* @param {string} attributeName - The name of the attribute
88
* @returns {string | null} Attribute value
99
*/

src/govuk/common/index.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,13 @@ export function extractConfigByNamespace (configObject, namespace) {
135135
if (!configObject || typeof configObject !== 'object') {
136136
throw new Error('Provide a `configObject` of type "object".')
137137
}
138+
138139
if (!namespace || typeof namespace !== 'string') {
139140
throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
140141
}
142+
141143
var newObject = {}
144+
142145
for (var key in configObject) {
143146
// Split the key into parts, using . as our namespace separator
144147
var keyParts = key.split('.')

src/govuk/components/accordion/accordion.mjs

+42-13
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,14 @@ var ACCORDION_TRANSLATIONS = {
3838
* attribute, which also provides accessibility.
3939
*
4040
* @class
41-
* @param {HTMLElement} $module - HTML element to use for accordion
41+
* @param {Element} $module - HTML element to use for accordion
4242
* @param {AccordionConfig} [config] - Accordion config
4343
*/
4444
function Accordion ($module, config) {
45+
if (!$module) {
46+
return this
47+
}
48+
4549
this.$module = $module
4650

4751
var defaultConfig = {
@@ -79,16 +83,21 @@ function Accordion ($module, config) {
7983
this.sectionSummaryFocusClass = 'govuk-accordion__section-summary-focus'
8084
this.sectionContentClass = 'govuk-accordion__section-content'
8185

82-
this.$sections = this.$module.querySelectorAll('.' + this.sectionClass)
86+
var $sections = this.$module.querySelectorAll('.' + this.sectionClass)
87+
if (!$sections.length) {
88+
return this
89+
}
90+
91+
this.$sections = $sections
8392
this.browserSupportsSessionStorage = helper.checkForSessionStorage()
8493
}
8594

8695
/**
8796
* Initialise component
8897
*/
8998
Accordion.prototype.init = function () {
90-
// Check for module
91-
if (!this.$module) {
99+
// Check that required elements are present
100+
if (!this.$module || !this.$sections) {
92101
return
93102
}
94103

@@ -145,6 +154,9 @@ Accordion.prototype.initSectionHeaders = function () {
145154
// Loop through sections
146155
nodeListForEach($sections, function ($section, i) {
147156
var $header = $section.querySelector('.' + $component.sectionHeaderClass)
157+
if (!$header) {
158+
return
159+
}
148160

149161
// Set header attributes
150162
$component.constructHeaderMarkup($header, i)
@@ -162,14 +174,18 @@ Accordion.prototype.initSectionHeaders = function () {
162174
/**
163175
* Construct section header
164176
*
165-
* @param {HTMLElement} $header - Section header
177+
* @param {Element} $header - Section header
166178
* @param {number} index - Section index
167179
*/
168180
Accordion.prototype.constructHeaderMarkup = function ($header, index) {
169181
var $span = $header.querySelector('.' + this.sectionButtonClass)
170182
var $heading = $header.querySelector('.' + this.sectionHeadingClass)
171183
var $summary = $header.querySelector('.' + this.sectionSummaryClass)
172184

185+
if (!$span || !$heading) {
186+
return
187+
}
188+
173189
// Create a button element that will replace the '.govuk-accordion__section-button' span
174190
var $button = document.createElement('button')
175191
$button.setAttribute('type', 'button')
@@ -227,7 +243,7 @@ Accordion.prototype.constructHeaderMarkup = function ($header, index) {
227243
$button.appendChild(this.getButtonPunctuationEl())
228244

229245
// If summary content exists add to DOM in correct order
230-
if (typeof ($summary) !== 'undefined' && $summary !== null) {
246+
if ($summary) {
231247
// Create a new `span` element and copy the summary line content from the original `div` to the
232248
// new `span`
233249
// This is because the summary line text is now inside a button element, which can only contain
@@ -267,7 +283,13 @@ Accordion.prototype.constructHeaderMarkup = function ($header, index) {
267283
* @param {Event} event - Generic event
268284
*/
269285
Accordion.prototype.onBeforeMatch = function (event) {
270-
var $section = event.target.closest('.' + this.sectionClass)
286+
var $fragment = event.target
287+
if (!$fragment) {
288+
return
289+
}
290+
291+
// Handle when fragment is inside section
292+
var $section = $fragment.closest('.' + this.sectionClass)
271293
if ($section) {
272294
this.setExpanded(true, $section)
273295
}
@@ -276,7 +298,7 @@ Accordion.prototype.onBeforeMatch = function (event) {
276298
/**
277299
* When section toggled, set and store state
278300
*
279-
* @param {HTMLElement} $section - Section element
301+
* @param {Element} $section - Section element
280302
*/
281303
Accordion.prototype.onSectionToggle = function ($section) {
282304
var expanded = this.isExpanded($section)
@@ -309,14 +331,21 @@ Accordion.prototype.onShowOrHideAllToggle = function () {
309331
* Set section attributes when opened/closed
310332
*
311333
* @param {boolean} expanded - Section expanded
312-
* @param {HTMLElement} $section - Section element
334+
* @param {Element} $section - Section element
313335
*/
314336
Accordion.prototype.setExpanded = function (expanded, $section) {
315337
var $showHideIcon = $section.querySelector('.' + this.upChevronIconClass)
316338
var $showHideText = $section.querySelector('.' + this.sectionShowHideTextClass)
317339
var $button = $section.querySelector('.' + this.sectionButtonClass)
318340
var $content = $section.querySelector('.' + this.sectionContentClass)
319341

342+
if (!$showHideIcon ||
343+
!$showHideText ||
344+
!$button ||
345+
!$content) {
346+
return
347+
}
348+
320349
var newButtonText = expanded
321350
? this.i18n.t('hideSection')
322351
: this.i18n.t('showSection')
@@ -368,7 +397,7 @@ Accordion.prototype.setExpanded = function (expanded, $section) {
368397
/**
369398
* Get state of section
370399
*
371-
* @param {HTMLElement} $section - Section element
400+
* @param {Element} $section - Section element
372401
* @returns {boolean} True if expanded
373402
*/
374403
Accordion.prototype.isExpanded = function ($section) {
@@ -434,7 +463,7 @@ var helper = {
434463
/**
435464
* Set the state of the accordions in sessionStorage
436465
*
437-
* @param {HTMLElement} $section - Section element
466+
* @param {Element} $section - Section element
438467
*/
439468
Accordion.prototype.storeState = function ($section) {
440469
if (this.browserSupportsSessionStorage) {
@@ -458,7 +487,7 @@ Accordion.prototype.storeState = function ($section) {
458487
/**
459488
* Read the state of the accordions from sessionStorage
460489
*
461-
* @param {HTMLElement} $section - Section element
490+
* @param {Element} $section - Section element
462491
*/
463492
Accordion.prototype.setInitialState = function ($section) {
464493
if (this.browserSupportsSessionStorage) {
@@ -482,7 +511,7 @@ Accordion.prototype.setInitialState = function ($section) {
482511
* into thematic chunks.
483512
* See https://github.com/alphagov/govuk-frontend/issues/2327#issuecomment-922957442
484513
*
485-
* @returns {HTMLElement} DOM element
514+
* @returns {Element} DOM element
486515
*/
487516
Accordion.prototype.getButtonPunctuationEl = function () {
488517
var $punctuationEl = document.createElement('span')

src/govuk/components/button/button.mjs

+10-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ var DEBOUNCE_TIMEOUT_IN_SECONDS = 1
1212
* JavaScript enhancements for the Button component
1313
*
1414
* @class
15-
* @param {HTMLElement} $module - HTML element to use for button
15+
* @param {Element} $module - HTML element to use for button
1616
* @param {ButtonConfig} [config] - Button config
1717
*/
1818
function Button ($module, config) {
@@ -26,6 +26,7 @@ function Button ($module, config) {
2626
var defaultConfig = {
2727
preventDoubleClick: false
2828
}
29+
2930
this.config = mergeConfigs(
3031
defaultConfig,
3132
config || {},
@@ -37,6 +38,7 @@ function Button ($module, config) {
3738
* Initialise component
3839
*/
3940
Button.prototype.init = function () {
41+
// Check that required elements are present
4042
if (!this.$module) {
4143
return
4244
}
@@ -58,7 +60,13 @@ Button.prototype.init = function () {
5860
Button.prototype.handleKeyDown = function (event) {
5961
var $target = event.target
6062

61-
if ($target.getAttribute('role') === 'button' && event.keyCode === KEY_SPACE) {
63+
// Handle space bar only
64+
if (event.keyCode !== KEY_SPACE) {
65+
return
66+
}
67+
68+
// Handle elements with [role="button"] only
69+
if ($target.getAttribute('role') === 'button') {
6270
event.preventDefault() // prevent the page from scrolling
6371
$target.click()
6472
}

0 commit comments

Comments
 (0)