From 3ec8da9fe067f27fc6a1aabc6bd1c66778a057f5 Mon Sep 17 00:00:00 2001 From: Chad Hietala Date: Mon, 11 Feb 2019 11:48:03 -0500 Subject: [PATCH] [FEATURE | BREAKING] Change semantics of in-element --- .../@glimmer/debug/lib/opcode-metadata.ts | 2 +- .../lib/suites/in-element.ts | 47 +++++++++++++++++-- .../interfaces/lib/dom/attributes.d.ts | 3 +- .../@glimmer/node/lib/serialize-builder.ts | 5 +- .../opcode-compiler/lib/syntax/builtins.ts | 4 +- .../runtime/lib/compiled/opcodes/dom.ts | 5 +- .../runtime/lib/vm/element-builder.ts | 16 +++++-- .../runtime/lib/vm/rehydrate-builder.ts | 9 +++- .../lib/parser/handlebars-node-visitors.ts | 15 ++++++ 9 files changed, 90 insertions(+), 16 deletions(-) diff --git a/packages/@glimmer/debug/lib/opcode-metadata.ts b/packages/@glimmer/debug/lib/opcode-metadata.ts index 44252a4c78..3e11fd1d15 100644 --- a/packages/@glimmer/debug/lib/opcode-metadata.ts +++ b/packages/@glimmer/debug/lib/opcode-metadata.ts @@ -549,7 +549,7 @@ METADATA[Op.PushRemoteElement] = { name: 'PushRemoteElement', mnemonic: 'apnd_remotetag', before: null, - stackChange: -3, + stackChange: -4, ops: [], operands: 0, check: true, diff --git a/packages/@glimmer/integration-tests/lib/suites/in-element.ts b/packages/@glimmer/integration-tests/lib/suites/in-element.ts index d38119ff1a..27a31e0b7d 100644 --- a/packages/@glimmer/integration-tests/lib/suites/in-element.ts +++ b/packages/@glimmer/integration-tests/lib/suites/in-element.ts @@ -79,10 +79,13 @@ export class InElementSuite extends RenderTest { let initialContent = '

Hello there!

'; replaceHTML(externalElement, initialContent); - this.render(stripTight`{{#in-element externalElement}}[{{foo}}]{{/in-element}}`, { - externalElement, - foo: 'Yippie!', - }); + this.render( + stripTight`{{#in-element externalElement insertBefore=null}}[{{foo}}]{{/in-element}}`, + { + externalElement, + foo: 'Yippie!', + } + ); equalsElement(externalElement, 'div', {}, `${initialContent}[Yippie!]`); this.assertHTML(''); @@ -107,13 +110,47 @@ export class InElementSuite extends RenderTest { @test 'With nextSibling'() { let externalElement = this.delegate.createElement('div'); - replaceHTML(externalElement, 'Hellothere!'); this.render( stripTight`{{#in-element externalElement nextSibling=nextSibling}}[{{foo}}]{{/in-element}}`, { externalElement, nextSibling: externalElement.lastChild, foo: 'Yippie!' } ); + equalsElement(externalElement, 'div', {}, '[Yippie!]'); + this.assertHTML(''); + this.assertStableRerender(); + + this.rerender({ foo: 'Double Yips!' }); + equalsElement(externalElement, 'div', {}, '[Double Yips!]'); + this.assertHTML(''); + this.assertStableNodes(); + + this.rerender({ nextSibling: null }); + equalsElement(externalElement, 'div', {}, '[Double Yips!]'); + this.assertHTML(''); + this.assertStableRerender(); + + this.rerender({ externalElement: null }); + equalsElement(externalElement, 'div', {}, ''); + this.assertHTML(''); + this.assertStableRerender(); + + this.rerender({ externalElement, nextSibling: externalElement.lastChild, foo: 'Yippie!' }); + equalsElement(externalElement, 'div', {}, '[Yippie!]'); + this.assertHTML(''); + this.assertStableRerender(); + } + + @test + 'With nextSibling (retain content)'() { + let externalElement = this.delegate.createElement('div'); + replaceHTML(externalElement, 'Hellothere!'); + + this.render( + stripTight`{{#in-element externalElement nextSibling=nextSibling insertBefore=null}}[{{foo}}]{{/in-element}}`, + { externalElement, nextSibling: externalElement.lastChild, foo: 'Yippie!' } + ); + equalsElement(externalElement, 'div', {}, 'Hello[Yippie!]there!'); this.assertHTML(''); this.assertStableRerender(); diff --git a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts index 69f70986e4..ffa6a56747 100644 --- a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts +++ b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts @@ -40,7 +40,8 @@ export interface DOMStack { pushRemoteElement( element: SimpleElement, guid: string, - nextSibling: Option + nextSibling: Option, + insertBefore: Option ): Option; popRemoteElement(): void; popElement(): void; diff --git a/packages/@glimmer/node/lib/serialize-builder.ts b/packages/@glimmer/node/lib/serialize-builder.ts index 9e83bc8488..1b93827b6e 100644 --- a/packages/@glimmer/node/lib/serialize-builder.ts +++ b/packages/@glimmer/node/lib/serialize-builder.ts @@ -96,13 +96,14 @@ class SerializeBuilder extends NewElementBuilder implements ElementBuilder { pushRemoteElement( element: SimpleElement, cursorId: string, - nextSibling: Option = null + nextSibling: Option = null, + _insertBefore: Option ): Option { let { dom } = this; let script = dom.createElement('script'); script.setAttribute('glmr', cursorId); dom.insertBefore(element, script, nextSibling); - return super.pushRemoteElement(element, cursorId, nextSibling); + return super.pushRemoteElement(element, cursorId, nextSibling, null); } } diff --git a/packages/@glimmer/opcode-compiler/lib/syntax/builtins.ts b/packages/@glimmer/opcode-compiler/lib/syntax/builtins.ts index c90ef1dd46..3c1fb4fd4b 100644 --- a/packages/@glimmer/opcode-compiler/lib/syntax/builtins.ts +++ b/packages/@glimmer/opcode-compiler/lib/syntax/builtins.ts @@ -173,7 +173,7 @@ export function populateBuiltins( for (let i = 0; i < keys.length; i++) { let key = keys[i]; - if (key === 'nextSibling' || key === 'guid') { + if (key === 'nextSibling' || key === 'guid' || key === 'insertBefore') { actions.push(op('Expr', values[i])); } else { throw new Error(`SYNTAX ERROR: #in-element does not take a \`${keys[0]}\` option`); @@ -182,7 +182,7 @@ export function populateBuiltins( actions.push(op('Expr', params[0]), op(Op.Dup, $sp, 0)); - return { count: 4, actions }; + return { count: 5, actions }; }, ifTrue() { diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 944cdd40b9..23d88fa6b3 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -43,6 +43,7 @@ APPEND_OPCODES.add(Op.OpenDynamicElement, vm => { APPEND_OPCODES.add(Op.PushRemoteElement, vm => { let elementRef = check(vm.stack.pop(), CheckReference); + let insertBefore = check(vm.stack.pop(), CheckReference); let nextSiblingRef = check(vm.stack.pop(), CheckReference); let guidRef = check(vm.stack.pop(), CheckReference); @@ -66,7 +67,9 @@ APPEND_OPCODES.add(Op.PushRemoteElement, vm => { vm.updateWith(new Assert(cache)); } - let block = vm.elements().pushRemoteElement(element, guid, nextSibling); + let shouldClear = insertBefore.value(); + + let block = vm.elements().pushRemoteElement(element, guid, nextSibling, shouldClear); if (block) vm.associateDestroyable(block); }); diff --git a/packages/@glimmer/runtime/lib/vm/element-builder.ts b/packages/@glimmer/runtime/lib/vm/element-builder.ts index 66bd2fb37d..d2080e6b3c 100644 --- a/packages/@glimmer/runtime/lib/vm/element-builder.ts +++ b/packages/@glimmer/runtime/lib/vm/element-builder.ts @@ -205,18 +205,28 @@ export class NewElementBuilder implements ElementBuilder { pushRemoteElement( element: SimpleElement, guid: string, - nextSibling: Option = null + nextSibling: Option = null, + insertBefore: Option ): Option { - return this.__pushRemoteElement(element, guid, nextSibling); + return this.__pushRemoteElement(element, guid, nextSibling, insertBefore); } __pushRemoteElement( element: SimpleElement, _guid: string, - nextSibling: Option + nextSibling: Option, + insertBefore: Option ): Option { this.pushElement(element, nextSibling); + + if (insertBefore !== null) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + } + let block = new RemoteLiveBlock(element); + return this.pushLiveBlock(block, true); } diff --git a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts index d6634e8325..a6d2da567f 100644 --- a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts +++ b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts @@ -379,11 +379,18 @@ export class RehydrateBuilder extends NewElementBuilder implements ElementBuilde __pushRemoteElement( element: SimpleElement, cursorId: string, - nextSibling: Option = null + nextSibling: Option = null, + insertBefore: Option ): Option { let marker = this.getMarker(element as HTMLElement, cursorId); if (marker.parentNode === element) { + if (insertBefore !== null) { + while (element.lastChild !== marker) { + element.removeChild(element.lastChild!); + } + } + let currentCursor = this.currentCursor; let candidate = currentCursor!.candidate; diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index 029b7ae9eb..44849cfc93 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -442,11 +442,20 @@ function addElementModifier(element: Tag<'StartTag'>, mustache: AST.MustacheStat function addInElementHash(cursor: string, hash: AST.Hash, loc: AST.SourceLocation) { let hasNextSibling = false; + let hasInsertBefore = false; hash.pairs.forEach(pair => { if (pair.key === 'guid') { throw new SyntaxError('Cannot pass `guid` from user space', loc); } + if (pair.key === 'insertBefore') { + if (pair.value.type !== 'NullLiteral') { + throw new SyntaxError('insertBefore only takes `null` as an argument', loc); + } + + hasInsertBefore = true; + } + if (pair.key === 'nextSibling') { hasNextSibling = true; } @@ -462,6 +471,12 @@ function addInElementHash(cursor: string, hash: AST.Hash, loc: AST.SourceLocatio hash.pairs.push(nextSibling); } + if (!hasInsertBefore) { + let undefinedLiteral = b.literal('UndefinedLiteral', undefined); + let beforeSibling = b.pair('insertBefore', undefinedLiteral); + hash.pairs.push(beforeSibling); + } + return hash; }