From b5eda2880a31245b1cd8005accbe4ba0c89dae78 Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Fri, 19 Aug 2016 01:05:56 -0400 Subject: [PATCH] dynamically scoped variable accessors This adds the expressison syntax `{{-get-dynamic-var "yourVariableName"}}` and the statement syntax `{{#-with-dynamic-var "yourVariableName" someValue}}...{{/with-dynamic-var}}`. --- .../lib/javascript-compiler.ts | 4 ++ .../glimmer-compiler/lib/template-compiler.ts | 24 +++++++++- .../compiled/expressions/get-dynamic-var.ts | 27 ++++++++++++ packages/glimmer-runtime/lib/environment.ts | 3 ++ .../lib/syntax/builtins/with-dynamic-var.ts | 44 +++++++++++++++++++ packages/glimmer-runtime/lib/syntax/core.ts | 29 ++++++++++++ .../glimmer-runtime/lib/syntax/expressions.ts | 5 ++- .../tests/ember-component-test.ts | 37 ++++++++++++++++ packages/glimmer-wire-format/index.ts | 3 ++ 9 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 packages/glimmer-runtime/lib/compiled/expressions/get-dynamic-var.ts create mode 100644 packages/glimmer-runtime/lib/syntax/builtins/with-dynamic-var.ts diff --git a/packages/glimmer-compiler/lib/javascript-compiler.ts b/packages/glimmer-compiler/lib/javascript-compiler.ts index 3de68acd57..57492f7383 100644 --- a/packages/glimmer-compiler/lib/javascript-compiler.ts +++ b/packages/glimmer-compiler/lib/javascript-compiler.ts @@ -184,6 +184,10 @@ export default class JavaScriptCompiler { this.template.yields.add(name); } + getDynamicVar(varName: string) { + this.pushValue(['get-dynamic-var', varName]); + } + /// Expressions literal(value: Expressions.Value | undefined) { diff --git a/packages/glimmer-compiler/lib/template-compiler.ts b/packages/glimmer-compiler/lib/template-compiler.ts index 9875a33179..f84edf3617 100644 --- a/packages/glimmer-compiler/lib/template-compiler.ts +++ b/packages/glimmer-compiler/lib/template-compiler.ts @@ -180,6 +180,10 @@ export default class TemplateCompiler { this.opcode('hasBlockParams', action, name); } + getDynamicVar(varName: string) { + this.opcode('getDynamicVar', null, varName); + } + builtInHelper(expr) { if (isHasBlock(expr)) { let name = assertValidHasBlock(expr); @@ -187,6 +191,9 @@ export default class TemplateCompiler { } else if (isHasBlockParams(expr)) { let name = assertValidHasBlockParams(expr); this.hasBlockParams(name, expr); + } else if (isGetDynamicVar(expr)) { + let varName = assertValidGetDynamicVarParams(expr); + this.getDynamicVar(varName); } } @@ -352,9 +359,14 @@ function isHasBlockParams({ path }) { return path.original === 'has-block-params'; } +function isGetDynamicVar({ path }) { + return path.original === '-get-dynamic-var'; +} + function isBuiltInHelper(expr) { return isHasBlock(expr) - || isHasBlockParams(expr); + || isHasBlockParams(expr) + || isGetDynamicVar(expr); } function assertValidYield({ hash }): string { @@ -398,3 +410,13 @@ function assertValidHasBlockParams({ params }): string { throw new Error(`has-block-params only takes a single positional argument`); } } + +function assertValidGetDynamicVarParams({ params }): string { + if (params.length !== 1) { + throw new Error(`get-dynamic-var requires exactly one parameter (the name of the dynamic variable you wish to access)`); + } + if (params[0].type !== 'StringLiteral') { + throw new Error(`get-dynamic-var only accepts string literals`); + } + return params[0].value; +} diff --git a/packages/glimmer-runtime/lib/compiled/expressions/get-dynamic-var.ts b/packages/glimmer-runtime/lib/compiled/expressions/get-dynamic-var.ts new file mode 100644 index 0000000000..ca306fcfea --- /dev/null +++ b/packages/glimmer-runtime/lib/compiled/expressions/get-dynamic-var.ts @@ -0,0 +1,27 @@ +import VM from '../../vm/append'; +import { CompiledExpression } from '../expressions'; +import { ValueReference } from './value'; +import { UNDEFINED_REFERENCE } from '../../references'; + +export default class CompiledGetDynamicVar extends CompiledExpression { + public type = "get-dynamic-var"; + public varName: string; + + constructor({ varName }: { varName: string }) { + super(); + this.varName = varName; + } + + evaluate(vm: VM): ValueReference { + let scope = vm.dynamicScope(); + if (scope.hasOwnProperty(this.varName)) { + return scope[this.varName]; + } else { + return UNDEFINED_REFERENCE; + } + } + + toJSON(): string { + return `get-dynamic-var(${this.varName})`; + } +} diff --git a/packages/glimmer-runtime/lib/environment.ts b/packages/glimmer-runtime/lib/environment.ts index e06ca8c471..4298bf6728 100644 --- a/packages/glimmer-runtime/lib/environment.ts +++ b/packages/glimmer-runtime/lib/environment.ts @@ -49,6 +49,7 @@ import * as Syntax from './syntax/core'; import IfSyntax from './syntax/builtins/if'; import UnlessSyntax from './syntax/builtins/unless'; import WithSyntax from './syntax/builtins/with'; +import WithDynamicVarSyntax from './syntax/builtins/with-dynamic-var'; import EachSyntax from './syntax/builtins/each'; import PartialSyntax from './syntax/builtins/partial'; @@ -174,6 +175,8 @@ export abstract class Environment { return new IfSyntax({ args, templates }); case 'with': return new WithSyntax({ args, templates }); + case '-with-dynamic-var': + return new WithDynamicVarSyntax({ args, templates }); case 'unless': return new UnlessSyntax({ args, templates }); } diff --git a/packages/glimmer-runtime/lib/syntax/builtins/with-dynamic-var.ts b/packages/glimmer-runtime/lib/syntax/builtins/with-dynamic-var.ts new file mode 100644 index 0000000000..d79d7ff1ef --- /dev/null +++ b/packages/glimmer-runtime/lib/syntax/builtins/with-dynamic-var.ts @@ -0,0 +1,44 @@ +import { + Statement as StatementSyntax +} from '../../syntax'; + +import OpcodeBuilderDSL from '../../compiled/opcodes/builder'; +import * as Syntax from '../core'; +import Environment from '../../environment'; +import { default as VM } from '../../vm/append'; +import { DynamicScope } from '../../environment'; +import { EvaluatedArgs } from '../../compiled/expressions/args'; + +export default class WithDynamicVarSyntax extends StatementSyntax { + type = "with-dynamic-var-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 callback = (_vm: VM, _scope: DynamicScope) => { + let vm = _vm as any; + let scope = _scope as any; + + let args: EvaluatedArgs = vm.frame.getArgs(); + + scope[args.positional.values[0].inner] = args.positional.values[1]; + }; + + let { args, templates } = this; + + dsl.unit({ templates }, dsl => { + dsl.putArgs(args); + dsl.setupDynamicScope(callback); + dsl.evaluate('default'); + dsl.popDynamicScope(); + }); + } +} diff --git a/packages/glimmer-runtime/lib/syntax/core.ts b/packages/glimmer-runtime/lib/syntax/core.ts index bb99f93664..e808816159 100644 --- a/packages/glimmer-runtime/lib/syntax/core.ts +++ b/packages/glimmer-runtime/lib/syntax/core.ts @@ -59,6 +59,8 @@ import { import CompiledHasBlock from '../compiled/expressions/has-block'; +import CompiledGetDynamicVar from '../compiled/expressions/get-dynamic-var'; + import CompiledHasBlockParams from '../compiled/expressions/has-block-params'; import CompiledHelper from '../compiled/expressions/helper'; @@ -970,6 +972,33 @@ export class HasBlockParams extends ExpressionSyntax { } } +export class GetDynamicVar extends ExpressionSyntax { + type = "get-dynamic-var"; + + static fromSpec(sexp: SerializedExpressions.GetDynamicVar): GetDynamicVar { + let [, varName] = sexp; + return new GetDynamicVar({ varName }); + } + + static build(varName: string): GetDynamicVar { + console.log("build a dynamic var reference"); + return new this({ varName }); + } + + varName: string; + + constructor({ varName }: { varName: string }) { + super(); + this.varName = varName; + } + + compile(compiler: SymbolLookup, env: Environment): CompiledGetDynamicVar { + return new CompiledGetDynamicVar({ + varName: this.varName + }); + } +} + export class Concat { type = "concat"; diff --git a/packages/glimmer-runtime/lib/syntax/expressions.ts b/packages/glimmer-runtime/lib/syntax/expressions.ts index bcbe3287fa..8c2aaaa3ad 100644 --- a/packages/glimmer-runtime/lib/syntax/expressions.ts +++ b/packages/glimmer-runtime/lib/syntax/expressions.ts @@ -8,6 +8,7 @@ import { HasBlockParams as HasBlockParamsSyntax, Helper as HelperSyntax, Unknown as UnknownSyntax, + GetDynamicVar as GetDynamicVarSyntax } from './core'; import { @@ -25,7 +26,8 @@ const { isHelper, isUnknown, isPrimitiveValue, - isUndefined + isUndefined, + isGetDynamicVar } = SerializedExpressions; export default function(sexp: SerializedExpression): any { @@ -39,6 +41,7 @@ export default function(sexp: SerializedExpression): any { if (isUnknown(sexp)) return UnknownSyntax.fromSpec(sexp); if (isHasBlock(sexp)) return HasBlockSyntax.fromSpec(sexp); if (isHasBlockParams(sexp)) return HasBlockParamsSyntax.fromSpec(sexp); + if (isGetDynamicVar(sexp)) return GetDynamicVarSyntax.fromSpec(sexp); throw new Error(`Unexpected wire format: ${JSON.stringify(sexp)}`); }; diff --git a/packages/glimmer-runtime/tests/ember-component-test.ts b/packages/glimmer-runtime/tests/ember-component-test.ts index ffc59ee6cc..8c473e7fce 100644 --- a/packages/glimmer-runtime/tests/ember-component-test.ts +++ b/packages/glimmer-runtime/tests/ember-component-test.ts @@ -693,6 +693,43 @@ testComponent('parameterized has-block (concatted attr, default) when block not expected: '' }); +module('Dynamically-scoped variable accessors'); + +testComponent('Can get and set dynamic variable', { + layout: '{{#-with-dynamic-var "myKeyword" @value}}{{yield}}{{/-with-dynamic-var}}', + invokeAs: { + template: '{{-get-dynamic-var "myKeyword"}}', + context: { value: "hello" }, + args: { value: 'value' } + }, + expected: 'hello', + updates: [{ + expected: 'hello' + }, { + context: { value: 'goodbye' }, + expected: 'goodbye' + }] +}); + +testComponent('Can shadow existing dynamic variable', { + layout: '{{#-with-dynamic-var "myKeyword" @outer}}
{{-get-dynamic-var "myKeyword"}}
{{#-with-dynamic-var "myKeyword" @inner}}{{yield}}{{/-with-dynamic-var}}
{{-get-dynamic-var "myKeyword"}}
{{/-with-dynamic-var}}', + invokeAs: { + template: '
{{-get-dynamic-var "myKeyword"}}
', + context: { outer: 'original', inner: 'shadowed' }, + args: { outer: 'outer', inner: 'inner'} + }, + expected: '
original
shadowed
original
', + updates: [{ + expected: '
original
shadowed
original
' + }, { + context: { outer: 'original2', inner: 'shadowed' }, + expected: '
original2
shadowed
original2
' + }, { + context: { outer: 'original2', inner: 'shadowed2' }, + expected: '
original2
shadowed2
original2
' + }] +}); + module('Components - has-block-params helper'); testComponent('parameterized has-block-params (subexpr, inverse) when inverse supplied without block params', { diff --git a/packages/glimmer-wire-format/index.ts b/packages/glimmer-wire-format/index.ts index 0947ef5faa..21e050b672 100644 --- a/packages/glimmer-wire-format/index.ts +++ b/packages/glimmer-wire-format/index.ts @@ -41,6 +41,7 @@ export namespace Expressions { export type Get = ['get', Path]; export type SelfGet = ['self-get', Path]; export type Value = str | number | boolean | null; // tslint:disable-line + export type GetDynamicVar = ['get-dynamic-var', str]; export type HasBlock = ['has-block', str]; export type HasBlockParams = ['has-block-params', str]; export type Undefined = ['undefined']; @@ -56,6 +57,7 @@ export namespace Expressions { | Helper | Undefined | Value + | GetDynamicVar ; export interface Concat extends Array { @@ -79,6 +81,7 @@ export namespace Expressions { export const isHasBlock = is('has-block'); export const isHasBlockParams = is('has-block-params'); export const isUndefined = is('undefined'); + export const isGetDynamicVar = is('get-dynamic-var'); export function isPrimitiveValue(value: any): value is Value { if (value === null) {