diff --git a/test/test-template-engine.js b/test/test-template-engine.js index ae0a64f..f9b1f48 100644 --- a/test/test-template-engine.js +++ b/test/test-template-engine.js @@ -784,11 +784,14 @@ describe('html rendering', () => { assert(container.querySelector('#target').children[0] !== foo); }); + // TODO: #254: Uncomment “moves” lines when we leverage “moveBefore”. it('native map does not cause disconnectedCallback on prefix removal', () => { let connects = 0; + // let moves = 0; let disconnects = 0; class TestPrefixRemoval extends HTMLElement { connectedCallback() { connects++; } + // connectedMoveCallback() { moves++; } disconnectedCallback() { disconnects++; } } customElements.define('test-prefix-removal', TestPrefixRemoval); @@ -807,28 +810,35 @@ describe('html rendering', () => { document.body.append(container); assert(connects === 0); + // assert(moves === 0); assert(disconnects === 0); render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar' }] })); assert(connects === 2); + // assert(moves === 0); assert(disconnects === 0); render(container, getTemplate({ items: [{ id: 'bar' }] })); assert(connects === 2); + // assert(moves === 1); assert(disconnects === 1); render(container, getTemplate({ items: [] })); assert(connects === 2); + // assert(moves === 1); assert(disconnects === 2); container.remove(); }); + // TODO: #254: Uncomment “moves” lines when we leverage “moveBefore”. it('native map does not cause disconnectedCallback on suffix removal', () => { let connects = 0; + // let moves = 0; let disconnects = 0; class TestSuffixRemoval extends HTMLElement { connectedCallback() { connects++; } + // connectedMoveCallback() { moves++; } disconnectedCallback() { disconnects++; } } customElements.define('test-suffix-removal', TestSuffixRemoval); @@ -847,18 +857,22 @@ describe('html rendering', () => { document.body.append(container); assert(connects === 0); + // assert(moves === 0); assert(disconnects === 0); render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar' }] })); assert(connects === 2); + // assert(moves === 0); assert(disconnects === 0); render(container, getTemplate({ items: [{ id: 'foo' }] })); assert(connects === 2); + // assert(moves === 0); assert(disconnects === 1); render(container, getTemplate({ items: [] })); assert(connects === 2); + // assert(moves === 0); assert(disconnects === 2); container.remove(); @@ -867,9 +881,11 @@ describe('html rendering', () => { // TODO: #254: See https://chromestatus.com/feature/5135990159835136. it.todo('native map does not cause disconnectedCallback on list shuffle', () => { let connects = 0; + let moves = 0; let disconnects = 0; class TestListShuffle extends HTMLElement { connectedCallback() { connects++; } + connectedMoveCallback() { moves++; } disconnectedCallback() { disconnects++; } } customElements.define('test-list-shuffle', TestListShuffle); @@ -888,18 +904,22 @@ describe('html rendering', () => { document.body.append(container); assert(connects === 0); + assert(moves === 0); assert(disconnects === 0); render(container, getTemplate({ items: [{ id: 'foo' }, { id: 'bar' }] })); assert(connects === 2); + assert(moves === 0); assert(disconnects === 0); render(container, getTemplate({ items: [{ id: 'bar' }, { id: 'foo' }] })); assert(connects === 2); + assert(moves === 1); assert(disconnects === 0); render(container, getTemplate({ items: [] })); assert(connects === 2); + assert(moves === 1); assert(disconnects === 2); container.remove(); diff --git a/ts/x-element.d.ts.map b/ts/x-element.d.ts.map index 2555a1a..cb518f4 100644 --- a/ts/x-element.d.ts.map +++ b/ts/x-element.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"x-element.d.ts","sourceRoot":"","sources":["../x-element.js"],"names":[],"mappings":"AAEA,uDAAuD;AACvD;IACE;;;OAGG;IACH,iCAFa,MAAM,EAAE,CAKpB;IAED;;;OAGG;IACH,oCAFa;QAAC,CAAC,GAAG,EAAE,MAAM,YAAW;KAAC,CAIrC;IAED;;;;;;OAMG;IACH,6BAFa;QAAC,CAAC,GAAG,EAAE,MAAM,YAAW;KAAC,CAIrC;IAED;;;;;;;;;;;;;;;OAeG;IACH,qBAFa,aAAa,EAAE,CAI3B;IAED;;;;;;OAMG;IAEH;;;;;;;;;;;;;OAaG;IAEH;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,yBAFa;QAAC,CAAC,GAAG,EAAE,MAAM;mBA/BZ,GAAG;wBACH,MAAM;oBACN,MAAM,EAAE;;6BAVX,WAAW,SACX,GAAG,YACH,GAAG;sBAWA,OAAO;uBACP,OAAO;uBACP,OAAO;sBACP,GAAG,WAAS;sBACZ,GAAG,WAAS;UAsBW;KAAC,CAIrC;IAED;;;;;OAKG;IAEH;;;;;;;;;;;;;OAaG;IACH,wBAFa;QAAC,CAAC,GAAG,EAAE,MAAM,UAhBf,WAAW,SACX,KAAK,SAeoC;KAAC,CAIpD;IAED;;;;;OAKG;IACH,8BAHW,WAAW,GACT,WAAW,GAAC,UAAU,CAIlC;IAED;;;;;OAKG;IAEH;;;;;;;;;;;;OAYG;IACH,wCAHW;QAAC,CAAC,GAAG,EAAE,MAAM,YAAW;KAAC,gBAdzB,MAAM,QACN,WAAW,SAkBrB;IA+ID,0DA6BC;IAGD,yFAqCC;IAED,iFA2FC;IAED,0GAMC;IAGD,wFAaC;IAED,uFAQC;IAGD,iGAiBC;IAGD,yEAUC;IAGD,yEAkBC;IAGD,sEAUC;IAGD,yEAaC;IAGD,yEAsBC;IAGD,yEAYC;IAGD,8CAyCC;IAGD,8CA6CC;IAID,gDA6BC;IAGD,4CAMC;IAED,+CAEC;IAED,kDAyBC;IAGD,qDAMC;IAGD;;;MAmBC;IAED,kEAiBC;IAED,kGAGC;IAED,6CAMC;IAED,qGAGC;IAED,gDAMC;IAED,0DAMC;IAED,2CAYC;IAGD,+CAeC;IAED,2EAiBC;IAED,+DAGC;IAED,iFAQC;IAED,4EAQC;IAED,4EAOC;IAED,8EAoBC;IAGD,4DAEC;IAED,4CAEC;IAED,+CAEC;IAED,2DAOC;IAED,6CAKC;IAED,mDAAqC;IACrC,4CAA8B;IAC9B,yCAA8I;IAC9I,kGAA+D;IAC/D,sCAA4B;IAC5B,+CAAqF;IA50BrF;;OAEG;IACH,0BAEC;IAED;;;;;OAKG;IACH,oCAJW,MAAM,YACN,MAAM,GAAC,IAAI,SACX,MAAM,GAAC,IAAI,QAOrB;IAED;;OAEG;IACH,wBAAoB;IAEpB;;OAEG;IACH,6BAEC;IAED;;;;OAIG;IACH,eAYC;IAED;;;;OAIG;IAEH;;;;;;;OAOG;IACH,gBALW,WAAW,QACX,MAAM,oBAPN,KAAK,oBASL,MAAM,QAoBhB;IAED;;;;;;OAMG;IACH,kBALW,WAAW,QACX,MAAM,oBAlCN,KAAK,oBAoCL,MAAM,QAoBhB;IAED;;;OAGG;IACH,qBAFW,KAAK,QAMf;IAED;;;;;OAKG;IACH,gBAFa,MAAM,CAIlB;CA2sBF"} \ No newline at end of file +{"version":3,"file":"x-element.d.ts","sourceRoot":"","sources":["../x-element.js"],"names":[],"mappings":"AAEA,uDAAuD;AACvD;IACE;;;OAGG;IACH,iCAFa,MAAM,EAAE,CAKpB;IAED;;;OAGG;IACH,oCAFa;QAAC,CAAC,GAAG,EAAE,MAAM,YAAW;KAAC,CAIrC;IAED;;;;;;OAMG;IACH,6BAFa;QAAC,CAAC,GAAG,EAAE,MAAM,YAAW;KAAC,CAIrC;IAED;;;;;;;;;;;;;;;OAeG;IACH,qBAFa,aAAa,EAAE,CAI3B;IAED;;;;;;OAMG;IAEH;;;;;;;;;;;;;OAaG;IAEH;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,yBAFa;QAAC,CAAC,GAAG,EAAE,MAAM;mBA/BZ,GAAG;wBACH,MAAM;oBACN,MAAM,EAAE;;6BAVX,WAAW,SACX,GAAG,YACH,GAAG;sBAWA,OAAO;uBACP,OAAO;uBACP,OAAO;sBACP,GAAG,WAAS;sBACZ,GAAG,WAAS;UAsBW;KAAC,CAIrC;IAED;;;;;OAKG;IAEH;;;;;;;;;;;;;OAaG;IACH,wBAFa;QAAC,CAAC,GAAG,EAAE,MAAM,UAhBf,WAAW,SACX,KAAK,SAeoC;KAAC,CAIpD;IAED;;;;;OAKG;IACH,8BAHW,WAAW,GACT,WAAW,GAAC,UAAU,CAIlC;IAED;;;;;OAKG;IAEH;;;;;;;;;;;;OAYG;IACH,wCAHW;QAAC,CAAC,GAAG,EAAE,MAAM,YAAW;KAAC,gBAdzB,MAAM,QACN,WAAW,SAkBrB;IAqJD,0DA6BC;IAGD,yFAqCC;IAED,iFA2FC;IAED,0GAMC;IAGD,wFAaC;IAED,uFAQC;IAGD,iGAiBC;IAGD,yEAUC;IAGD,yEAkBC;IAGD,sEAUC;IAGD,yEAaC;IAGD,yEAsBC;IAGD,yEAYC;IAGD,8CAyCC;IAGD,8CA6CC;IAID,gDA6BC;IAGD,4CAMC;IAED,+CAEC;IAED,kDAyBC;IAGD,qDAMC;IAGD;;;MAmBC;IAED,kEAiBC;IAED,kGAGC;IAED,6CAMC;IAED,qGAGC;IAED,gDAMC;IAED,0DAMC;IAED,2CAYC;IAGD,+CAeC;IAED,2EAiBC;IAED,+DAGC;IAED,iFAQC;IAED,4EAQC;IAED,4EAOC;IAED,8EAoBC;IAGD,4DAEC;IAED,4CAEC;IAED,+CAEC;IAED,2DAOC;IAED,6CAKC;IAED,mDAAqC;IACrC,4CAA8B;IAC9B,yCAA8I;IAC9I,kGAA+D;IAC/D,sCAA4B;IAC5B,+CAAqF;IAl1BrF;;OAEG;IACH,0BAEC;IAQD;;;;;OAKG;IACH,oCAJW,MAAM,YACN,MAAM,GAAC,IAAI,SACX,MAAM,GAAC,IAAI,QAOrB;IAED;;OAEG;IACH,wBAAoB;IAEpB;;OAEG;IACH,6BAEC;IAED;;;;OAIG;IACH,eAYC;IAED;;;;OAIG;IAEH;;;;;;;OAOG;IACH,gBALW,WAAW,QACX,MAAM,oBAPN,KAAK,oBASL,MAAM,QAoBhB;IAED;;;;;;OAMG;IACH,kBALW,WAAW,QACX,MAAM,oBAlCN,KAAK,oBAoCL,MAAM,QAoBhB;IAED;;;OAGG;IACH,qBAFW,KAAK,QAMf;IAED;;;;;OAKG;IACH,gBAFa,MAAM,CAIlB;CA2sBF"} \ No newline at end of file diff --git a/ts/x-template.d.ts.map b/ts/x-template.d.ts.map index 4b3ef0c..15f1805 100644 --- a/ts/x-template.d.ts.map +++ b/ts/x-template.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"x-template.d.ts","sourceRoot":"","sources":["../x-template.js"],"names":[],"mappings":"AAsxBA,yBAA2E;AAC3E,uBAAuE;AAGvE,4BAAiF;AACjF,yBAA2E"} \ No newline at end of file +{"version":3,"file":"x-template.d.ts","sourceRoot":"","sources":["../x-template.js"],"names":[],"mappings":"AAk4BA,yBAA2E;AAC3E,uBAAuE;AAGvE,4BAAiF;AACjF,yBAA2E"} \ No newline at end of file diff --git a/x-element.js b/x-element.js index 41f4c88..16cf74f 100644 --- a/x-element.js +++ b/x-element.js @@ -172,6 +172,12 @@ export default class XElement extends HTMLElement { XElement.#connectHost(this); } + // TODO: #254: Uncomment once we leverage “moveBefore”. + // /** + // * Extends HTMLElement.prototype.connectedMoveCallback. + // */ + // connectedMoveCallback() {} + /** * Extends HTMLElement.prototype.attributeChangedCallback. * @param {string} attribute diff --git a/x-template.js b/x-template.js index 827f3f8..91c1ba4 100644 --- a/x-template.js +++ b/x-template.js @@ -314,113 +314,6 @@ class TemplateEngine { return targets; } - // Validates array item or map entry and returns an “id” and a “rawResult”. - static #parseListValue(value, index, category, ids) { - if (category === 'array') { - // Values should look like "". - const id = String(index); - const rawResult = value; - ids.add(id); - if (!TemplateEngine.#isRawResult(rawResult)) { - throw new Error(`Unexpected non-template value found in array item at ${index} "${rawResult}".`); - } - return [id, rawResult]; - } else { - // Values should look like "[, ]". - if (value.length !== 2) { - throw new Error(`Unexpected entry length found in map entry at ${index} with length "${value.length}".`); - } - const [id, rawResult] = value; - if (typeof id !== 'string') { - throw new Error(`Unexpected non-string key found in map entry at ${index} "${id}".`); - } - if (ids.has(id)) { - throw new Error(`Unexpected duplicate key found in map entry at ${index} "${id}".`); - } - ids.add(id); - if (!TemplateEngine.#isRawResult(rawResult)) { - throw new Error(`Unexpected non-template value found in map entry at ${index} "${rawResult}".`); - } - return [id, rawResult]; - } - } - - // TODO: #254: Use new “moveBefore” when available with cross-browser support. - // This enables us to preserve things like animations and prevent node - // disconnects. See https://chromestatus.com/feature/5135990159835136 - // Loops over given value array to either create-or-update a list of nodes. - static #list(node, startNode, values, category) { - const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE); - if (!arrayState.map) { - // There is no mapping in our state — we have a clean slate to work with. - TemplateEngine.#clearObject(arrayState); - arrayState.map = new Map(); - const ids = new Set(); // Populated in “parseListValue”. - let index = 0; - for (const value of values) { - const [id, rawResult] = TemplateEngine.#parseListValue(value, index, category, ids); - const cursors = TemplateEngine.#createCursors(node); - const preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true); - arrayState.map.set(id, { id, preparedResult, ...cursors }); - index++; - } - } else { - // TODO: Can we refactor this all into a _single_ loop? Right now, we do - // the following: - // 1. Loop once to add new things. - // 2. Loop a second time to remove old things. - // 3. Loop a third time to reorder (if we have a mapping). - - // A mapping has already been created — we need to update the items. - const ids = new Set(); // Populated in “parseListValue”. - let index = 0; - for (const value of values) { - const [id, rawResult] = TemplateEngine.#parseListValue(value, index, category, ids); - let item = arrayState.map.get(id); - if (item) { - if (!TemplateEngine.#canReuseDom(item.preparedResult, rawResult)) { - // Add new comment cursors before removing old comment cursors. - const cursors = TemplateEngine.#createCursors(item.startNode); - TemplateEngine.#removeThrough(item.startNode, item.node); - item.preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true); - item.startNode = cursors.startNode; - item.node = cursors.node; - } else { - TemplateEngine.#update(item.preparedResult, rawResult); - } - } else { - const cursors = TemplateEngine.#createCursors(node); - const preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true); - item = { id, preparedResult, ...cursors }; - arrayState.map.set(id, item); - } - index++; - } - for (const [id, item] of arrayState.map.entries()) { - if (!ids.has(id)) { - TemplateEngine.#removeThrough(item.startNode, item.node); - arrayState.map.delete(id); - } - } - let lastItem; - for (const id of ids) { - const item = arrayState.map.get(id); - // TODO: We should be able to make the following code more performant. - if (category === 'map') { - const referenceNode = lastItem ? lastItem.node.nextSibling : startNode.nextSibling; - if (referenceNode !== item.startNode) { - const nodesToMove = [item.startNode]; - while (nodesToMove[nodesToMove.length - 1] !== item.node) { - nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); - } - TemplateEngine.#insertAllBefore(referenceNode.parentNode, referenceNode, nodesToMove); - } - } - lastItem = item; - } - } - } - static #commitAttribute(node, name, value, lastValue) { const update = TemplateEngine.#symbolToUpdate.get(value); if (update) { @@ -477,40 +370,225 @@ class TemplateEngine { // node[name] = value; // } - static #commitContent(node, startNode, value, lastValue) { - const category = TemplateEngine.#getCategory(value); - const lastCategory = TemplateEngine.#getCategory(lastValue); - if (category !== lastCategory && lastValue !== TemplateEngine.#UNSET) { - // Reset content under certain conditions. E.g., `map` >> `null`. - const state = TemplateEngine.#getState(node, TemplateEngine.#STATE); - const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE); + static #commitContentResultValue(node, startNode, value) { + const state = TemplateEngine.#getState(node, TemplateEngine.#STATE); + const rawResult = value; + if (!TemplateEngine.#canReuseDom(state.preparedResult, rawResult)) { TemplateEngine.#removeBetween(startNode, node); TemplateEngine.#clearObject(state); + const preparedResult = TemplateEngine.#inject(rawResult, node, true); + state.preparedResult = preparedResult; + } else { + TemplateEngine.#update(state.preparedResult, rawResult); + } + } + + // Validates array value and returns a “rawResult”. + static #parseArrayValue(value, index) { + // Values should look like "". + const rawResult = value; + if (!TemplateEngine.#isRawResult(rawResult)) { + throw new Error(`Unexpected non-template value found in array item at ${index} "${rawResult}".`); + } + return rawResult; + } + + // Validates array entry and returns an “id” and a “rawResult”. + static #parseArrayEntry(entry, index, ids) { + // Entries should look like "[, ]". + if (entry.length !== 2) { + throw new Error(`Unexpected entry length found in map entry at ${index} with length "${entry.length}".`); + } + const [id, rawResult] = entry; + if (typeof id !== 'string') { + throw new Error(`Unexpected non-string key found in map entry at ${index} "${id}".`); + } + if (ids.has(id)) { + throw new Error(`Unexpected duplicate key found in map entry at ${index} "${id}".`); + } + ids.add(id); + if (!TemplateEngine.#isRawResult(rawResult)) { + throw new Error(`Unexpected non-template value found in map entry at ${index} "${rawResult}".`); + } + return [id, rawResult]; + } + + // Helper to create / insert “cursors” in managed array of nodes. + static #createArrayItem(node, id, rawResult) { + const cursors = TemplateEngine.#createCursors(node); + const preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true); + return { id, preparedResult, ...cursors }; + } + + // Helper to destroy, create, and replace “cursors” in managed array of nodes. + static #recreateArrayItem(item, rawResult) { + // Add new comment cursors before removing old comment cursors. + const cursors = TemplateEngine.#createCursors(item.startNode); + TemplateEngine.#removeThrough(item.startNode, item.node); + item.preparedResult = TemplateEngine.#inject(rawResult, cursors.node, true); + item.startNode = cursors.startNode; + item.node = cursors.node; + } + + // Loops over given array of “values” to manage an array of nodes. + static #commitContentArrayValues(node, startNode, values) { + const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE); + if (!arrayState.map) { + // There is no mapping in our state — create an empty one as our base. TemplateEngine.#clearObject(arrayState); + arrayState.map = new Map(); } - if (category === 'result') { - const state = TemplateEngine.#getState(node, TemplateEngine.#STATE); - const rawResult = value; - if (!TemplateEngine.#canReuseDom(state.preparedResult, rawResult)) { - TemplateEngine.#removeBetween(startNode, node); - TemplateEngine.#clearObject(state); - const preparedResult = TemplateEngine.#inject(rawResult, node, true); - state.preparedResult = preparedResult; + + if (values.length > 0 && arrayState.map.size > 0) { + // Update existing values. + for (let index = 0; index < Math.min(arrayState.map.size, values.length); index++) { + const id = String(index); + const value = values[index]; + const rawResult = TemplateEngine.#parseArrayValue(value, index); + const item = arrayState.map.get(id); + if (!TemplateEngine.#canReuseDom(item.preparedResult, rawResult)) { + TemplateEngine.#recreateArrayItem(item, rawResult); + } else { + TemplateEngine.#update(item.preparedResult, rawResult); + } + } + } + + if (values.length > arrayState.map.size) { + // Add new values. + for (let index = arrayState.map.size; index < values.length; index++) { + const id = String(index); + const value = values[index]; + const rawResult = TemplateEngine.#parseArrayValue(value, index); + const item = TemplateEngine.#createArrayItem(node, id, rawResult); + arrayState.map.set(id, item); + } + } + + if (arrayState.map.size > values.length) { + // Delete removed values. + const index = values.length; + const id = String(index); + const item = arrayState.map.get(id); + TemplateEngine.#removeThrough(item.startNode, node); + arrayState.map.delete(id); + } + } + + // Loops over given array of “entries” to manage an array of nodes. + static #commitContentArrayEntries(node, startNode, entries) { + const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE); + if (!arrayState.map) { + // There is no mapping in our state — create an empty one as our base. + TemplateEngine.#clearObject(arrayState); + arrayState.map = new Map(); + } + + // A mapping has already been created — we need to update the items. + const ids = new Set(); // Populated in “parseListValue”. + let index = 0; + for (const entry of entries) { + const [id, rawResult] = TemplateEngine.#parseArrayEntry(entry, index, ids); + let item = arrayState.map.get(id); + if (item) { + if (!TemplateEngine.#canReuseDom(item.preparedResult, rawResult)) { + TemplateEngine.#recreateArrayItem(item, rawResult); + } else { + TemplateEngine.#update(item.preparedResult, rawResult); + } } else { - TemplateEngine.#update(state.preparedResult, rawResult); + item = TemplateEngine.#createArrayItem(node, id, rawResult); + arrayState.map.set(id, item); } - } else if (category === 'array' || category === 'map') { - TemplateEngine.#list(node, startNode, value, category); - } else if (category === 'fragment') { - if (value.childElementCount === 0) { - throw new Error(`Unexpected child element count of zero for given DocumentFragment.`); + index++; + } + for (const [id, item] of arrayState.map.entries()) { + if (!ids.has(id)) { + TemplateEngine.#removeThrough(item.startNode, item.node); + arrayState.map.delete(id); } - const previousSibling = node.previousSibling; - if (previousSibling !== startNode) { - TemplateEngine.#removeBetween(startNode, node); + } + let lastItem; + for (const id of ids) { + const item = arrayState.map.get(id); + // TODO: We should be able to make the following code more performant. + const referenceNode = lastItem ? lastItem.node.nextSibling : startNode.nextSibling; + if (referenceNode !== item.startNode) { + const nodesToMove = [item.startNode]; + while (nodesToMove[nodesToMove.length - 1] !== item.node) { + nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); + } + TemplateEngine.#insertAllBefore(referenceNode.parentNode, referenceNode, nodesToMove); } - node.parentNode.insertBefore(value, node); - } else { + lastItem = item; + } + } + + // TODO: #254: Future state where the “moveBefore” API is better-supported. + // // Loops over given array of “entries” to manage an array of nodes. + // static #commitContentArrayEntries(node, startNode, entries) { + // const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE); + // if (!arrayState.map) { + // // There is no mapping in our state — create an empty one as our base. + // TemplateEngine.#clearObject(arrayState); + // arrayState.map = new Map(); + // } + // + // const idsToRemove = new Set(arrayState.map.keys()); + // const ids = new Set(); // Populated in “parseArrayEntry”. + // let reference = startNode.nextSibling; + // for (let index = 0; index < entries.length; index++) { + // const entry = entries[index]; + // const [id, rawResult] = TemplateEngine.#parseArrayEntry(entry, index, ids); + // let item = arrayState.map.get(id); + // if (item) { + // // Update existing item. + // idsToRemove.delete(id); + // if (!TemplateEngine.#canReuseDom(item.preparedResult, rawResult)) { + // const referenceWasStartNode = reference === item.startNode; + // TemplateEngine.#recreateArrayItem(item, rawResult); + // reference = referenceWasStartNode ? item.startNode : reference; + // } else { + // TemplateEngine.#update(item.preparedResult, rawResult); + // } + // } else { + // // Create new item. + // item = TemplateEngine.#createArrayItem(node, id, rawResult); + // arrayState.map.set(id, item); + // } + // // Move to the correct location + // if (item.startNode !== reference) { + // const nodesToMove = [item.startNode]; + // while (nodesToMove[nodesToMove.length - 1] !== item.node) { + // nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); + // } + // TemplateEngine.#moveAllBefore(reference.parentNode, reference, nodesToMove); + // } + // + // // Move our position forward. + // reference = item.node.nextSibling; + // } + // + // // Remove any ids which are not longer in the entries. + // for (const id of idsToRemove) { + // const item = arrayState.map.get(id); + // TemplateEngine.#removeThrough(item.startNode, item.node); + // arrayState.map.delete(id); + // } + // } + + static #commitContentFragmentValue(node, startNode, value) { + if (value.childElementCount === 0) { + throw new Error(`Unexpected child element count of zero for given DocumentFragment.`); + } + const previousSibling = node.previousSibling; + if (previousSibling !== startNode) { + TemplateEngine.#removeBetween(startNode, node); + } + node.parentNode.insertBefore(value, node); + } + + static #commitContentTextValue(node, startNode, value) { // TODO: Is there a way to more-performantly skip this init step? E.g., if // the prior value here was not “unset” and we didn’t just reset? We // could cache the target node in these cases or something? @@ -526,6 +604,25 @@ class TemplateEngine { } else { previousSibling.textContent = value; } + } + + static #commitContent(node, startNode, value, lastValue) { + const category = TemplateEngine.#getCategory(value); + const lastCategory = TemplateEngine.#getCategory(lastValue); + if (category !== lastCategory && lastValue !== TemplateEngine.#UNSET) { + // Reset content under certain conditions. E.g., `map` >> `null`. + const state = TemplateEngine.#getState(node, TemplateEngine.#STATE); + const arrayState = TemplateEngine.#getState(startNode, TemplateEngine.#ARRAY_STATE); + TemplateEngine.#removeBetween(startNode, node); + TemplateEngine.#clearObject(state); + TemplateEngine.#clearObject(arrayState); + } + switch (category) { + case 'result': TemplateEngine.#commitContentResultValue(node, startNode, value); break; + case 'array': TemplateEngine.#commitContentArrayValues(node, startNode, value); break; + case 'map': TemplateEngine.#commitContentArrayEntries(node, startNode, value); break; + case 'fragment': TemplateEngine.#commitContentFragmentValue(node, startNode, value); break; + default: TemplateEngine.#commitContentTextValue(node, startNode, value); break; } } @@ -678,6 +775,17 @@ class TemplateEngine { return { startNode, node }; } + // TODO: #254: Future state when we leverage “moveBefore”. + // static #moveAllBefore(parentNode, referenceNode, nodes) { + // // Iterate backwards over the live node collection since we’re mutating it. + // // Note that passing “null” as the reference node moves nodes to the end. + // for (let iii = nodes.length - 1; iii >= 0; iii--) { + // const node = nodes[iii]; + // parentNode.moveBefore(node, referenceNode); + // referenceNode = node; + // } + // } + static #insertAllBefore(parentNode, referenceNode, nodes) { // Iterate backwards over the live node collection since we’re mutating it. // Note that passing “null” as the reference node appends nodes to the end.