diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a126083..66504e58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## Unreleased + +- Added `patchBetween` to be able to run a patch function between to sibling ndoes. + ## 0.7.0 - Added an option to specify which attribute to use for a key when importing diff --git a/index.ts b/index.ts index 4a893cbe..57aafeba 100644 --- a/index.ts +++ b/index.ts @@ -15,7 +15,7 @@ */ export {applyAttr, applyProp, attributes,} from './src/attributes'; -export {alignWithDOM, alwaysDiffAttributes, close, createPatchInner, createPatchOuter, currentElement, currentContext, currentPointer, open, patchInner as patch, patchInner, patchOuter, skip, skipNode} from './src/core'; +export {alignWithDOM, alwaysDiffAttributes, close, createPatchInner, createPatchOuter, currentElement, currentContext, currentPointer, open, patchInner as patch, patchInner, patchOuter, patchBetween, skip, skipNode} from './src/core'; export {setKeyAttributeName} from './src/global'; export {clearCache,getKey, importNode, isDataInitialized} from './src/node_data'; export {notifications} from './src/notifications'; diff --git a/src/core.ts b/src/core.ts index 9b4e37bd..5f39a6ea 100644 --- a/src/core.ts +++ b/src/core.ts @@ -68,6 +68,8 @@ let currentNode: Node | null = null; let currentParent: Node | null = null; +let currentEndNode: Node | null = null; + let doc: Document | null = null; let focusPath: Array = []; @@ -141,6 +143,9 @@ function getMatchingNode( let cur: Node | null = matchNode; do { + if (cur === currentEndNode) { + return null; + } if (matches(cur, nameOrCtor, key)) { return cur; } @@ -364,6 +369,7 @@ function createPatcher( const prevAttrsBuilder = attrsBuilder; const prevCurrentNode = currentNode; const prevCurrentParent = currentParent; + const prevCurrentEndNode = currentEndNode; const prevMatchFn = matchFn; let previousInAttributes = false; let previousInSkip = false; @@ -400,6 +406,8 @@ function createPatcher( attrsBuilder = prevAttrsBuilder; currentNode = prevCurrentNode; currentParent = prevCurrentParent; + currentEndNode = prevCurrentEndNode; + focusPath = prevFocusPath; // Needs to be done after assertions because assertions rely on state @@ -478,6 +486,40 @@ function createPatchOuter( }, patchConfig); } +/** + * Creates a patcher that patches the document starting at node with a + * provided function. This function may be called during an existing patch operation. + * @param patchConfig The config to use for the patch. + * @returns The created function for patching an Element's children. + */ +function createPatchBetween(patchConfig?: PatchConfig) { + return (startNode: Node, endNode: Node, template: (a: T | undefined) => void, + data?: T | undefined) => { + const patcher = createPatcher((node, fn, data) => { + currentNode = startNode; + currentParent = startNode.parentNode; + currentEndNode = endNode; + + fn(data); + if (DEBUG) { + if (currentNode === null) { + assertNoUnclosedTags(currentParent, endNode); + } + } + if (currentNode && currentNode.nextSibling !== endNode) { + clearUnvisitedDOM(startNode.parentNode, currentNode.nextSibling, endNode); + } + if (DEBUG) { + if (currentNode && currentNode.nextSibling !== endNode) { + throw new Error("Leftover elements after patchBetween"); + } + } + return node as unknown as R; + }, patchConfig); + return patcher(startNode as Element, template, data); + }; +} + const patchInner: ( node: Element | DocumentFragment, template: (a: T | undefined) => void, @@ -488,6 +530,7 @@ const patchOuter: ( template: (a: T | undefined) => void, data?: T | undefined ) => Node | null = createPatchOuter(); +const patchBetween = createPatchBetween(); export { alignWithDOM, @@ -497,8 +540,10 @@ export { text, createPatchInner, createPatchOuter, + createPatchBetween, patchInner, patchOuter, + patchBetween, open, close, currentElement, diff --git a/test/functional/patchbetween_spec.ts b/test/functional/patchbetween_spec.ts new file mode 100644 index 00000000..02e0f4ea --- /dev/null +++ b/test/functional/patchbetween_spec.ts @@ -0,0 +1,233 @@ +/** + * Copyright 2015 The Incremental DOM Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + // taze: chai from //third_party/javascript/typings/chai + +import { + patch, + patchBetween, + elementOpen, + elementOpenStart, + elementOpenEnd, + elementClose, + elementVoid, + text +} from '../../index'; +import { + assertHTMLElement, +} from '../util/dom'; +const {expect} = chai; + + +describe('patching between', () => { + let container:HTMLElement; + + beforeEach(() => { + container = document.createElement('div'); + container.setAttribute('tid', 'container'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + it('should create the required DOM nodes between comments', () => { + const startNode = document.createComment('start'); + const endNode = document.createComment('end'); + container.append(startNode, endNode); + + patchBetween(startNode, endNode, () => { + elementOpen('div', null, null, 'id', 'aDiv'); + elementClose('div'); + elementOpen('div', null, null, 'id', 'bDiv'); + elementClose('div'); + }); + + expect(container.innerHTML).to.eq('
'); + expect(assertHTMLElement(container.childNodes[1]).id).to.equal('aDiv'); + expect(assertHTMLElement(container.childNodes[2]).id).to.equal('bDiv'); + }); + + it('should create the required DOM nodes between divs', () => { + const startNode = document.createElement('div'); + // startNode.setAttribute('tid', 'start'); + const endNode = document.createElement('div'); + // endNode.setAttribute('tid', 'end'); + container.append(startNode, endNode); + + patchBetween(startNode, endNode, () => { + elementOpen('div', null, null, 'id', 'aDiv'); + elementClose('div'); + elementOpen('div', null, null, 'id', 'bDiv'); + elementClose('div'); + }); + + expect(container.innerHTML).to.eq('
'); + expect(assertHTMLElement(container.childNodes[1]).id).to.equal('aDiv'); + expect(assertHTMLElement(container.childNodes[2]).id).to.equal('bDiv'); + }); + + describe('with an existing document tree', () => { + // TODO: add test w/ keyed items after the endNode + + let div:HTMLElement; + let startNode; + let endNode; + + function render() { + elementVoid('div', null, null, 'tabindex', '0'); + } + + beforeEach(function() { + div = document.createElement('div'); + div.setAttribute('tabindex', '-1'); + startNode = document.createComment('start'); + endNode = document.createComment('end'); + container.append(startNode, div, endNode); + }); + + it('should preserve existing nodes', () => { + patchBetween(startNode, endNode, render); + const child = container.childNodes[1]; + expect(child).to.equal(div); + }); + + describe('should return DOM node', () => { + let node:HTMLElement; + + it('from elementOpen', () => { + patchBetween(startNode, endNode, () => { + node = elementOpen('div'); + elementClose('div'); + }); + + expect(node).to.equal(div); + }); + + it('from elementClose', () => { + patchBetween(startNode, endNode, () => { + elementOpen('div'); + node = assertHTMLElement(elementClose('div')); + }); + + expect(node).to.equal(div); + }); + + it('from elementVoid', () => { + patchBetween(startNode, endNode, () => { + node = assertHTMLElement(elementVoid('div')); + }); + + expect(node).to.equal(div); + }); + + it('from elementOpenEnd', () => { + patchBetween(startNode, endNode, () => { + elementOpenStart('div'); + node = elementOpenEnd(); + elementClose('div'); + }); + + expect(node).to.equal(div); + }); + }); + }); + + it('should be re-entrant', () => { + const startNode = document.createElement('div'); + // startNode.setAttribute('tid', 'start'); + const endNode = document.createElement('div'); + // endNode.setAttribute('tid', 'end'); + container.append(startNode, endNode); + + patchBetween(startNode, endNode, () => { + const div1 = elementOpen('div', null, null, 'id', 'aDiv'); + elementClose('div'); + const div2 = elementOpen('div', null, null, 'id', 'bDiv'); + elementClose('div'); + patchBetween(div1, div2, () => { + elementOpen('div', null, null, 'id', 'cDiv'); + elementClose('div'); + }); + elementOpen('div', null, null, 'id', 'dDiv'); + elementClose('div'); + }); + + expect(container.innerHTML).to.eq( + '
'); + expect(assertHTMLElement(container.childNodes[1]).id).to.equal('aDiv'); + expect(assertHTMLElement(container.childNodes[2]).id).to.equal('cDiv'); + }); + + it('should pass third argument to render function', () => { + const startNode = document.createElement('div'); + const endNode = document.createElement('div'); + container.append(startNode, endNode); + + const render = (content:unknown) => { + text(content as string); + }; + + patchBetween(startNode, endNode, render, 'foobar'); + + expect(container.textContent).to.equal('foobar'); + }); + + it('should patch a detached node', () => { + const container = document.createElement('div'); + const startNode = document.createElement('div'); + const endNode = document.createElement('div'); + container.append(startNode, endNode); + + const render = () => { + elementVoid('span'); + }; + + patchBetween(startNode, endNode, render); + + expect(assertHTMLElement(container.childNodes[1]).tagName).to.equal('SPAN'); + }); + + it('should throw when an element is unclosed', () => { + const startNode = document.createComment('start'); + const endNode = document.createComment('end'); + container.append(startNode, endNode); + + expect(() => { + patchBetween(startNode, endNode, () => { + elementOpen('div'); + }); + }).to.throw('One or more tags were not closed:\ndiv'); + }); + +}); + +describe('patching a documentFragment', () => { + it('should create the required DOM nodes', () => { + const frag = document.createDocumentFragment(); + const startNode = document.createComment('start'); + const endNode = document.createComment('end'); + frag.append(startNode, endNode); + + patchBetween(startNode, endNode, () => { + elementOpen('div', null, null, 'id', 'aDiv'); + elementClose('div'); + }); + + expect(assertHTMLElement(frag.childNodes[1]).id).to.equal('aDiv'); + }); +});