From ad19235f0ffcb6af3f0d938ef36973eb0838f978 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Sun, 25 Sep 2016 10:18:57 -0400 Subject: [PATCH] Continue fleshing out `{{-in-element`. --- packages/glimmer-runtime/index.ts | 4 + packages/glimmer-runtime/lib/builder.ts | 37 +- .../lib/compiled/opcodes/builder.ts | 12 + .../lib/compiled/opcodes/dom.ts | 29 +- .../lib/syntax/builtins/in-element.ts | 34 ++ .../lib/syntax/builtins/with-dynamic-vars.ts | 8 +- .../tests/ember-component-test.ts | 16 +- .../glimmer-runtime/tests/in-element-test.ts | 401 ++++++++++++++++++ .../glimmer-test-helpers/lib/environment.ts | 3 + 9 files changed, 515 insertions(+), 29 deletions(-) create mode 100644 packages/glimmer-runtime/lib/syntax/builtins/in-element.ts create mode 100644 packages/glimmer-runtime/tests/in-element-test.ts diff --git a/packages/glimmer-runtime/index.ts b/packages/glimmer-runtime/index.ts index d32d688528..a5f80899fa 100644 --- a/packages/glimmer-runtime/index.ts +++ b/packages/glimmer-runtime/index.ts @@ -105,6 +105,10 @@ export { default as WithDynamicVarsSyntax } from './lib/syntax/builtins/with-dynamic-vars'; +export { + default as InElementSyntax +} from './lib/syntax/builtins/in-element'; + export { PublicVM as VM, UpdatingVM, RenderResult } from './lib/vm'; export { SafeString, isSafeString } from './lib/upsert'; diff --git a/packages/glimmer-runtime/lib/builder.ts b/packages/glimmer-runtime/lib/builder.ts index e2859f289e..6a918ed0f6 100644 --- a/packages/glimmer-runtime/lib/builder.ts +++ b/packages/glimmer-runtime/lib/builder.ts @@ -27,11 +27,7 @@ export interface LastNode { } class First { - private node: Node; - - constructor(node) { - this.node = node; - } + constructor(private node: Node) { } firstNode(): Node { return this.node; @@ -39,11 +35,7 @@ class First { } class Last { - private node: Node; - - constructor(node) { - this.node = node; - } + constructor(private node: Node) { } lastNode(): Node { return this.node; @@ -151,12 +143,15 @@ export class ElementStack implements Cursor { return tracker; } - private pushBlockTracker(tracker: Tracker) { + private pushBlockTracker(tracker: Tracker, isRemote = false) { let current = this.blockStack.current; if (current !== null) { current.newDestroyable(tracker); - current.newBounds(tracker); + + if (!isRemote) { + current.newBounds(tracker); + } } this.blockStack.push(tracker); @@ -207,9 +202,13 @@ export class ElementStack implements Cursor { pushRemoteElement(element: Simple.Element) { this.pushElement(element); - let tracker = new SimpleBlockTracker(this.element); - this.pushBlockTracker(tracker); - return tracker; + let tracker = new RemoteBlockTracker(element); + this.pushBlockTracker(tracker, true); + } + + popRemoteElement() { + this.popBlock(); + this.popElement(); } private pushElement(element: Simple.Element) { @@ -346,6 +345,14 @@ export class SimpleBlockTracker implements Tracker { } } +class RemoteBlockTracker extends SimpleBlockTracker { + destroy() { + super.destroy(); + + clear(this); + } +} + export interface UpdatableTracker extends Tracker { reset(env: Environment); } diff --git a/packages/glimmer-runtime/lib/compiled/opcodes/builder.ts b/packages/glimmer-runtime/lib/compiled/opcodes/builder.ts index 17f1eb31d4..9a6a255c4c 100644 --- a/packages/glimmer-runtime/lib/compiled/opcodes/builder.ts +++ b/packages/glimmer-runtime/lib/compiled/opcodes/builder.ts @@ -223,6 +223,18 @@ export abstract class BasicOpcodeBuilder extends StatementCompilationBufferProxy // vm + pushRemoteElement() { + this.append(new dom.PushRemoteElementOpcode()); + } + + popRemoteElement() { + this.append(new dom.PopRemoteElementOpcode()); + } + + popElement() { + this.append(new dom.PopElementOpcode()); + } + label(name: string) { this.append(this.labelFor(name)); } diff --git a/packages/glimmer-runtime/lib/compiled/opcodes/dom.ts b/packages/glimmer-runtime/lib/compiled/opcodes/dom.ts index 70f773312a..2dc603b005 100644 --- a/packages/glimmer-runtime/lib/compiled/opcodes/dom.ts +++ b/packages/glimmer-runtime/lib/compiled/opcodes/dom.ts @@ -21,6 +21,7 @@ import { ValueReference } from '../../compiled/expressions/value'; import { CompiledArgs, EvaluatedArgs } from '../../compiled/expressions/args'; import { AttributeManager } from '../../dom/attribute-managers'; import { ElementOperations } from '../../builder'; +import { Assert } from './vm'; export class TextOpcode extends Opcode { public type = "text"; @@ -66,8 +67,8 @@ export class PushRemoteElementOpcode extends Opcode { public type = "push-remote-element"; evaluate(vm: VM) { - let reference = vm.frame.getOperand(); - let cache = isConst(reference) ? undefined : new ReferenceCache(reference); + let reference = vm.frame.getOperand(); + let cache = isConstReference(reference) ? undefined : new ReferenceCache(reference); let element = cache ? cache.peek() : reference.value(); vm.stack().pushRemoteElement(element); @@ -81,11 +82,19 @@ export class PushRemoteElementOpcode extends Opcode { return { guid: this._guid, type: this.type, - args: [JSON.stringify(this.tag)] + args: ['$OPERAND'] }; } } +export class PopRemoteElementOpcode extends Opcode { + public type = "pop-remote-element"; + + evaluate(vm: VM) { + vm.stack().popRemoteElement(); + } +} + export class OpenComponentElementOpcode extends Opcode { public type = "open-component-element"; @@ -369,6 +378,20 @@ export class CloseElementOpcode extends Opcode { } } +export class PopElementOpcode extends Opcode { + public type = "pop-element"; + + evaluate(vm: VM) { + vm.stack().popElement(); + } +} + +export interface StaticAttrOptions { + namespace: string; + name: string; + value: string; +} + export class StaticAttrOpcode extends Opcode { public type = "static-attr"; diff --git a/packages/glimmer-runtime/lib/syntax/builtins/in-element.ts b/packages/glimmer-runtime/lib/syntax/builtins/in-element.ts new file mode 100644 index 0000000000..2a365f3c52 --- /dev/null +++ b/packages/glimmer-runtime/lib/syntax/builtins/in-element.ts @@ -0,0 +1,34 @@ +import { + Statement as StatementSyntax +} from '../../syntax'; + +import OpcodeBuilderDSL from '../../compiled/opcodes/builder'; +import * as Syntax from '../core'; +import Environment from '../../environment'; + +export default class InElementSyntax extends StatementSyntax { + type = "in-element-statement"; + + public args: Syntax.Args; + public templates: Syntax.Templates; + public isStatic = false; + + constructor({ args, templates }: { args: Syntax.Args, templates: Syntax.Templates }) { + super(); + this.args = args; + this.templates = templates; + } + + compile(dsl: OpcodeBuilderDSL, env: Environment) { + let { args, templates } = this; + + dsl.block({ templates, args }, (dsl, BEGIN, END) => { + dsl.putArgs(args); + dsl.test('simple'); + dsl.jumpUnless(END); + dsl.pushRemoteElement(); + dsl.evaluate('default'); + dsl.popRemoteElement(); + }); + } +} diff --git a/packages/glimmer-runtime/lib/syntax/builtins/with-dynamic-vars.ts b/packages/glimmer-runtime/lib/syntax/builtins/with-dynamic-vars.ts index 56f01ba93b..37befcc473 100644 --- a/packages/glimmer-runtime/lib/syntax/builtins/with-dynamic-vars.ts +++ b/packages/glimmer-runtime/lib/syntax/builtins/with-dynamic-vars.ts @@ -22,10 +22,12 @@ export default class WithDynamicVarsSyntax extends StatementSyntax { compile(dsl: OpcodeBuilderDSL, env: Environment) { let { args, templates } = this; - dsl.block({ templates, args }, (dsl) => { - dsl.pushRemoteElement(); + dsl.unit({ templates }, dsl => { + dsl.putArgs(args); + dsl.pushDynamicScope(); + dsl.bindDynamicScope(args.named.keys); dsl.evaluate('default'); - dsl.popElement(); + dsl.popDynamicScope(); }); } } diff --git a/packages/glimmer-runtime/tests/ember-component-test.ts b/packages/glimmer-runtime/tests/ember-component-test.ts index 7b5382dc5a..3db25d2514 100644 --- a/packages/glimmer-runtime/tests/ember-component-test.ts +++ b/packages/glimmer-runtime/tests/ember-component-test.ts @@ -27,7 +27,7 @@ import { equalTokens, stripTight } from "glimmer-test-helpers"; import { CLASS_META, UpdatableReference, setProperty as set } from 'glimmer-object-reference'; -class EmberishRootView extends EmberObject { +export class EmberishRootView extends EmberObject { private parent: Element; protected _result: RenderResult; protected template: Template<{}>; @@ -73,7 +73,7 @@ function module(name: string) { module("Components - generic - props"); -function appendViewFor(template: string, context: Object = {}) { +export function appendViewFor(template: string, context: Object = {}) { class MyRootView extends EmberishRootView { protected env = env; protected template = env.compile(template); @@ -90,7 +90,7 @@ function appendViewFor(template: string, context: Object = {}) { return view; } -function assertAppended(content: string) { +export function assertAppended(content: string) { equalTokens((document.querySelector('#qunit-fixture')), content); } @@ -147,12 +147,12 @@ function assertEmberishElement(...args) { equalsElement(view.element, tagName, fullAttrs, contents); } -function assertElementIsEmberishElement(element: Element, tagName: string, attrs: Object, contents: string); -function assertElementIsEmberishElement(element: Element, tagName: string, attrs: Object); -function assertElementIsEmberishElement(element: Element, tagName: string, contents: string); -function assertElementIsEmberishElement(element: Element, tagName: string); +export function assertElementIsEmberishElement(element: Element, tagName: string, attrs: Object, contents: string); +export function assertElementIsEmberishElement(element: Element, tagName: string, attrs: Object); +export function assertElementIsEmberishElement(element: Element, tagName: string, contents: string); +export function assertElementIsEmberishElement(element: Element, tagName: string); -function assertElementIsEmberishElement(element: Element, ...args) { +export function assertElementIsEmberishElement(element: Element, ...args) { let tagName, attrs, contents; if (args.length === 2) { if (typeof args[1] === 'string') [tagName, attrs, contents] = [args[0], {}, args[1]]; diff --git a/packages/glimmer-runtime/tests/in-element-test.ts b/packages/glimmer-runtime/tests/in-element-test.ts new file mode 100644 index 0000000000..e03513ce93 --- /dev/null +++ b/packages/glimmer-runtime/tests/in-element-test.ts @@ -0,0 +1,401 @@ +import { + TestEnvironment, + stripTight, + equalsElement, + EmberishCurlyComponent + } from "glimmer-test-helpers"; + +import { + assertAppended, + assertElementIsEmberishElement, + EmberishRootView +} from './ember-component-test'; + +import { CLASS_META, setProperty as set } from 'glimmer-object-reference'; + +let view, env; + +function rerender() { + view.rerender(); +} + +function appendViewFor(template: string, context: Object = {}) { + class MyRootView extends EmberishRootView { + protected env = env; + protected template = env.compile(template); + } + + view = new MyRootView(context); + MyRootView[CLASS_META].seal(); + + env.begin(); + view.appendTo('#qunit-fixture'); + env.commit(); + + return view; +} + +QUnit.module('Targeting a remote element', { + setup() { + env = new TestEnvironment(); + } +}); + +QUnit.test('basic', function(assert) { + let externalElement = document.createElement('div'); + + appendViewFor( + stripTight`{{#-in-element externalElement}}[{{foo}}]{{/-in-element}}`, + { externalElement, foo: 'Yippie!' } + ); + + equalsElement(externalElement, 'div', {}, stripTight`[Yippie!]`); + + set(view, 'foo', 'Double Yips!'); + rerender(); + + equalsElement(externalElement, 'div', {}, stripTight`[Double Yips!]`); + + set(view, 'foo', 'Yippie!'); + rerender(); + + equalsElement(externalElement, 'div', {}, stripTight`[Yippie!]`); +}); + +QUnit.test('changing to falsey', function(assert) { + let first = document.createElement('div'); + let second = document.createElement('div'); + + appendViewFor( + stripTight` + |{{foo}}| + {{#-in-element first}}[{{foo}}]{{/-in-element}} + {{#-in-element second}}[{{foo}}]{{/-in-element}} + `, + { first, second: null, foo: 'Yippie!' } + ); + + equalsElement(first, 'div', {}, `[Yippie!]`); + equalsElement(second, 'div', {}, ``); + assertAppended('|Yippie!|'); + + set(view, 'foo', 'Double Yips!'); + rerender(); + + equalsElement(first, 'div', {}, `[Double Yips!]`); + equalsElement(second, 'div', {}, ``); + assertAppended('|Double Yips!|'); + + set(view, 'first', null); + rerender(); + + equalsElement(first, 'div', {}, ``); + equalsElement(second, 'div', {}, ``); + assertAppended('|Double Yips!|'); + + set(view, 'second', second); + rerender(); + + equalsElement(first, 'div', {}, ``); + equalsElement(second, 'div', {}, `[Double Yips!]`); + assertAppended('|Double Yips!|'); + + set(view, 'foo', 'Yippie!'); + rerender(); + + equalsElement(first, 'div', {}, ``); + equalsElement(second, 'div', {}, `[Yippie!]`); + assertAppended('|Yippie!|'); + + set(view, 'first', first); + set(view, 'second', null); + rerender(); + + equalsElement(first, 'div', {}, `[Yippie!]`); + equalsElement(second, 'div', {}, ``); + assertAppended('|Yippie!|'); +}); + +QUnit.test('with pre-existing content', function(assert) { + let externalElement = document.createElement('div'); + let initialContent = externalElement.innerHTML = '

Hello there!

'; + + appendViewFor( + stripTight`{{#-in-element externalElement}}[{{foo}}]{{/-in-element}}`, + { externalElement, foo: 'Yippie!' } + ); + + assertAppended(''); + equalsElement(externalElement, 'div', {}, `${initialContent}[Yippie!]`); + + set(view, 'foo', 'Double Yips!'); + rerender(); + + assertAppended(''); + equalsElement(externalElement, 'div', {}, `${initialContent}[Double Yips!]`); + + set(view, 'foo', 'Yippie!'); + rerender(); + + assertAppended(''); + equalsElement(externalElement, 'div', {}, `${initialContent}[Yippie!]`); + + set(view, 'externalElement', null); + rerender(); + + assertAppended(''); + equalsElement(externalElement, 'div', {}, `${initialContent}`); + + set(view, 'externalElement', externalElement); + rerender(); + + assertAppended(''); + equalsElement(externalElement, 'div', {}, `${initialContent}[Yippie!]`); +}); + +QUnit.test('updating remote element', function(assert) { + let first = document.createElement('div'); + let second = document.createElement('div'); + + appendViewFor( + stripTight`{{#-in-element targetElement}}[{{foo}}]{{/-in-element}}`, + { + targetElement: first, + foo: 'Yippie!' + } + ); + + equalsElement(first, 'div', {}, `[Yippie!]`); + equalsElement(second, 'div', {}, ``); + + set(view, 'foo', 'Double Yips!'); + rerender(); + + equalsElement(first, 'div', {}, `[Double Yips!]`); + equalsElement(second, 'div', {}, ``); + + set(view, 'foo', 'Yippie!'); + rerender(); + + equalsElement(first, 'div', {}, `[Yippie!]`); + equalsElement(second, 'div', {}, ``); + + set(view, 'targetElement', second); + rerender(); + + equalsElement(first, 'div', {}, ``); + equalsElement(second, 'div', {}, `[Yippie!]`); + + set(view, 'foo', 'Double Yips!'); + rerender(); + + equalsElement(first, 'div', {}, ``); + equalsElement(second, 'div', {}, `[Double Yips!]`); + + set(view, 'foo', 'Yippie!'); + rerender(); + + equalsElement(first, 'div', {}, ``); + equalsElement(second, 'div', {}, `[Yippie!]`); +}); + +QUnit.test('inside an `{{if}}', function(assert) { + let first = document.createElement('div'); + let second = document.createElement('div'); + + appendViewFor( + stripTight` + {{#if showFirst}} + {{#-in-element first}}[{{foo}}]{{/-in-element}} + {{/if}} + {{#if showSecond}} + {{#-in-element second}}[{{foo}}]{{/-in-element}} + {{/if}} + `, + { + first, + second, + showFirst: true, + showSecond: false, + foo: 'Yippie!' + } + ); + + equalsElement(first, 'div', {}, stripTight`[Yippie!]`); + equalsElement(second, 'div', {}, stripTight``); + + set(view, 'showFirst', false); + rerender(); + + equalsElement(first, 'div', {}, stripTight``); + equalsElement(second, 'div', {}, stripTight``); + + set(view, 'showSecond', true); + rerender(); + + equalsElement(first, 'div', {}, stripTight``); + equalsElement(second, 'div', {}, stripTight`[Yippie!]`); + + set(view, 'foo', 'Double Yips!'); + rerender(); + + equalsElement(first, 'div', {}, stripTight``); + equalsElement(second, 'div', {}, stripTight`[Double Yips!]`); + + set(view, 'showSecond', false); + rerender(); + + equalsElement(first, 'div', {}, stripTight``); + equalsElement(second, 'div', {}, stripTight``); + + set(view, 'showFirst', true); + rerender(); + + equalsElement(first, 'div', {}, stripTight`[Double Yips!]`); + equalsElement(second, 'div', {}, stripTight``); + + set(view, 'foo', 'Yippie!'); + rerender(); + + equalsElement(first, 'div', {}, stripTight`[Yippie!]`); + equalsElement(second, 'div', {}, stripTight``); +}); + +QUnit.test('multiple', function(assert) { + let firstElement = document.createElement('div'); + let secondElement = document.createElement('div'); + + appendViewFor( + stripTight` + {{#-in-element firstElement}} + [{{foo}}] + {{/-in-element}} + {{#-in-element secondElement}} + [{{bar}}] + {{/-in-element}} + `, + { + firstElement, + secondElement, + foo: 'Hello!', + bar: 'World!' + } + ); + + equalsElement(firstElement, 'div', {}, stripTight`[Hello!]`); + equalsElement(secondElement, 'div', {}, stripTight`[World!]`); + + set(view, 'foo', 'GoodBye!'); + rerender(); + + equalsElement(firstElement, 'div', {}, stripTight`[GoodBye!]`); + equalsElement(secondElement, 'div', {}, stripTight`[World!]`); + + set(view, 'bar', 'Folks!'); + rerender(); + + equalsElement(firstElement, 'div', {}, stripTight`[GoodBye!]`); + equalsElement(secondElement, 'div', {}, stripTight`[Folks!]`); + + set(view, 'bar', 'World!'); + rerender(); + + equalsElement(firstElement, 'div', {}, stripTight`[GoodBye!]`); + equalsElement(secondElement, 'div', {}, stripTight`[World!]`); + + set(view, 'foo', 'Hello!'); + rerender(); + + equalsElement(firstElement, 'div', {}, stripTight`[Hello!]`); + equalsElement(secondElement, 'div', {}, stripTight`[World!]`); +}); + +QUnit.test('nesting', function(assert) { + let firstElement = document.createElement('div'); + let secondElement = document.createElement('div'); + + appendViewFor( + stripTight` + {{#-in-element firstElement}} + [{{foo}}] + {{#-in-element secondElement}} + [{{bar}}] + {{/-in-element}} + {{/-in-element}} + `, + { + firstElement, + secondElement, + foo: 'Hello!', + bar: 'World!' + } + ); + + equalsElement(firstElement, 'div', {}, stripTight`[Hello!]`); + equalsElement(secondElement, 'div', {}, stripTight`[World!]`); + + set(view, 'foo', 'GoodBye!'); + rerender(); + + equalsElement(firstElement, 'div', {}, stripTight`[GoodBye!]`); + equalsElement(secondElement, 'div', {}, stripTight`[World!]`); + + set(view, 'bar', 'Folks!'); + rerender(); + + equalsElement(firstElement, 'div', {}, stripTight`[GoodBye!]`); + equalsElement(secondElement, 'div', {}, stripTight`[Folks!]`); + + set(view, 'bar', 'World!'); + rerender(); + + equalsElement(firstElement, 'div', {}, stripTight`[GoodBye!]`); + equalsElement(secondElement, 'div', {}, stripTight`[World!]`); + + set(view, 'foo', 'Hello!'); + rerender(); + + equalsElement(firstElement, 'div', {}, stripTight`[Hello!]`); + equalsElement(secondElement, 'div', {}, stripTight`[World!]`); +}); + +QUnit.test('components are destroyed', function(assert) { + let destroyed = 0; + let DestroyMeComponent = EmberishCurlyComponent.extend({ + destroy() { + this._super(); + destroyed++; + } + }); + + env.registerEmberishCurlyComponent('destroy-me', DestroyMeComponent as any, 'destroy me!'); + + let externalElement = document.createElement('div'); + + appendViewFor( + stripTight` + {{#if showExternal}} + {{#-in-element externalElement}}[{{destroy-me}}]{{/-in-element}} + {{/if}} + `, + { + externalElement, + showExternal: false, + } + ); + + equalsElement(externalElement, 'div', {}, stripTight``); + assert.equal(destroyed, 0, 'component was destroyed'); + + set(view, 'showExternal', true); + rerender(); + + assertElementIsEmberishElement(externalElement.firstElementChild, 'div', { }, 'destroy me!'); + assert.equal(destroyed, 0, 'component was destroyed'); + + set(view, 'showExternal', false); + rerender(); + + equalsElement(externalElement, 'div', {}, stripTight``); + assert.equal(destroyed, 1, 'component was destroyed'); +}); diff --git a/packages/glimmer-test-helpers/lib/environment.ts b/packages/glimmer-test-helpers/lib/environment.ts index 69a93c6fac..9569713205 100644 --- a/packages/glimmer-test-helpers/lib/environment.ts +++ b/packages/glimmer-test-helpers/lib/environment.ts @@ -42,6 +42,7 @@ import { ArgsSyntax, OptimizedAppend, WithDynamicVarsSyntax, + InElementSyntax, // References ValueReference, @@ -788,6 +789,8 @@ export class TestEnvironment extends Environment { return new RenderInverseIdentitySyntax({ args, templates }); case '-with-dynamic-vars': return new WithDynamicVarsSyntax({ args, templates }); + case '-in-element': + return new InElementSyntax({ args, templates }); } }