UNPKG

govuk-frontend

Version:

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

197 lines (193 loc) 6.58 kB
import { Component } from '../component.mjs'; import { ConfigError } from '../errors/index.mjs'; import { isObject, formatErrorMessage, isScope } from './index.mjs'; const configOverride = Symbol.for('configOverride'); class ConfigurableComponent extends Component { [configOverride](param) { return {}; } /** * Returns the root element of the component * * @protected * @returns {ConfigurationType} - the root element of component */ get config() { return this._config; } constructor($root, config) { super($root); this._config = void 0; const childConstructor = this.constructor; if (!isObject(childConstructor.defaults)) { throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined')); } const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset); this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig); } } function normaliseString(value, property) { const trimmedValue = value ? value.trim() : ''; let output; let outputType = property == null ? void 0 : property.type; if (!outputType) { if (['true', 'false'].includes(trimmedValue)) { outputType = 'boolean'; } if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) { outputType = 'number'; } } switch (outputType) { case 'boolean': output = trimmedValue === 'true'; break; case 'number': output = Number(trimmedValue); break; default: output = value; } return output; } function normaliseDataset(Component, dataset) { if (!isObject(Component.schema)) { throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined')); } const out = {}; const entries = Object.entries(Component.schema.properties); for (const entry of entries) { const [namespace, property] = entry; const field = namespace.toString(); if (field in dataset) { out[field] = normaliseString(dataset[field], property); } if ((property == null ? void 0 : property.type) === 'object') { out[field] = extractConfigByNamespace(Component.schema, dataset, namespace); } } return out; } function normaliseOptions(scopeOrOptions) { let $scope = document; let onError; if (isObject(scopeOrOptions)) { const options = scopeOrOptions; if (isScope(options.scope) || options.scope === null) { $scope = options.scope; } if (typeof options.onError === 'function') { onError = options.onError; } } if (isScope(scopeOrOptions)) { $scope = scopeOrOptions; } else if (scopeOrOptions === null) { $scope = null; } else if (typeof scopeOrOptions === 'function') { onError = scopeOrOptions; } return { scope: $scope, onError }; } function mergeConfigs(...configObjects) { const formattedConfigObject = {}; for (const configObject of configObjects) { for (const key of Object.keys(configObject)) { const option = formattedConfigObject[key]; const override = configObject[key]; if (isObject(option) && isObject(override)) { formattedConfigObject[key] = mergeConfigs(option, override); } else { formattedConfigObject[key] = override; } } } return formattedConfigObject; } function validateConfig(schema, config) { const validationErrors = []; for (const [name, conditions] of Object.entries(schema)) { const errors = []; if (Array.isArray(conditions)) { for (const { required, errorMessage } of conditions) { if (!required.every(key => !!config[key])) { errors.push(errorMessage); } } if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) { validationErrors.push(...errors); } } } return validationErrors; } function extractConfigByNamespace(schema, dataset, namespace) { const property = schema.properties[namespace]; if ((property == null ? void 0 : property.type) !== 'object') { return; } const newObject = { [namespace]: {} }; for (const [key, value] of Object.entries(dataset)) { let current = newObject; const keyParts = key.split('.'); for (const [index, name] of keyParts.entries()) { if (isObject(current)) { if (index < keyParts.length - 1) { if (!isObject(current[name])) { current[name] = {}; } current = current[name]; } else if (key !== namespace) { current[name] = normaliseString(value); } } } } return newObject[namespace]; } /** * Schema for component config * * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType * @typedef {object} Schema * @property {Record<keyof ConfigurationType, SchemaProperty | undefined>} properties - Schema properties * @property {SchemaCondition<ConfigurationType>[]} [anyOf] - List of schema conditions */ /** * Schema property for component config * * @typedef {object} SchemaProperty * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type */ /** * Schema condition for component config * * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType * @typedef {object} SchemaCondition * @property {(keyof ConfigurationType)[]} required - List of required config fields * @property {string} errorMessage - Error message when required config fields not provided */ /** * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested] * @typedef ChildClass * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component * @property {Schema<ConfigurationType>} [schema] - The schema of the component configuration * @property {ConfigurationType} [defaults] - The default values of the configuration of the component */ /** * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested] * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType> */ /** * @import { CompatibleClass, Config, CreateAllOptions, OnErrorCallback } from '../init.mjs' */ export { ConfigurableComponent, configOverride, extractConfigByNamespace, mergeConfigs, normaliseDataset, normaliseOptions, normaliseString, validateConfig }; //# sourceMappingURL=configuration.mjs.map