diff --git a/demos/glimmer-demos/visualizer.ts b/demos/glimmer-demos/visualizer.ts index 8e0bf5af0d..dc88727011 100644 --- a/demos/glimmer-demos/visualizer.ts +++ b/demos/glimmer-demos/visualizer.ts @@ -399,7 +399,7 @@ function renderContent() { env.begin(); let self = new UpdatableReference(data); - let res = app.render(self, env, { appendTo: div }); + let res = app.render(self, env, { appendTo: div, dynamicScope: new TestDynamicScope() }); env.commit(); ui.rendered = true; diff --git a/packages/glimmer-reference/index.ts b/packages/glimmer-reference/index.ts index ef5f3218d4..7634f011ba 100644 --- a/packages/glimmer-reference/index.ts +++ b/packages/glimmer-reference/index.ts @@ -6,7 +6,7 @@ export { PushPullReference } from './lib/references/push-pull'; export * from './lib/types'; export { default as ObjectReference } from './lib/references/path'; export { default as UpdatableReference, referenceFromParts } from './lib/references/root'; -export { ConstReference } from './lib/references/const'; +export { ConstReference, isConst } from './lib/references/const'; export { IterationItem, Iterator, diff --git a/packages/glimmer-reference/lib/references/const.ts b/packages/glimmer-reference/lib/references/const.ts index af85ec2f45..6e61e3eac7 100644 --- a/packages/glimmer-reference/lib/references/const.ts +++ b/packages/glimmer-reference/lib/references/const.ts @@ -1,8 +1,13 @@ import { Reference } from '../types'; +import { Opaque } from 'glimmer-util'; + +export const CONST = "29c7034c-f1e1-4cf4-a843-1783dda9b744"; export class ConstReference implements Reference { protected inner: T; + public "29c7034c-f1e1-4cf4-a843-1783dda9b744" = true; + constructor(inner: T) { this.inner = inner; } @@ -13,6 +18,9 @@ export class ConstReference implements Reference { isDirty() { return false; } value(): T { return this.inner; } - chain() { return null; } destroy() {} } + +export function isConst(reference: Reference): boolean { + return !!reference[CONST]; +} diff --git a/packages/glimmer-runtime/lib/compat/svg-inner-html-fix.ts b/packages/glimmer-runtime/lib/compat/svg-inner-html-fix.ts index 87f3d406cc..d72b1f98d5 100644 --- a/packages/glimmer-runtime/lib/compat/svg-inner-html-fix.ts +++ b/packages/glimmer-runtime/lib/compat/svg-inner-html-fix.ts @@ -14,13 +14,13 @@ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; // approach is used. A pre/post SVG tag is added to the string, then // that whole string is added to a div. The created nodes are plucked // out and applied to the target location on DOM. -export default function applyInnerHTMLFix(document: Document, DOMHelperClass: typeof DOMHelper, svgNamespace: String): typeof DOMHelper { +export default function applyInnerHTMLFix(document: Document, DOMHelperClass: typeof DOMHelper, svgNamespace: string): typeof DOMHelper { if (!document) return DOMHelperClass; let svg = document.createElementNS(svgNamespace, 'svg'); try { - svg.insertAdjacentHTML('beforeEnd', ''); + svg['insertAdjacentHTML']('beforeEnd', ''); } catch (e) { // IE, Edge: Will throw, insertAdjacentHTML is unsupported on SVG // Safari: Will throw, insertAdjacentHTML is not present on SVG diff --git a/packages/glimmer-runtime/lib/compiled/opcodes/component.ts b/packages/glimmer-runtime/lib/compiled/opcodes/component.ts index 6af0f01ca9..1e04846f5c 100644 --- a/packages/glimmer-runtime/lib/compiled/opcodes/component.ts +++ b/packages/glimmer-runtime/lib/compiled/opcodes/component.ts @@ -7,7 +7,7 @@ import { Templates } from '../../syntax/core'; import { layoutFor } from '../../compiler'; import { DynamicScope } from '../../environment'; import { InternedString, Opaque, dict } from 'glimmer-util'; -import { Reference } from 'glimmer-reference'; +import { Reference, isConst } from 'glimmer-reference'; export type DynamicComponentFactory = (args: EvaluatedArgs, vm: PublicVM) => Reference>; @@ -73,7 +73,10 @@ export class OpenDynamicComponentOpcode extends Opcode { vm.invokeLayout({ templates, args, shadow, layout, callerScope }); vm.env.didCreate(component, manager); - vm.updateWith(new Assert(definitionRef, definition)); + if (!isConst(definitionRef)) { + vm.updateWith(new Assert(definitionRef, definition)); + } + vm.updateWith(new UpdateComponentOpcode({ name: definition.name, component, manager, args, dynamicScope })); } diff --git a/packages/glimmer-runtime/lib/compiled/opcodes/content.ts b/packages/glimmer-runtime/lib/compiled/opcodes/content.ts index 2101ee066d..42ebee9474 100644 --- a/packages/glimmer-runtime/lib/compiled/opcodes/content.ts +++ b/packages/glimmer-runtime/lib/compiled/opcodes/content.ts @@ -1,6 +1,6 @@ import { Opcode, OpcodeJSON, UpdatingOpcode } from '../../opcodes'; import { VM, UpdatingVM } from '../../vm'; -import { PathReference } from 'glimmer-reference'; +import { PathReference, isConst } from 'glimmer-reference'; import { Opaque, dict } from 'glimmer-util'; import { clear } from '../../bounds'; import { Fragment } from '../../builder'; @@ -28,7 +28,10 @@ export class AppendOpcode extends Opcode { let reference = vm.frame.getOperand(); let value = normalizeTextValue(reference.value()); let node = vm.stack().appendText(value); - vm.updateWith(new UpdateAppendOpcode(reference, value, node)); + + if (!isConst(reference)) { + vm.updateWith(new UpdateAppendOpcode(reference, value, node)); + } } toJSON(): OpcodeJSON { @@ -79,9 +82,11 @@ export class TrustingAppendOpcode extends Opcode { evaluate(vm: VM) { let reference = vm.frame.getOperand(); let value = normalizeTextValue(reference.value()); - let bounds = vm.stack().insertHTMLBefore(null, value); - vm.updateWith(new UpdateTrustingAppendOpcode(reference, value, bounds)); + + if (!isConst(reference)) { + vm.updateWith(new UpdateTrustingAppendOpcode(reference, value, bounds)); + } } } diff --git a/packages/glimmer-runtime/lib/compiled/opcodes/dom.ts b/packages/glimmer-runtime/lib/compiled/opcodes/dom.ts index 7f3f3150fa..3b4861f419 100644 --- a/packages/glimmer-runtime/lib/compiled/opcodes/dom.ts +++ b/packages/glimmer-runtime/lib/compiled/opcodes/dom.ts @@ -1,8 +1,9 @@ import { Opcode, OpcodeJSON, UpdatingOpcode } from '../../opcodes'; import { VM, UpdatingVM } from '../../vm'; import { FIXME, InternedString, dict } from 'glimmer-util'; -import { PathReference, Reference } from 'glimmer-reference'; +import { PathReference, Reference, isConst as isConstReference } from 'glimmer-reference'; import { DOMHelper } from '../../dom'; +import { NULL_REFERENCE } from '../../references'; import { ValueReference } from '../../compiled/expressions/value'; abstract class DOMUpdatingOpcode extends UpdatingOpcode { @@ -74,29 +75,55 @@ export class OpenDynamicPrimitiveElementOpcode extends Opcode { } } -class ClassList implements Reference { - private list: PathReference[] = []; +class ClassList { + private list: Reference[] = null; + private isConst = true; - isEmpty() { - return this.list.length === 0; + append(reference: Reference) { + let { list, isConst } = this; + + if (list === null) list = this.list = []; + + list.push(reference); + this.isConst = isConst && isConstReference(reference); } - destroy() {} - isDirty() { return true; } + toReference(): Reference { + let { list, isConst } = this; + + if (!list) return NULL_REFERENCE; - append(reference: PathReference) { - this.list.push(reference); + if (isConst) return new ValueReference(toClassName(list)); + + return new ClassListReference(list); + } + +} + +class ClassListReference implements Reference { + private list: Reference[] = []; + + constructor(list: Reference[]) { + this.list = list; } value(): string { - if (this.list.length === 0) return null; - let ret = []; - for (let i = 0; i < this.list.length; i++) { - let value = this.list[i].value(); - if (value !== null) ret.push(String(value)); - } - return ret.join(' '); + return toClassName(this.list); } + + isDirty() { return true; } + destroy() {} +} + +function toClassName(list: Reference[]) { + let ret = []; + + for (let i = 0; i < list.length; i++) { + let value = list[i].value(); + if (value !== null) ret.push(value); + } + + return (ret.length === 0) ? null : ret.join(' '); } export class CloseElementOpcode extends Opcode { @@ -107,7 +134,7 @@ export class CloseElementOpcode extends Opcode { let stack = vm.stack(); let { element, elementOperations: { groups } } = stack; - let classes = new ClassList(); + let classList = new ClassList(); let flattened = dict(); let flattenedKeys = []; @@ -120,9 +147,9 @@ export class CloseElementOpcode extends Opcode { for (let j = 0; j < groups[i].length; j++) { let op = groups[i][j]; let name = op['name'] as FIXME; - let value = op['value'] as FIXME>; + let reference = op['reference'] as FIXME>; if (name === 'class') { - classes.append(value); + classList.append(reference); } else if (!flattened[name]) { flattenedKeys.push(name); flattened[name] = op; @@ -130,12 +157,18 @@ export class CloseElementOpcode extends Opcode { } } - if (!classes.isEmpty()) { - vm.updateWith(new NonNamespacedAttribute('class' as InternedString, classes).flush(dom, element)); + let className = classList.toReference(); + + if (isConstReference(className)) { + let value = className.value(); + if (value !== null) dom.setAttribute(element, 'class', value); + } else { + vm.updateWith(new NonNamespacedAttribute('class' as InternedString, className).flush(dom, element)); } for (let k = 0; k < flattenedKeys.length; k++) { - vm.updateWith(flattened[flattenedKeys[k]].flush(dom, element)); + let opcode = flattened[flattenedKeys[k]].flush(dom, element); + if (opcode) vm.updateWith(opcode); } stack.closeElement(); @@ -171,7 +204,7 @@ export class StaticAttrOpcode extends Opcode { let details = dict(); details["name"] = JSON.stringify(name); - details["value"] = JSON.stringify(value); + details["value"] = JSON.stringify(value.value()); if (namespace) { details["namespace"] = JSON.stringify(namespace); @@ -192,24 +225,28 @@ interface ElementPatchOperation extends ElementOperation { export class NamespacedAttribute implements ElementPatchOperation { name: InternedString; - value: PathReference; + reference: Reference; namespace: InternedString; - constructor(name: InternedString, value: PathReference, namespace: InternedString) { + constructor(name: InternedString, reference: Reference, namespace: InternedString) { this.name = name; - this.value = value; + this.reference = reference; this.namespace = namespace; } flush(dom: DOMHelper, element: Element): PatchElementOpcode { + let { reference } = this; let value = this.apply(dom, element); - return new PatchElementOpcode(element, this, value); + + if (!isConstReference(reference)) { + return new PatchElementOpcode(element, this, value); + } } apply(dom: DOMHelper, element: Element, lastValue: string = null): any { let { name, - value: reference, + reference, namespace } = this; @@ -232,20 +269,24 @@ export class NamespacedAttribute implements ElementPatchOperation { export class NonNamespacedAttribute implements ElementPatchOperation { name: InternedString; - value: Reference; + reference: Reference; constructor(name: InternedString, value: Reference) { this.name = name; - this.value = value; + this.reference = value; } flush(dom: DOMHelper, element: Element): PatchElementOpcode { + let { reference } = this; let value = this.apply(dom, element); - return new PatchElementOpcode(element, this, value); + + if (!isConstReference(reference)) { + return new PatchElementOpcode(element, this, value); + } } apply(dom: DOMHelper, element: Element, lastValue: any = null): any { - let { name, value: reference } = this; + let { name, reference } = this; let value = reference.value(); if (value === lastValue) { @@ -265,20 +306,24 @@ export class NonNamespacedAttribute implements ElementPatchOperation { export class Property implements ElementPatchOperation { name: InternedString; - value: PathReference; + reference: PathReference; constructor(name: InternedString, value: PathReference) { this.name = name; - this.value = value; + this.reference = value; } flush(dom: DOMHelper, element: Element): PatchElementOpcode { + let { reference } = this; let value = this.apply(dom, element); - return new PatchElementOpcode(element, this, value); + + if (!isConstReference(reference)) { + return new PatchElementOpcode(element, this, value); + } } apply(dom: DOMHelper, element: Element, lastValue: any = null): any { - let { name, value: reference } = this; + let { name, reference } = this; let value = reference.value(); if (value === lastValue) { @@ -398,6 +443,21 @@ export class PatchElementOpcode extends DOMUpdatingOpcode { evaluate(vm: UpdatingVM) { this.lastValue = this.attribute.apply(vm.env.getDOM(), this.element, this.lastValue); } + + toJSON(): OpcodeJSON { + let { _guid: guid, type, element, attribute, lastValue } = this; + + let details = dict(); + + let [attributeType, attributeName] = attribute.toJSON(); + + details["element"] = JSON.stringify(`<${element.tagName.toLowerCase()} />`); + details["type"] = JSON.stringify(attributeType); + details["name"] = JSON.stringify(attributeName); + details["lastValue"] = JSON.stringify(lastValue); + + return { guid, type, details }; + } } export class CommentOpcode extends Opcode { diff --git a/packages/glimmer-runtime/lib/compiled/opcodes/vm.ts b/packages/glimmer-runtime/lib/compiled/opcodes/vm.ts index 7ea5917fb2..2179a58abf 100644 --- a/packages/glimmer-runtime/lib/compiled/opcodes/vm.ts +++ b/packages/glimmer-runtime/lib/compiled/opcodes/vm.ts @@ -6,7 +6,7 @@ import { Layout, InlineBlock } from '../blocks'; import { turbocharge } from '../../utils'; import { NULL_REFERENCE } from '../../references'; import { ListSlice, Opaque, Slice, Dict, dict, assign } from 'glimmer-util'; -import { Reference } from 'glimmer-reference'; +import { Reference, isConst } from 'glimmer-reference'; abstract class VMUpdatingOpcode extends UpdatingOpcode { public type: string; @@ -362,7 +362,9 @@ export class JumpIfOpcode extends JumpOpcode { super.evaluate(vm); } - vm.updateWith(new Assert(reference, value)); + if (!isConst(reference)) { + vm.updateWith(new Assert(reference, value)); + } } } @@ -377,7 +379,9 @@ export class JumpUnlessOpcode extends JumpOpcode { super.evaluate(vm); } - vm.updateWith(new Assert(reference, value)); + if (!isConst(reference)) { + vm.updateWith(new Assert(reference, value)); + } } } @@ -400,4 +404,15 @@ export class Assert extends VMUpdatingOpcode { vm.throw(); } } + + toJSON(): OpcodeJSON { + let { type, _guid, expected } = this; + + return { + guid: _guid, + type, + args: [], + details: { expected: JSON.stringify(expected) } + }; + } } diff --git a/packages/glimmer-runtime/lib/dom.ts b/packages/glimmer-runtime/lib/dom.ts index 602b36858a..f66d690ce2 100644 --- a/packages/glimmer-runtime/lib/dom.ts +++ b/packages/glimmer-runtime/lib/dom.ts @@ -86,7 +86,13 @@ class DOMHelper { return this.document.createElement(tag); } - insertHTMLBefore(parent: HTMLElement, nextSibling: Node, html: string): Bounds { + insertHTMLBefore(_parent: Element, nextSibling: Node, html: string): Bounds { + // TypeScript vendored an old version of the DOM spec where `insertAdjacentHTML` + // only exists on `HTMLElement` but not on `Element`. We actually work with the + // newer version of the DOM API here (and monkey-patch this method in `./compat` + // when we detect older browsers). This is a hack to work around this limitation. + let parent = _parent as HTMLElement; + let prev = nextSibling ? nextSibling.previousSibling : parent.lastChild; let last; diff --git a/packages/glimmer-runtime/lib/environment.ts b/packages/glimmer-runtime/lib/environment.ts index d696ee12ba..809cd0e775 100644 --- a/packages/glimmer-runtime/lib/environment.ts +++ b/packages/glimmer-runtime/lib/environment.ts @@ -109,7 +109,7 @@ export abstract class Environment { this.dom = dom; } - toConditionalReference(reference: Reference): ConditionalReference { + toConditionalReference(reference: Reference): Reference { return new ConditionalReference(reference); } diff --git a/packages/glimmer-runtime/lib/vm/update.ts b/packages/glimmer-runtime/lib/vm/update.ts index ea4d6ad6cf..7280cbbdad 100644 --- a/packages/glimmer-runtime/lib/vm/update.ts +++ b/packages/glimmer-runtime/lib/vm/update.ts @@ -180,10 +180,9 @@ export class ListRevalidationDelegate implements IteratorSynchronizerDelegate { private marker: Comment; constructor(opcode: ListBlockOpcode, marker: Comment) { - let { map, updating } = opcode; this.opcode = opcode; - this.map = map; - this.updating = updating; + this.map = opcode.map; + this.updating = opcode['updating']; this.marker = marker; } diff --git a/packages/glimmer-runtime/tests/updating-test.ts b/packages/glimmer-runtime/tests/updating-test.ts index a822ff2941..2cfe7853e3 100644 --- a/packages/glimmer-runtime/tests/updating-test.ts +++ b/packages/glimmer-runtime/tests/updating-test.ts @@ -90,7 +90,6 @@ test("null and undefined produces empty text nodes", () => { strictEqual(root.firstChild.firstChild.firstChild, valueNode1, "The text node was not blown away"); strictEqual(root.firstChild.lastChild.firstChild, valueNode2, "The text node was not blown away"); - object.v1 = 'hello'; rerender(); @@ -1320,7 +1319,7 @@ test("expression nested inside a namespace", function() { test("expression nested inside a namespaced root element", function() { let content = 'Maurice'; let context = { content }; - let getSvg = () => root.firstChild; + let getSvg = () => root.firstChild as Element; let template = compile('{{content}}'); render(template, context); diff --git a/packages/glimmer-test-helpers/lib/environment.ts b/packages/glimmer-test-helpers/lib/environment.ts index 072a8d39ae..484553a9a8 100644 --- a/packages/glimmer-test-helpers/lib/environment.ts +++ b/packages/glimmer-test-helpers/lib/environment.ts @@ -69,7 +69,8 @@ import { OpaqueIterator, OpaqueIterable, AbstractIterable, - IterationItem + IterationItem, + isConst } from "glimmer-reference"; type KeyFor = (item: Opaque, index: number) => string; @@ -347,13 +348,17 @@ class EmberishCurlyComponentManager implements ComponentManager 0; + } else { + return !!value; + } +} + class EmberishConditionalReference extends ConditionalReference { protected toBool(value: any): boolean { - if (Array.isArray(value)) { - return value.length > 0; - } else { - return super.toBool(value); - } + return emberToBool(value); } } @@ -442,7 +447,11 @@ export class TestEnvironment extends Environment { return new UpdatableReference(object); } - toConditionalReference(reference: Reference): ConditionalReference { + toConditionalReference(reference: Reference): Reference { + if (isConst(reference)) { + return new ValueReference(emberToBool(reference.value())); + } + return new EmberishConditionalReference(reference); }