diff --git a/packages/@glimmer/integration-tests/lib/suites/in-element.ts b/packages/@glimmer/integration-tests/lib/suites/in-element.ts index d38119ff1..960377f7d 100644 --- a/packages/@glimmer/integration-tests/lib/suites/in-element.ts +++ b/packages/@glimmer/integration-tests/lib/suites/in-element.ts @@ -29,6 +29,29 @@ export class InElementSuite extends RenderTest { this.assertStableNodes(); } + @test + 'clears existing content'() { + let externalElement = this.delegate.createElement('div'); + let initialContent = '

Hello there!

'; + replaceHTML(externalElement, initialContent); + + this.render('{{#in-element externalElement}}[{{foo}}]{{/in-element}}', { + externalElement, + foo: 'Yippie!', + }); + + equalsElement(externalElement, 'div', {}, '[Yippie!]'); + this.assertStableRerender(); + + this.rerender({ foo: 'Double Yups!' }); + equalsElement(externalElement, 'div', {}, '[Double Yups!]'); + this.assertStableNodes(); + + this.rerender({ foo: 'Yippie!' }); + equalsElement(externalElement, 'div', {}, '[Yippie!]'); + this.assertStableNodes(); + } + @test 'Changing to falsey'() { let first = this.delegate.createElement('div'); @@ -79,10 +102,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(''); @@ -105,38 +131,17 @@ export class InElementSuite extends RenderTest { } @test - 'With nextSibling'() { + '`insertBefore` can only be null'() { 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', {}, 'Hello[Yippie!]there!'); - this.assertHTML(''); - this.assertStableRerender(); - - this.rerender({ foo: 'Double Yips!' }); - equalsElement(externalElement, 'div', {}, 'Hello[Double Yips!]there!'); - this.assertHTML(''); - this.assertStableNodes(); + let before = this.delegate.createElement('div'); - this.rerender({ nextSibling: null }); - equalsElement(externalElement, 'div', {}, 'Hellothere![Double Yips!]'); - this.assertHTML(''); - this.assertStableRerender(); - - this.rerender({ externalElement: null }); - equalsElement(externalElement, 'div', {}, 'Hellothere!'); - this.assertHTML(''); - this.assertStableRerender(); - - this.rerender({ externalElement, nextSibling: externalElement.lastChild, foo: 'Yippie!' }); - equalsElement(externalElement, 'div', {}, 'Hello[Yippie!]there!'); - this.assertHTML(''); - this.assertStableRerender(); + this.assert.throws(() => { + this.render('{{#in-element externalElement insertBefore=before}}[{{foo}}]{{/in-element}}', { + externalElement, + before, + foo: 'Yippie!', + }); + }, /insertBefore only takes `null` as an argument/); } @test diff --git a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts index 69f70986e..ed526446e 100644 --- a/packages/@glimmer/interfaces/lib/dom/attributes.d.ts +++ b/packages/@glimmer/interfaces/lib/dom/attributes.d.ts @@ -40,7 +40,7 @@ export interface DOMStack { pushRemoteElement( element: SimpleElement, guid: string, - 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 9e83bc848..b845d1530 100644 --- a/packages/@glimmer/node/lib/serialize-builder.ts +++ b/packages/@glimmer/node/lib/serialize-builder.ts @@ -96,13 +96,13 @@ class SerializeBuilder extends NewElementBuilder implements ElementBuilder { pushRemoteElement( element: SimpleElement, cursorId: string, - nextSibling: Option = null + insertBefore: Option = null ): Option { let { dom } = this; let script = dom.createElement('script'); script.setAttribute('glmr', cursorId); - dom.insertBefore(element, script, nextSibling); - return super.pushRemoteElement(element, cursorId, nextSibling); + dom.insertBefore(element, script, insertBefore); + return super.pushRemoteElement(element, cursorId, insertBefore); } } diff --git a/packages/@glimmer/opcode-compiler/lib/syntax/builtins.ts b/packages/@glimmer/opcode-compiler/lib/syntax/builtins.ts index c90ef1dd4..cf6b1e51b 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 === 'guid' || key === 'insertBefore') { actions.push(op('Expr', values[i])); } else { throw new Error(`SYNTAX ERROR: #in-element does not take a \`${keys[0]}\` option`); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 944cdd40b..006a37ee8 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -7,9 +7,8 @@ import { isConst, isConstTag, } from '@glimmer/reference'; -import { Option } from '@glimmer/util'; -import { check, CheckString, CheckElement, CheckNode, CheckOption } from '@glimmer/debug'; -import { Op } from '@glimmer/interfaces'; +import { check, CheckString, CheckElement } from '@glimmer/debug'; +import { Op, Option } from '@glimmer/interfaces'; import { $t0 } from '@glimmer/vm'; import { ModifierDefinition, @@ -22,7 +21,7 @@ import { Assert } from './vm'; import { DynamicAttribute } from '../../vm/attributes/dynamic'; import { CheckReference, CheckArguments, CheckOperations } from './-debug-strip'; import { CONSTANTS } from '../../symbols'; -import { SimpleElement, SimpleNode } from '@simple-dom/interface'; +import { SimpleElement } from '@simple-dom/interface'; APPEND_OPCODES.add(Op.Text, (vm, { op1: text }) => { vm.elements().appendText(vm[CONSTANTS].getString(text)); @@ -43,11 +42,10 @@ APPEND_OPCODES.add(Op.OpenDynamicElement, vm => { APPEND_OPCODES.add(Op.PushRemoteElement, vm => { let elementRef = check(vm.stack.pop(), CheckReference); - let nextSiblingRef = check(vm.stack.pop(), CheckReference); + let insertBeforeRef = check(vm.stack.pop(), CheckReference); let guidRef = check(vm.stack.pop(), CheckReference); let element: SimpleElement; - let nextSibling: Option; let guid = guidRef.value() as string; if (isConst(elementRef)) { @@ -58,15 +56,9 @@ APPEND_OPCODES.add(Op.PushRemoteElement, vm => { vm.updateWith(new Assert(cache)); } - if (isConst(nextSiblingRef)) { - nextSibling = check(nextSiblingRef.value(), CheckOption(CheckNode)); - } else { - let cache = new ReferenceCache(nextSiblingRef as Reference>); - nextSibling = check(cache.peek(), CheckOption(CheckNode)); - vm.updateWith(new Assert(cache)); - } + let insertBefore = insertBeforeRef.value() as Option; - let block = vm.elements().pushRemoteElement(element, guid, nextSibling); + let block = vm.elements().pushRemoteElement(element, guid, insertBefore); 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 66bd2fb37..8941fb4b1 100644 --- a/packages/@glimmer/runtime/lib/vm/element-builder.ts +++ b/packages/@glimmer/runtime/lib/vm/element-builder.ts @@ -205,18 +205,26 @@ export class NewElementBuilder implements ElementBuilder { pushRemoteElement( element: SimpleElement, guid: string, - nextSibling: Option = null + insertBefore: Option ): Option { - return this.__pushRemoteElement(element, guid, nextSibling); + return this.__pushRemoteElement(element, guid, insertBefore); } __pushRemoteElement( element: SimpleElement, _guid: string, - nextSibling: Option + insertBefore: Option ): Option { - this.pushElement(element, nextSibling); + this.pushElement(element, insertBefore); + + if (insertBefore === undefined) { + while (element.lastChild) { + element.removeChild(element.lastChild); + } + } + 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 d6634e832..c5ba914f3 100644 --- a/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts +++ b/packages/@glimmer/runtime/lib/vm/rehydrate-builder.ts @@ -379,15 +379,21 @@ export class RehydrateBuilder extends NewElementBuilder implements ElementBuilde __pushRemoteElement( element: SimpleElement, cursorId: string, - nextSibling: Option = null + insertBefore: Option ): Option { let marker = this.getMarker(element as HTMLElement, cursorId); if (marker.parentNode === element) { + if (insertBefore === undefined) { + while (element.lastChild !== marker) { + element.removeChild(element.lastChild!); + } + } + let currentCursor = this.currentCursor; let candidate = currentCursor!.candidate; - this.pushElement(element, nextSibling); + this.pushElement(element, insertBefore); currentCursor!.candidate = candidate; this.candidate = this.remove(marker); diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index 029b7ae9e..e86b50369 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -441,14 +441,18 @@ 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 === 'nextSibling') { - hasNextSibling = true; + if (pair.key === 'insertBefore') { + if (pair.value.type !== 'NullLiteral') { + throw new SyntaxError('insertBefore only takes `null` as an argument', loc); + } + + hasInsertBefore = true; } }); @@ -456,10 +460,10 @@ function addInElementHash(cursor: string, hash: AST.Hash, loc: AST.SourceLocatio let guidPair = b.pair('guid', guid); hash.pairs.unshift(guidPair); - if (!hasNextSibling) { - let nullLiteral = b.literal('NullLiteral', null); - let nextSibling = b.pair('nextSibling', nullLiteral); - hash.pairs.push(nextSibling); + if (!hasInsertBefore) { + let undefinedLiteral = b.literal('UndefinedLiteral', undefined); + let beforeSibling = b.pair('insertBefore', undefinedLiteral); + hash.pairs.push(beforeSibling); } return hash;