UNPKG

govuk-frontend

Version:

GOV.UK Frontend contains the code you need to start building a user interface for government platforms and services.

1 lines 16.8 kB
{"version":3,"file":"configuration.mjs","sources":["../../../src/govuk/common/configuration.mjs"],"sourcesContent":["import { ConfigError } from '../errors/index.mjs'\nimport { GOVUKFrontendComponent } from '../govuk-frontend-component.mjs'\n\nimport { isObject, formatErrorMessage } from './index.mjs'\n\nexport const configOverride = Symbol.for('configOverride')\n\n/**\n * Base Component class\n *\n * Centralises the behaviours shared by our components\n *\n * @virtual\n * @template {ObjectNested} [ConfigurationType={}]\n * @template {Element & { dataset: DOMStringMap }} [RootElementType=HTMLElement]\n * @augments GOVUKFrontendComponent<RootElementType>\n */\nexport class ConfigurableComponent extends GOVUKFrontendComponent {\n /**\n * configOverride\n *\n * Function which defines configuration overrides to prioritize\n * properties from the root element's dataset.\n *\n * It should take a subset of configuration as input and return\n * a new configuration object with properties that should be\n * overridden based on the root element's dataset. A Symbol\n * is used for indexing to prevent conflicts.\n *\n * @internal\n * @virtual\n * @param {ObjectNested} [param] - Configuration object\n * @returns {ObjectNested} return - Configuration object\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n [configOverride](param) {\n return {}\n }\n\n /**\n * Returns the root element of the component\n *\n * @protected\n * @returns {ConfigurationType} - the root element of component\n */\n get config() {\n return this._config\n }\n\n /**\n *\n * @type {ConfigurationType}\n */\n _config\n\n /**\n * Constructs a new component, validating that GOV.UK Frontend is supported\n *\n * @internal\n * @param {Element | null} [$root] - HTML element to use for component\n * @param {ConfigurationType} [config] - HTML element to use for component\n */\n constructor($root, config) {\n super($root)\n\n const childConstructor =\n /** @type {ChildClassConstructor<ConfigurationType>} */ (this.constructor)\n\n if (typeof childConstructor.defaults === 'undefined') {\n throw new ConfigError(\n formatErrorMessage(\n childConstructor,\n 'Config passed as parameter into constructor but no defaults defined'\n )\n )\n }\n\n const datasetConfig = /** @type {ConfigurationType} */ (\n normaliseDataset(childConstructor, this._$root.dataset)\n )\n\n this._config = /** @type {ConfigurationType} */ (\n mergeConfigs(\n childConstructor.defaults,\n config ?? {},\n this[configOverride](datasetConfig),\n datasetConfig\n )\n )\n }\n}\n\n/**\n * Normalise string\n *\n * 'If it looks like a duck, and it quacks like a duck…' 🦆\n *\n * If the passed value looks like a boolean or a number, convert it to a boolean\n * or number.\n *\n * Designed to be used to convert config passed via data attributes (which are\n * always strings) into something sensible.\n *\n * @internal\n * @param {DOMStringMap[string]} value - The value to normalise\n * @param {SchemaProperty} [property] - Component schema property\n * @returns {string | boolean | number | undefined} Normalised data\n */\nexport function normaliseString(value, property) {\n const trimmedValue = value ? value.trim() : ''\n\n let output\n let outputType = property?.type\n\n // No schema type set? Determine automatically\n if (!outputType) {\n if (['true', 'false'].includes(trimmedValue)) {\n outputType = 'boolean'\n }\n\n // Empty / whitespace-only strings are considered finite so we need to check\n // the length of the trimmed string as well\n if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {\n outputType = 'number'\n }\n }\n\n switch (outputType) {\n case 'boolean':\n output = trimmedValue === 'true'\n break\n\n case 'number':\n output = Number(trimmedValue)\n break\n\n default:\n output = value\n }\n\n return output\n}\n\n/**\n * Normalise dataset\n *\n * Loop over an object and normalise each value using {@link normaliseString},\n * optionally expanding nested `i18n.field`\n *\n * @internal\n * @param {{ schema?: Schema, moduleName: string }} Component - Component class\n * @param {DOMStringMap} dataset - HTML element dataset\n * @returns {ObjectNested} Normalised dataset\n */\nexport function normaliseDataset(Component, dataset) {\n if (typeof Component.schema === 'undefined') {\n throw new ConfigError(\n formatErrorMessage(\n Component,\n 'Config passed as parameter into constructor but no schema defined'\n )\n )\n }\n\n const out = /** @type {ReturnType<typeof normaliseDataset>} */ ({})\n\n // Normalise top-level dataset ('data-*') values using schema types\n for (const [field, property] of Object.entries(Component.schema.properties)) {\n if (field in dataset) {\n out[field] = normaliseString(dataset[field], property)\n }\n\n /**\n * Extract and normalise nested object values automatically using\n * {@link normaliseString} but only schema object types are allowed\n */\n if (property?.type === 'object') {\n out[field] = extractConfigByNamespace(Component.schema, dataset, field)\n }\n }\n\n return out\n}\n\n/**\n * Config merging function\n *\n * Takes any number of objects and combines them together, with\n * greatest priority on the LAST item passed in.\n *\n * @internal\n * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge\n * @returns {{ [key: string]: unknown }} A merged config object\n */\nexport function mergeConfigs(...configObjects) {\n // Start with an empty object as our base\n /** @type {{ [key: string]: unknown }} */\n const formattedConfigObject = {}\n\n // Loop through each of the passed objects\n for (const configObject of configObjects) {\n for (const key of Object.keys(configObject)) {\n const option = formattedConfigObject[key]\n const override = configObject[key]\n\n // Push their keys one-by-one into formattedConfigObject. Any duplicate\n // keys with object values will be merged, otherwise the new value will\n // override the existing value.\n if (isObject(option) && isObject(override)) {\n // @ts-expect-error Index signature for type 'string' is missing\n formattedConfigObject[key] = mergeConfigs(option, override)\n } else {\n // Apply override\n formattedConfigObject[key] = override\n }\n }\n }\n\n return formattedConfigObject\n}\n\n/**\n * Validate component config by schema\n *\n * Follows limited examples in JSON schema for wider support in future\n *\n * {@link https://ajv.js.org/json-schema.html#compound-keywords}\n * {@link https://ajv.js.org/packages/ajv-errors.html#single-message}\n *\n * @internal\n * @param {Schema} schema - Config schema\n * @param {{ [key: string]: unknown }} config - Component config\n * @returns {string[]} List of validation errors\n */\nexport function validateConfig(schema, config) {\n const validationErrors = []\n\n // Check errors for each schema\n for (const [name, conditions] of Object.entries(schema)) {\n const errors = []\n\n // Check errors for each schema condition\n if (Array.isArray(conditions)) {\n for (const { required, errorMessage } of conditions) {\n if (!required.every((key) => !!config[key])) {\n errors.push(errorMessage) // Missing config key value\n }\n }\n\n // Check one condition passes or add errors\n if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {\n validationErrors.push(...errors)\n }\n }\n }\n\n return validationErrors\n}\n\n/**\n * Extracts keys starting with a particular namespace from dataset ('data-*')\n * object, removing the namespace in the process, normalising all values\n *\n * @internal\n * @param {Schema} schema - The schema of a component\n * @param {DOMStringMap} dataset - The object to extract key-value pairs from\n * @param {string} namespace - The namespace to filter keys with\n * @returns {ObjectNested | undefined} Nested object with dot-separated key namespace removed\n */\nexport function extractConfigByNamespace(schema, dataset, namespace) {\n const property = schema.properties[namespace]\n\n // Only extract configs for object schema properties\n if (property?.type !== 'object') {\n return\n }\n\n // Add default empty config\n const newObject = {\n [namespace]: /** @type {ObjectNested} */ ({})\n }\n\n for (const [key, value] of Object.entries(dataset)) {\n /** @type {ObjectNested | ObjectNested[NestedKey]} */\n let current = newObject\n\n // Split the key into parts, using . as our namespace separator\n const keyParts = key.split('.')\n\n /**\n * Create new level per part\n *\n * e.g. 'i18n.textareaDescription.other' becomes\n * `{ i18n: { textareaDescription: { other } } }`\n */\n for (const [index, name] of keyParts.entries()) {\n if (typeof current === 'object') {\n // Drop down to nested object until the last part\n if (index < keyParts.length - 1) {\n // New nested object (optionally) replaces existing value\n if (!isObject(current[name])) {\n current[name] = {}\n }\n\n // Drop down into new or existing nested object\n current = current[name]\n } else if (key !== namespace) {\n // Normalised value (optionally) replaces existing value\n current[name] = normaliseString(value)\n }\n }\n }\n }\n\n return newObject[namespace]\n}\n\n/**\n * @internal\n * @typedef {keyof ObjectNested} NestedKey\n * @typedef {{ [key: string]: string | boolean | number | ObjectNested | undefined }} ObjectNested\n */\n\n/**\n * Schema for component config\n *\n * @typedef {object} Schema\n * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties\n * @property {SchemaCondition[]} [anyOf] - List of schema conditions\n */\n\n/**\n * Schema property for component config\n *\n * @typedef {object} SchemaProperty\n * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type\n */\n\n/**\n * Schema condition for component config\n *\n * @typedef {object} SchemaCondition\n * @property {string[]} required - List of required config fields\n * @property {string} errorMessage - Error message when required config fields not provided\n */\n\n/**\n * @template {ObjectNested} [ConfigurationType={}]\n * @typedef ChildClass\n * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component\n * @property {Schema} [schema] - The schema of the component configuration\n * @property {ConfigurationType} [defaults] - The default values of the configuration of the component\n */\n\n/**\n * @template {ObjectNested} [ConfigurationType={}]\n * @typedef {typeof GOVUKFrontendComponent & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>\n */\n"],"names":["configOverride","Symbol","for","ConfigurableComponent","GOVUKFrontendComponent","param","config","_config","constructor","$root","childConstructor","defaults","ConfigError","formatErrorMessage","datasetConfig","normaliseDataset","_$root","dataset","mergeConfigs","normaliseString","value","property","trimmedValue","trim","output","outputType","type","includes","length","isFinite","Number","Component","schema","out","field","Object","entries","properties","extractConfigByNamespace","configObjects","formattedConfigObject","configObject","key","keys","option","override","isObject","validateConfig","validationErrors","name","conditions","errors","Array","isArray","required","errorMessage","every","push","namespace","newObject","current","keyParts","split","index"],"mappings":"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"}