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

Update internal developer docs to use new configuration guidance #5516

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
229 changes: 33 additions & 196 deletions docs/contributing/coding-standards/component-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,191 +4,29 @@ Configuration options allow developers to pass additional information into a com

Configuration options can be configured using Nunjucks, HTML or JavaScript, but only JavaScript will use the options.

## Preparing the component's JavaScript

First, make sure the component class has a constructor parameter for passing in a configuration object.

```mjs
export class Accordion {
constructor($root, config = {}) {
// ...
}
}
```

Within the entry class, you'll then create an object with the default configuration for the component. These are the options the component will use if no other options are provided.

```mjs
export class Accordion {
static defaults = Object.freeze({
rememberExpanded: true
})
}
```

Next, use the `mergeConfigs` helper to combine the default config and the configuration object. The result of the merge is assigned globally (using `this`) so that it's accessible anywhere within the component's JavaScript.

The order in which variables are written defines their priority, with objects passed sooner being overwritten by those passed later. As we want the user's configuration to take precedence over our defaults, we list our default configuration object first.

There is no guarantee `config` will have any value at all, so we set the default to an empty object (`{}`) in the constructor parameters.

```mjs
import { mergeConfigs } from '../../common/configuration.mjs'

export class Accordion {
constructor($root, config = {}) {
this.config = mergeConfigs(
Accordion.defaults,
config
)
}
}
```

We can now reference the configuration option anywhere we need to in the component's JavaScript:

```js
this.config.rememberExpanded
// => true
```

It's now possible to individually initialise the component with configuration options.

```html
<script type="module">
import { Accordion } from '{path-to-javascript}/govuk-frontend.min.js'

const $element = document.getElementById('accordion')

new Accordion($element, {
rememberExpanded: false
})
</script>
```

## Allowing options to be passed through the `initAll` function

Often, teams will not be individually initialising components. Instead, GOV.UK Frontend ships with an `initAll` function, which searches the page for instances of components and automatically initialises them.

In `packages/govuk-frontend/src/govuk/init.mjs`, update the component's `new` class to pass through a nested configuration object. The nested object should use the component's name converted to camelCase (for example, the 'Character Count' component becomes `characterCount`).

```js
new Accordion($accordion, config.accordion)
```

It's now possible to pass configuration options for your component, as well as multiple other components, using the `initAll` function.

```html
<script type="module">
import { initAll } from '{path-to-javascript}/govuk-frontend.min.js'

initAll({
accordion: {
rememberExpanded: false
},
characterCount: {
threshold: 90
}
})
</script>
```

## Adding support for HTML `data-*` attributes
For more information, see our [guidance on building configurable components](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#building-configurable-components).

For convenience, we also allow for configuration options to be passed through HTML `data-*` attributes.

You can find `data-*` attributes in JavaScript by looking at an element's `dataset` property. Browsers will convert the attribute names from HTML's kebab-case to more JavaScript-friendly camelCase when working with `dataset`.

See ['Naming configuration options'](#naming-configuration-options) for exceptions to how names are transformed.

As we expect configuration-related `data-*` attributes to always be on the component's root element (the same element with the `data-module` attribute), we can access them all using `this.$root.dataset`.

Using the `mergeConfigs` call discussed earlier in this document, update it to include `this.$root.dataset` as the highest priority after calling the constuctor of `GOVUKFrontendComponent` with `super()`.

```mjs
import { mergeConfigs, normaliseDataset } from '../../common/configuration.mjs'
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs'

export class Accordion extends GOVUKFrontendComponent{
constructor($root, config = {}) {
super($root)

this.config = mergeConfigs(
Accordion.defaults,
config,
normaliseDataset(Accordion, this.$root.dataset)
)
}
}
```

Here, we pass the value of `this.$root.dataset` through our `normaliseDataset` function. This is because attribute values in dataset are always interpreted as strings. `normaliseDataset` looks at the component's configuration schema and converts values into numbers or booleans where needed.

Now, in our HTML, we could pass configuration options by using the kebab-case version of the option's name.

```html
<div
class="govuk-accordion"
data-module="govuk-accordion"
data-remember-expanded="false">
...
</div>
```

However, this only works for developers who are writing raw HTML. We include Nunjucks macros for each component with GOV.UK Frontend to make development easier and faster, but this also makes it harder for developers to manually alter the raw HTML. We'll add a new parameter to Nunjucks to help them out.
## Preparing the component's JavaScript

### Adding a configuration schema
If a component class needs configuration, [extend the `ConfigurableComponent` class](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/) that GOV.UK Frontend provides. This class accepts configuration from the following sources:

Components that accept configuration using `data-*` attributes also require a schema. This schema documents what parameters a configuration object may contain and what types of value they're expected to be.
- [defaults defined as a `defaults` static property](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#setting-a-default-configuration-for-your-component) on the component’s class
- [configuration passed as second argument](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#receiving-configuration-during-initialisation) to the component’s constructor or [`createAll`](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#using-createall-with-your-components)
- [data attributes on the root element](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#receiving-configuration-from-data-attributes) of the component

Having a schema is required for the `normaliseDataset` function to work. A schema is also needed to use the `validateConfig` and `extractConfigByNamespace` functions we'll cover later on.
Configuration options from these sources are merged into a single configuration object by the `ConfigurableComponent` class. Note that:

```mjs
export class Accordion {
static schema = Object.freeze({
properties: {
i18n: { type: 'object' },
rememberExpanded: { type: 'boolean' }
}
})
}
```
- configuration options passed to the constructor will override any defaults
- data attribute values have highest precedence and will override any configuration set elsewhere

### Validating a provided configuration against the schema
Your component will then be able to:

You can use the `validateConfig` function to ensure that a configuration object matches the schema format.

If it doesn't, you can return a `ConfigError`.

```mjs
import { mergeConfigs, validateConfig, normaliseDataset } from '../../common/configuration.mjs'
import { ConfigError } from '../../errors/index.mjs'
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs'

export class Accordion extends GOVUKFrontendComponent {
constructor($root, config = {}) {
super($root)

this.config = mergeConfigs(
Accordion.defaults,
config,
normaliseDataset(Accordion, this.$root.dataset)
)

// Check that the configuration provided is valid
const errors = validateConfig(Accordion.schema, this.config)
if (errors[0]) {
throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]))
}
}
}
```
[access the merged configuration](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#accessing-the-component-s-configuration) using `this.config`
use [the`Component` class](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#using-the-component-class) features

## Adding a Nunjucks parameter

Most, but not all, components support adding arbitrary attributes and values through the `attributes` parameter. This method is also more verbose compared to having a dedicated Nunjucks parameter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion I think that bit is quite useful as it explains the two ways Nunjucks components can pass data-* attributes to the HTML for use by JavaScript components:

  • via an attributes parameter allowing arbitrary data-* attributes
  • via specific Nunjucks options that get converted to data-* attributes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed!


Using the previous bit of HTML, update it to make the `data-*` attribute changeable.
Most, but not all, components support adding arbitrary attributes and values through the `attributes` parameter. This method is also more verbose compared to having a dedicated Nunjucks parameter to set data attributes on the root element. For example, this is how the Accordion does it for its `rememberExpanded` option:

```nunjucks
<div
Expand All @@ -201,7 +39,7 @@ Using the previous bit of HTML, update it to make the `data-*` attribute changea

The above code checks for the existence of the `rememberExpanded` parameter. If the parameter is present it adds the `data-remember-expanded` attribute. It then passes in the developer's defined value, running it through [Nunjucks' native `escape` filter](https://mozilla.github.io/nunjucks/templating.html#escape-aliased-as-e) to make sure that values can't break our HTML.

We can now call the Accordion's Nunjucks macro with our new `rememberExpanded` parameter:
With the previous code in the Accordion's template, this is how a page may set a specific value for the `rememberExpanded` option:

```nunjucks
{% from "govuk/components/accordion/macro.njk" import govukAccordion %}
Expand Down Expand Up @@ -231,37 +69,36 @@ In our example, `rememberExpanded` becomes `data-remember-expanded`.

### In Nunjucks

Unlike the `data-*` attribute in HTML and our use of `dataset` in JavaScript, there is no intrinsic link between the Nunjucks parameter name and the names used elsewhere. The Nunjucks parameter can therefore be named differently, if convenient.
Unlike the `data-*` attribute in HTML, there is no intrinsic link between the Nunjucks parameter name and the names used elsewhere. The Nunjucks parameter can therefore be named differently, if convenient.
domoscargin marked this conversation as resolved.
Show resolved Hide resolved

A common case is specifying whether a parameter accepts HTML or only plain text. For example, if a configuration option's value is inserted into the page using `innerText`, you might want to name the Nunjucks parameter something like `sectionLabelText` to indicate that HTML will not be parsed if provided.

There is more guidance on naming Nunjucks parameters in [the Nunjucks API documentation](https://github.com/alphagov/govuk-frontend/blob/main/docs/contributing/coding-standards/nunjucks-api.md#naming-options).

## Namespacing configuration options

You can group configuration options in JavaScript and HTML together by using namespaces; period-separated strings that prefix the configuration option name. Namespaces follow the same formats as other option names, being camelCase in JavaScript and kebab-case in HTML.

These are most commonly used for translation strings, which are usually namespaced under `i18n` (short for 'internationalisation').
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion Can we keep that as an explanation for the use of nested objects as it's the main one we have in govuk-frontend? Maybe to form a paragraph with "When passing configuration into your component's constructor...", moving the explanation on how to use data attributes to a second paragraph?


For example, we could namespace our `rememberExpanded` option under the `stateInfo` namespace. Our `data-*` attribute would now be named `data-state-info.remember-expanded` and accessed in the component's JavaScript using `this.config.stateInfo.rememberExpanded`.

The `extractConfigByNamespace` JavaScript helper can be used to create an object containing _only_ the configuration options that belong to a certain namespace.
When passing configuration into your component's constructor, you can use nested objects to group options. For example, GOV.UK Frontend's translation strings are defined as an object named `i18n` (short for 'internationalisation'), as seen here with the Accordion:

```mjs
import { mergeConfigs, extractConfigByNamespace, normaliseDataset } from '../../common/configuration.mjs'
import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs'

export class Accordion extends GOVUKFrontendComponent {
constructor($root, config = {}) {
super($root)
createAll(Accordion, {
i18n: {
hideAllSections: 'Hide all sections',
hideSection: 'Hide',
hideSectionAriaLabel: 'Hide this section',
showAllSections: 'Show all sections',
showSection: 'Show',
showSectionAriaLabel: 'Show this section'
},
rememberExpanded: true
})
```

this.config = mergeConfigs(
Accordion.defaults,
config,
normaliseDataset(Accordion, this.$root.dataset)
)
Data attributes only accept strings, but our [naming conventions for data attributes](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#receiving-configuration-from-data-attributes) allow you to set options in nested objects using a dot `.` separator for each level of nesting. For example `data-i18n.showLabel="Show"` will set the same option as the following constructor argument:

this.stateInfo = extractConfigByNamespace(Accordion, this.config, 'stateInfo');
```json
{
i18n: {
showLabel: 'Show'
}
}
```
4 changes: 3 additions & 1 deletion docs/contributing/coding-standards/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ If your component uses JavaScript, you must also create the following files in t

## Building your components

If you need help building a component, [contact the Design System team](https://design-system.service.gov.uk/get-in-touch/) and we'll support you.
To help you build and initialise your own JavaScript components, GOV.UK Frontend provides some of its internal features for you to reuse. Follow our guidance on [building JavaScript components](https://frontend.design-system.service.gov.uk/building-your-own-javascript-components/#building-your-own-javascript-components) to use these features.

Learn more about styling components in our [CSS style guide](./css.md). Our [JavaScript style guide](./js.md) has more information on coding components.

If you need help building a component, [contact the Design System team](https://design-system.service.gov.uk/get-in-touch/) and we'll support you.