UNPKG

taggedjs

Version:

tagged template reactive html

305 lines 12.3 kB
import { escapeVariable, variablePrefix } from './Tag.class.js'; import { deepClone } from '../deepFunctions.js'; import { isTagComponent } from '../isInstance.js'; import { cloneValueArray } from './cloneValueArray.function.js'; import { restoreTagMarker } from './checkDestroyPrevious.function.js'; import { runBeforeDestroy } from './tagRunner.js'; import { getChildTagsToDestroy } from './destroy.support.js'; import { elementDestroyCheck } from './elementDestroyCheck.function.js'; import { updateContextItem } from './update/updateContextItem.function.js'; import { processNewValue } from './update/processNewValue.function.js'; import { setTagPlaceholder } from './setTagPlaceholder.function.js'; import { interpolateElement, interpolateString } from '../interpolations/interpolateElement.js'; import { subscribeToTemplate } from '../interpolations/interpolateTemplate.js'; import { afterInterpolateElement } from '../interpolations/afterInterpolateElement.function.js'; import { Subject } from '../subject/Subject.class.js'; const prefixSearch = new RegExp(variablePrefix, 'g'); /** used only for apps, otherwise use TagSupport */ export class BaseTagSupport { templater; subject; isApp = true; appElement; // only seen on this.getAppTagSupport().appElement strings; values; propsConfig; // stays with current render memory = { state: [], }; // travels with all rerenderings global = { destroy$: new Subject(), context: {}, // populated after reading interpolated.values array converted to an object {variable0, variable:1} providers: [], /** Indicator of re-rending. Saves from double rending something already rendered */ renderCount: 0, subscriptions: [], oldest: this, blocked: [], // renders that did not occur because an event was processing childTags: [], // tags on me clones: [], // elements on document. Needed at destroy process to know what to destroy }; hasLiveElements = false; constructor(templater, subject, castedProps) { this.templater = templater; this.subject = subject; const props = templater.props; // natural props this.propsConfig = this.clonePropsBy(props, castedProps); } clonePropsBy(props, castedProps) { const children = this.templater.children; // children tags passed in as arguments const kidValue = children.value; const latestCloned = props.map(props => deepClone(props)); return this.propsConfig = { latest: props, latestCloned, // assume its HTML children and then detect castProps: castedProps, //?? castProps(props, this, this.memory.state), lastClonedKidValues: kidValue.map(kid => { const cloneValues = cloneValueArray(kid.values); return cloneValues; }) }; } /** Function that kicks off actually putting tags down as HTML elements */ buildBeforeElement(insertBefore, options = { counts: { added: 0, removed: 0 }, }) { const subject = this.subject; const global = this.global; global.insertBefore = insertBefore; if (!global.placeholder) { setTagPlaceholder(global); } const placeholderElm = global.placeholder; global.oldest = this; global.newest = this; subject.tagSupport = this; this.hasLiveElements = true; const context = this.update(); const template = this.getTemplate(); const elementContainer = document.createDocumentFragment(); const tempDraw = document.createElement('template'); tempDraw.innerHTML = template.string; elementContainer.appendChild(tempDraw); // Search/replace innerHTML variables but don't interpolate tag components just yet const { tagComponents } = interpolateElement(elementContainer, context, template, this, // ownerSupport, { counts: options.counts }); afterInterpolateElement(elementContainer, placeholderElm, this, // ownerSupport context, options); // Any tag components that were found should be processed AFTER the owner processes its elements. Avoid double processing of elements attributes like (oninit)=${} const length = tagComponents.length; for (let index = 0; index < length; ++index) { const tagComponent = tagComponents[index]; subscribeToTemplate(tagComponent.insertBefore, tagComponent.subject, tagComponent.ownerSupport, options.counts); afterInterpolateElement(elementContainer, tagComponent.insertBefore, tagComponent.ownerSupport, context, options); } } getTemplate() { const thisTag = this.templater.tag; const strings = this.strings || thisTag.strings; const values = this.values || thisTag.values; const string = strings.map((string, index) => { const safeString = string.replace(prefixSearch, escapeVariable); const endString = safeString + (values.length > index ? `{${variablePrefix}${index}}` : ''); const trimString = endString.replace(/>\s*/g, '>').replace(/\s*</g, '<'); return trimString; }).join(''); const interpolation = interpolateString(string); return { interpolation, string: interpolation.string, strings, values, context: this.global.context || {}, }; } update() { return this.updateContext(this.global.context); } updateContext(context) { const thisTag = this.templater.tag; const strings = this.strings || thisTag.strings; const values = this.values || thisTag.values; strings.forEach((_string, index) => { const hasValue = values.length > index; if (!hasValue) { return; } const variableName = variablePrefix + index; const value = values[index]; // is something already there? const exists = variableName in context; if (exists) { if (this.global.deleted) { const valueSupport = (value && value.tagSupport); if (valueSupport) { valueSupport.destroy(); return context; // item was deleted, no need to emit } } return updateContextItem(context, variableName, value); } // 🆕 First time values below context[variableName] = processNewValue(value, this); }); return context; } updateBy(tagSupport) { const tempTag = tagSupport.templater.tag; this.updateConfig(tempTag.strings, tempTag.values); } updateConfig(strings, values) { this.strings = strings; this.updateValues(values); } updateValues(values) { this.values = values; return this.updateContext(this.global.context); } destroy(options = { stagger: 0, }) { const global = this.global; const childTags = options.byParent ? [] : getChildTagsToDestroy(this.global.childTags); if (isTagComponent(this.templater)) { global.destroy$.next(); runBeforeDestroy(this, this); } this.destroySubscriptions(); // signify immediately child has been deleted (looked for during event processing) for (let index = childTags.length - 1; index >= 0; --index) { const child = childTags[index]; const subGlobal = child.global; delete subGlobal.newest; subGlobal.deleted = true; if (isTagComponent(child.templater)) { runBeforeDestroy(child, child); } child.destroySubscriptions(); } let mainPromise; // FIRST DOM Manipulation to cause painting cycle checkRestoreTagMarker(this, options); const { stagger, promise } = this.destroyClones(options); options.stagger = stagger; if (promise) { mainPromise = promise; } resetTagSupport(this); if (mainPromise) { return mainPromise.then(async () => { const promises = childTags.map(kid => kid.childDestroy()); return Promise.all(promises); }).then(() => options.stagger); } return Promise.all(childTags.map(kid => kid.childDestroy())).then(() => options.stagger); } childDestroy(options = { stagger: 0, }) { // const global = this.global this.destroyClones(); resetTagSupport(this); return Promise.resolve(options.stagger); } destroyClones({ stagger } = { stagger: 0, }) { const oldClones = [...this.global.clones]; this.global.clones.length = 0; // tag maybe used for something else const promises = oldClones.map(clone => this.checkCloneRemoval(clone, stagger)).filter(x => x); // only return promises // check subjects that may have clones attached to them const oldContext = this.global.context; for (const name in oldContext) { const value = oldContext[name]; const clone = value.clone; if (clone?.parentNode) { clone.parentNode.removeChild(clone); } } if (promises.length) { return { promise: Promise.all(promises), stagger }; } return { stagger }; } /** Reviews elements for the presences of ondestroy */ checkCloneRemoval(clone, stagger) { let promise; const customElm = clone; if (customElm.ondestroy) { promise = elementDestroyCheck(customElm, stagger); } const next = () => { const parentNode = clone.parentNode; if (parentNode) { parentNode.removeChild(clone); } const ownerSupport = this.ownerTagSupport; if (ownerSupport) { // Sometimes my clones were first registered to my owner, remove them from owner ownerSupport.global.clones = ownerSupport.global.clones.filter(compareClone => compareClone !== clone); } }; if (promise instanceof Promise) { return promise.then(next); } else { next(); } return promise; } destroySubscriptions() { const subs = this.global.subscriptions; for (let index = subs.length - 1; index >= 0; --index) { subs[index].unsubscribe(); } subs.length = 0; } } export class TagSupport extends BaseTagSupport { templater; ownerTagSupport; subject; version; isApp = false; constructor(templater, // at runtime rendering of a tag, it needs to be married to a new TagSupport() ownerTagSupport, subject, castedProps, version = 0) { super(templater, subject, castedProps); this.templater = templater; this.ownerTagSupport = ownerTagSupport; this.subject = subject; this.version = version; } getAppTagSupport() { let tag = this; while (tag.ownerTagSupport) { tag = tag.ownerTagSupport; } return tag; } } export function checkRestoreTagMarker(support, options) { const global = support.global; // HTML DOM manipulation. Put back down the template tag const insertBefore = global.insertBefore; if (insertBefore.nodeName === 'TEMPLATE') { const placeholder = global.placeholder; if (placeholder && !('arrayValue' in support.memory)) { restoreTagMarker(support); } } } export function resetTagSupport(support) { const global = support.global; delete global.placeholder; global.context = {}; delete global.oldest; // may not be needed delete global.newest; support.global.childTags.length = 0; const subject = support.subject; delete subject.tagSupport; } //# sourceMappingURL=TagSupport.class.js.map