UNPKG

create-modulo

Version:

Starter projects for Modulo.html - Ready for all uses - Markdown-SSG / SSR / API-backed SPA

181 lines (153 loc) 7.69 kB
<!DOCTYPE html><meta charset=utf8><script src=../static/Modulo.html></script><script type=md>--- title: Custom Engines --- # Custom DOMCursor > **Terminology** - In the source code and docs, "child" refers to the existing > element, and "rival" refers to the reference element attempting to be matched > (e.g., virtual DOM). The role of the DOMCursor is to most "logically" match > children elements to rival (VDOM) elements. The DOMCursor is in charge of matching elements when a component reconciles. That is to say, when a component re-renders (e.g. a user clicks and modifies something), it attempts to match the the newly rendered element generated from the `dom` or `render` phase of the lifecycle (e.g. the virtual DOM), with it's original in the real, existing DOM. Modulo's modular design extends to DOM element pairing as well. By extending the simple built-in DOMCursor, we can include basic reconciler features such as keyed elements, like in React and other frameworks, or even more complex logic if we so desire. ## Motivation: Optimizing large components > **When to optimize** - Why do we want to do this? The purpose is if you know > that Modulo's automatic reconciliation "guesses" are causing your program to > be slow. This often happens when adding and removing elements in large, > complex components with many elements in a row with the same tag name. > Ideally, eleeents should simply be hidden or shown, so this is only useful if > thye are actually getting removed and inserted in the DOM, and the default > Modulo matching ends up caussing slow-downs. The main reason you will want to extend the DOMCursor class is for optimizing large components that update often. This can happen in UX-intensive applications, such as tables that get insertions, swaps, and deletions in the middle. By writing custom code to better matching element, we can make fewer DOM transformations. ## Example 1: Adding example simple custom logic In the following example, we show how you can add custom logic specific to your optimization needs or component, in this case to 1) skip over already-rendered elements with the `skip-me` attribute, and 2) compare other elements with the first element resolved when selecting with the `repalce-me-with` attribute. ```modulo <script Configuration> modulo.engine.CustomCursor = class CustomCursor extends modulo.engine.DOMCursor { next() { let [ child, rival ] = super.next() // Get default behavior if (child.hasAttribute('skip-me')) { // e.g. custom logic this.nextRival = rival // ensure rival gets repeated return this.next() // skip over child } if (rival.hasAttribute('replace-me-with')) { // e.g. other custom logic rival = document.querySelector(rival.getAttribute('replace-me-with')) } return [ child, rival ]; } } <-script> ``` ## Example 2: Upgrading to KeyedCursor for key= support By extending the above to keep track of "keyed" elements into the following class, we can create a `KeyedCursor` that supports the keyed element matching feature when activated, for huge speed improvements when used correctly, and only very little extra overhead compared to the default DOMCursor: ```modulo <script Configuration> modulo.engine.KeyCursor = class KeyCursor extends modulo.engine.DOMCursor { initialize(parentNode, parentRival) { super.initialize(parentNode, parentRival); this.keyedChildren = {}; // Setup an object for children with keys this.keyedRivals = {}; // and ditto for rivals } _getMatchedNode(elem, keyedElems, keyedOthers) { const key = elem && elem.nodeType === 1 && elem.getAttribute('key'); if (!key) { return null; } else if (key in keyedOthers) { const matched = keyedOthers[key]; delete keyedOthers[key]; return matched; } else if (key in keyedElems) { console.warn('MODULO: Duplicate key:', key); } keyedElems[key] = elem; return false; } _hasNextInExcessKeys() { // There were "excess", unmatched keyed elements if (!this.activeExcess) { // Convert needed appends and removes to array const child = Object.values(this.keyedChildren).map(v => [v, null]); const rival = Object.values(this.keyedRivals).map(v => [null, v]); this.activeExcess = rival.concat(child); // Do appends before remove } return this.activeExcess.length > 0; // Return true if there is any left } hasNext() { if (this.nextChild || this.nextRival || this._hasNextInExcessKeys()) { return true; // Is pointing at another node, or has unmatched keys } else { return super.hasNext(); // Fall back on DOMCursor logic for slots } } next() { if (!this.nextRival && this.activeExcess && this.activeExcess.length > 0) { return this.activeExcess.shift(); // Return first pair (base logic) } let [ child, rival ] = super.next() // Get default behavior let matchedRival = this._getMatchedNode(child, this.keyedChildren, this.keyedRivals); let matchedChild = this._getMatchedNode(rival, this.keyedRivals, this.keyedChildren); if (matchedRival === false) { // Child has a key, but does not match child = this.nextChild; // Simply ignore Child, and on to next this.nextChild = child ? child.nextSibling : null; } else if (matchedChild === false) { // Rival has key, but no match rival = this.nextRival; // IGNORE rival - move on to this._setNextRival(rival); // (and setup next-next rival) } const keyWasFound = matchedRival !== null || matchedChild !== null; const matchFound = matchedChild !== child && keyWasFound; if (matchFound && matchedChild) { // Rival matches, but not child this.nextChild = child; // "Undo" this last "nextChild" (return) child = matchedChild; // Then substitute the matched instead } if (matchFound && matchedRival) { // Child matches, but not with rival. Swap in the correct one. this.nextRival = rival; // "Undo" this last "nextRival" rival = matchedRival; // Then substitute the matched rival } return [ child, rival ]; } } // To enable universally: // modulo.cursor.DOMCursor = modulo.cursor.KeyCursor <-script> ``` <!-- runLifecycle(parts, renderObj, lifecycleNames) { if (this.inRender && this.inRender !== lifecycleNames) { return; } this.inRender = Array.from(lifecycleNames); while (this.inRender.length) { const methodName = this.inRender.shift() + 'Callback'; for (const [ name, obj ] of Object.entries(parts)) { if (!(methodName in obj)) { continue; // Skip if obj has not registered callback } const result = obj[methodName].call(obj, renderObj); if (result.then) { const doAsync = async () => { renderObj[obj.conf.RenderObj || obj.conf.Name] = await result; this.runLifecycle(parts, renderObj, this.inRender); }; return window.setTimeout(doAsync, 0); } if (result) { // TODO: Change to (result !== undefined) and test renderObj[obj.conf.RenderObj || obj.conf.Name] = result; } } } delete this.inRender -->