UNPKG

appblocks

Version:

A lightweight javascript library for building micro apps for the front-end.

234 lines (189 loc) 7.9 kB
'use strict'; import {Idiomorph} from 'idiomorph/dist/idiomorph.esm.js'; import {updateTextNodePlaceholders} from './placeholders'; import {directives} from './directives'; import {filters} from './filters'; import {processNode} from './processing'; import {helpers} from './utils'; import {fetchRequest, axiosRequest} from './requests'; import { logError } from './logger'; const defaultRequestHeaders = { } export function AppBlock(config) { // Sets or Updates the data and then calls render() this.setData = function(newData, replaceData = false) { if (replaceData) { this.data = newData; } else { Object.assign(this.data, newData); } this.render(); } // Resets to the default state. Handy before making a request. this.resetState = function() { this.state.loading = false; this.state.error = false; this.state.success = false; } // Requests this.axiosRequest = (options, callbacks, delay) => axiosRequest(this, options, callbacks, delay); this.fetchRequest = (url, options, callbacks, delay) => fetchRequest(this, url, options, callbacks, delay); // Render ============================================================================================================ // This is the heart of an AppBlock. This is where all placeholders and directives get evaluated based on our // data, and content gets updated. this.prepareTmpDom = function() { const comp = this; const cache = new Map(); // Per-render ephemeral cache let tmpDOM = comp.template.cloneNode(true); // Wrap in a div to handle directives on root elements const wrapper = document.createElement('div'); wrapper.appendChild(tmpDOM); processNode(comp, wrapper, cache); updateTextNodePlaceholders(comp, wrapper, null, cache); // Return the processed children, not the wrapper div return wrapper.childNodes; } this.render = function(callback) { const comp = this; if (comp.methods.beforeRender instanceof Function) comp.methods.beforeRender(comp); let tmpDOM = this.prepareTmpDom(); if (comp.renderEngine === 'Idiomorph') { comp.idiomorphRender(tmpDOM); } else if (comp.renderEngine === 'plain') { comp.plainRender(tmpDOM); } else { logError(comp, `${comp.renderEngine} renderEngine does not exist.`); } if (comp.methods.afterRender instanceof Function) comp.methods.afterRender(comp); if (callback instanceof Function) callback(); } // Render engines this.plainRender = function(tmpDOM) { this.el.innerHTML = ''; // tmpDOM is a NodeList, append each child Array.from(tmpDOM).forEach(child => { this.el.appendChild(child); }); } this.idiomorphRender = function(tmpDOM) { // tmpDOM is a NodeList - Idiomorph handles it directly Idiomorph.morph(this.el, tmpDOM, {morphStyle:'innerHTML'}); } // Initialization ==================================================================================================== this.Init = function() { const comp = this; if (config.name) { comp.name = config.name; } else { comp.name = "AppBlock"; } // Initialize all the properties and update them from the config if they are included, or exit if no // config is provided. if (config !== undefined) { if (config.el === undefined) { logError(comp, "el is empty. Please assign a DOM element to el."); return; } if (config.el === null) { logError(comp, "The element you assigned to el is not present."); return; } comp.el = config.el; comp.renderEngine = config.renderEngine ? config.renderEngine : "Idiomorph"; // Get or create a document fragment with all the app's contents and pass it to the template. if (config.template) { comp.template = config.template.content; } else { comp.template = document.createDocumentFragment(); while (comp.el.firstChild) { comp.template.appendChild(comp.el.firstChild); } } comp.state = { loading: false, error: false, success: false }; comp.data = {}; if (config.data instanceof Object) comp.data = config.data; // A set of helper functions. comp.utils = helpers; comp.utils['comp'] = comp; comp.methods = { Parent: comp, isLoading(thisApp) { return thisApp.state.loading; }, isSuccessful(thisApp) { return thisApp.state.success; }, hasError(thisApp) { return thisApp.state.error; }, beforeRender(thisApp) {}, afterRender(thisApp) {} }; if (config.methods instanceof Object) Object.assign(comp.methods, config.methods); comp.directives = directives; if (config.directives instanceof Object) Object.assign(comp.directives, config.directives); comp.filters = filters; if (config.filters instanceof Object) Object.assign(comp.filters, config.filters); // Expression evaluation built-ins allow-list. Expect an array of strings (default: empty, no built-ins allowed). comp.allowBuiltins = []; if (Array.isArray(config.allowBuiltins)) { comp.allowBuiltins = config.allowBuiltins; } // Placeholder delimiters configuration. Expect an array of two non-empty strings. const defaultDelimiters = ['{', '}']; if (Array.isArray(config.delimiters) && config.delimiters.length === 2 && typeof config.delimiters[0] === 'string' && typeof config.delimiters[1] === 'string' && config.delimiters[0].length > 0 && config.delimiters[1].length > 0) { comp.delimiters = config.delimiters; } else { if (config.delimiters !== undefined) { // Developer provided an invalid configuration — log and fallback to default logError(comp, 'Invalid `delimiters` config provided. Falling back to default [`{`,`}`].'); } comp.delimiters = defaultDelimiters; } // Event handling ------------------------------------------------------------------------------------------------ comp.events = {}; if (config.events instanceof Object) { Object.assign(comp.events, config.events) // Add event listeners to :el for each event for (const ev in comp.events) { // Events are in this form "<eventName> <cssSelector>" where the selector may contain spaces. // Split only on the first space so the remainder is treated as a full selector string. const firstSpace = ev.indexOf(' '); if (firstSpace === -1) continue; // invalid key const eventName = ev.slice(0, firstSpace); const eventSelector = ev.slice(firstSpace + 1).trim(); comp.el.addEventListener(eventName, function(e) { const target = e.target || e.srcElement; // Use closest to test whether the event originated from within a matching element. // Ensure the matched element is inside this AppBlock's root. let matched = null; try { if (target && target.closest) matched = target.closest(eventSelector); } catch (err) { // If the selector is invalid, skip handling to avoid breaking the app. matched = null; } if (matched && comp.el.contains(matched)) { try { comp.events[ev](e, matched); } catch (err) { // swallow handler errors to avoid breaking other handlers logError(comp, err && err.message ? err.message : String(err)); } } }); } } comp.events['Parent'] = comp; } else { return false; } comp.render(); return comp; } return this.Init(); }