UNPKG

hmpl-dom

Version:

A module for using HMPL syntax directly in HTML, without the need for compilation on the JavaScript side

149 lines (148 loc) 5.41 kB
"use strict"; import hmpl from "hmpl-js"; const INIT_ERROR = `InitError`; const TEMPLATE_ERROR = `TemplateError`; const DATA_HMPL_ATTR = "data-hmpl"; const HMPL_ATTR = "hmpl"; const DATA_CONFIG_ID_ATTR = "data-config-id"; const CONFIG_ID_ATTR = "configId"; /** * Throws a new error with the provided message. * @param text - The error message. */ const createError = (text) => { throw new Error(text); }; /** * Checks if a value is an object (not null, not array). * @param val - The value to check. */ const checkObject = (val) => { return typeof val === "object" && !Array.isArray(val) && val !== null; }; /** * Validates the HMPLTemplateConfig object. * @param option - The option to validate. */ const validateInitOption = (option) => { if (!checkObject(option)) { createError(`${INIT_ERROR}: HMPLTemplateConfig must be an object`); } if (!option.hasOwnProperty("id") || !option.hasOwnProperty("value")) { createError(`${INIT_ERROR}: Missing "id" or "value" property`); } if (typeof option.id !== "string") { createError(`${INIT_ERROR}: ID must be a string`); } if (!checkObject(option.value)) { createError(`${INIT_ERROR}: Value must be an object`); } }; /** * Validates that there are no duplicate IDs in the options array. * @param options - The array of options to validate. */ const validateDuplicateIds = (options) => { const ids = []; for (let i = 0; i < options.length; i++) { const id = options[i].id; if (ids.indexOf(id) > -1) { createError(`${INIT_ERROR}: ID with value "${id}" already exists`); } else { ids.push(id); } } }; let initialized = false; const initOptionsMap = new Map(); const onDocumentLoad = (callback) => { const isDocumentLoaded = document.readyState === "complete" || document.readyState === "interactive"; void (isDocumentLoaded ? callback() : document.addEventListener("DOMContentLoaded", callback)); }; const mountTemplates = () => { const templates = document.querySelectorAll(`template[${DATA_HMPL_ATTR}], template[${HMPL_ATTR}]`); for (let i = 0; i < templates.length; i++) { const template = templates[i]; const hasDataHmpl = template.hasAttribute(DATA_HMPL_ATTR); const hasHmpl = template.hasAttribute(HMPL_ATTR); if (hasDataHmpl && hasHmpl) { createError(`${TEMPLATE_ERROR}: Cannot use both ${DATA_HMPL_ATTR} and ${HMPL_ATTR} attributes`); } const hasDataConfigId = template.hasAttribute(DATA_CONFIG_ID_ATTR); const hasConfigId = template.hasAttribute(CONFIG_ID_ATTR); if (hasDataConfigId && hasConfigId) { createError(`${TEMPLATE_ERROR}: Cannot use both ${DATA_CONFIG_ID_ATTR} and ${CONFIG_ID_ATTR} attributes`); } const configId = template.getAttribute(DATA_CONFIG_ID_ATTR) || template.getAttribute(CONFIG_ID_ATTR); let option = undefined; if (configId === "") { createError(`${TEMPLATE_ERROR}: configId cannot be empty. Use ${DATA_CONFIG_ID_ATTR} or ${CONFIG_ID_ATTR} attribute`); } if (configId) { option = initOptionsMap.get(configId); if (!option) { createError(`${TEMPLATE_ERROR}: Option with id "${configId}" not found. Make sure to define it in init() call`); } } const templateHTML = template.innerHTML; if (template.content.children.length > 1) { createError(`${TEMPLATE_ERROR}: Template must contain exactly one root element, found ${template.content.children.length}`); } if (template.content.children.length === 0) { createError(`${TEMPLATE_ERROR}: Template must contain at least one element`); } const compileOptions = option?.value.compile || {}; const templateFunctionOptions = option?.value.templateFunction || {}; const templateFn = hmpl.compile(templateHTML, compileOptions); const result = templateFn(templateFunctionOptions); if (result && result.response) { template.replaceWith(result.response); } } }; /** * Initializes the HMPL DOM system with configuration options. * Can only be called once. Subsequent calls will throw an error. * * @param options - Array of configuration objects for different template options. * Each option should have a unique ID and contain compile options and template function options. * * @throws {Error} When called more than once * @throws {Error} When duplicate IDs are found in options * @throws {Error} When option structure is not valid * * @example * ```typescript * init([ * { * id: "user-card", * value: { * compile: { memo: true }, * templateFunction: { credentials: "include" } * } * } * ]); * ``` */ export const init = (configs = []) => { if (initialized) { createError(`${INIT_ERROR}: init() can only be called once`); } initialized = true; validateDuplicateIds(configs); for (let i = 0; i < configs.length; i++) { const option = configs[i]; validateInitOption(option); initOptionsMap.set(option.id, option); } onDocumentLoad(mountTemplates); }; queueMicrotask(() => { if (!initialized) { onDocumentLoad(mountTemplates); } });