Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Property Access #37

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 56 additions & 6 deletions src/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,40 @@ var getKeyComputeData = function (key, scope, readOptions) {
//
// These expression types return a value. They are assembled by `expression.parse`.

// ### Bracket
// For accessing properties using bracket notation like `foo[bar]`
var Bracket = function (key, root) {
this.root = root;
this.key = key;
};
Bracket.prototype.value = function (scope) {
var prop = this.key;
var obj = this.root;

if (prop instanceof Literal) {
prop = prop.value();
} else if (prop instanceof Lookup) {
prop = lookupValue(prop.key, scope, {}, {});
prop = prop.value();
} else {
prop = prop.value(scope, {}, {})();
}

if (!obj) {
return lookupValue(prop, scope, {}, {}).value;
} else {
return compute(function() {
if (obj instanceof Lookup) {
obj = lookupValue(obj.key, scope, {}, {}).value();
} else {
obj = obj.value(scope, {}, {})();
}

return obj[prop];
});
}
};

// ### Literal
// For inline static values like `{{"Hello World"}}`
var Literal = function(value){
Expand Down Expand Up @@ -198,8 +232,6 @@ Call.prototype.value = function(scope, helperScope, helperOptions){

};



// ### HelperLookup
// An expression that looks up a value in the helper or scope.
// Any functions found prior to the last one are called with
Expand Down Expand Up @@ -384,7 +416,7 @@ Helper.prototype.value = function(scope, helperOptions, nodeList, truthyRenderer
// AT @NAME
//
var keyRegExp = /[\w\.\\\-_@\/\&%]+/,
tokensRegExp = /('.*?'|".*?"|=|[\w\.\\\-_@\/*%\$]+|[\(\)]|,|\~)/g,
tokensRegExp = /('.*?'|".*?"|=|[\w\.\\\-_@\/*%\$]+|[\(\)]|,|\~|\[|\])/g,
literalRegExp = /^('.*?'|".*?"|[0-9]+\.?[0-9]*|true|false|null|undefined)$/;

var isTokenKey = function(token){
Expand Down Expand Up @@ -529,7 +561,7 @@ var expression = {
Helper: Helper,
HelperLookup: HelperLookup,
HelperScopeLookup: HelperScopeLookup,

Bracket: Bracket,

SetIdentifier: function(value){ this.value = value; },
tokenize: function(expression){
Expand Down Expand Up @@ -614,6 +646,11 @@ var expression = {


return new (options.methodRule(ast))(this.hydrateAst(ast.method, options, ast.type), args, hashes);
} else if (ast.type === "Bracket") {
return new Bracket(
this.hydrateAst(ast.children[0], options),
ast.root ? this.hydrateAst(ast.root, options) : undefined
);
}
},
ast: function(expression){
Expand All @@ -635,7 +672,7 @@ var expression = {
// Literal
if(literalRegExp.test( token )) {
convertToHelperIfTopIsLookup(stack);
stack.addTo(["Helper", "Call", "Hash"], {type: "Literal", value: utils.jsonParse( token )});
stack.addTo(["Helper", "Call", "Hash", "Bracket"], {type: "Literal", value: utils.jsonParse( token )});
}
// Hash
else if(nextToken === "=") {
Expand Down Expand Up @@ -680,7 +717,7 @@ var expression = {
// if two scopes, that means a helper
convertToHelperIfTopIsLookup(stack);

stack.addToAndPush(["Helper", "Call","Hash","Arg"], {type: "Lookup", key: token});
stack.addToAndPush(["Helper", "Call", "Hash", "Arg", "Bracket"], {type: "Lookup", key: token});
}

}
Expand Down Expand Up @@ -709,6 +746,19 @@ var expression = {
else if(token === ",") {
stack.popUntil(["Call"]);
}
// Bracket
else if(token === "[") {
top = stack.top();
lastToken = stack.topLastChild();

if (lastToken && lastToken.type === "Call") {
stack.replaceTopAndPush({type: "Bracket", root: lastToken});
} else if (top.type === "Lookup") {
stack.replaceTopAndPush({type: "Bracket", root: top});
} else {
stack.replaceTopAndPush({type: "Bracket"});
}
}
}
return stack.root.children[0];
}
Expand Down
7 changes: 6 additions & 1 deletion src/mustache_core.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ var core = {
if(exprData.isHelper) {
return value;
}
} else if (exprData instanceof expression.Bracket) {
value = exprData.value(scope);
if(exprData.isHelper) {
return value;
}
} else {
var readOptions = {
// will return a function instead of calling it.
Expand Down Expand Up @@ -307,7 +312,7 @@ var core = {

// Pre-process the expression.
var exprData = core.expression.parse(expressionString);
if(!(exprData instanceof expression.Helper) && !(exprData instanceof expression.Call)) {
if(!(exprData instanceof expression.Helper) && !(exprData instanceof expression.Call) && !(exprData instanceof expression.Bracket)) {
exprData = new expression.Helper(exprData,[],{});
}
// A branching renderer takes truthy and falsey renderer.
Expand Down
154 changes: 154 additions & 0 deletions test/expression-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,157 @@ test("registerConverter helpers are chainable", function () {
equal(data.attr("observeVal"), 127, 'push converter called');
});

test("expression.ast - [] operator", function(){
var ast = expression.ast("['propName']");

deepEqual(ast, {
type: "Bracket",
children: [{type: "Literal", value: "propName"}]
});

var ast2 = expression.ast("[propName]");

deepEqual(ast2, {
type: "Bracket",
children: [{type: "Lookup", key: "propName"}]
});

var ast3 = expression.ast("foo['bar']");

deepEqual(ast3, {
type: "Bracket",
root: {type: "Lookup", key: "foo"},
children: [{type: "Literal", value: "bar"}]
});

var ast3 = expression.ast("foo[bar]");

deepEqual(ast3, {
type: "Bracket",
root: {type: "Lookup", key: "foo"},
children: [{type: "Lookup", key: "bar"}]
});

var ast4 = expression.ast("foo()[bar]");

deepEqual(ast4, {
type: "Bracket",
root: {type: "Call", method: {key: "@foo", type: "Lookup" } },
children: [{type: "Lookup", key: "bar"}]
});
});

test("expression.parse - [] operator", function(){
var exprData = expression.parse("['propName']");
deepEqual(exprData,
new expression.Bracket(
new expression.Literal('propName')
)
);

exprData = expression.parse("[propName]");
deepEqual(exprData,
new expression.Bracket(
new expression.Lookup('propName')
)
);

exprData = expression.parse("foo['bar']");
deepEqual(exprData,
new expression.Bracket(
new expression.Literal('bar'),
new expression.Lookup('foo')
)
);

exprData = expression.parse("foo[bar]");
deepEqual(exprData,
new expression.Bracket(
new expression.Lookup('bar'),
new expression.Lookup('foo')
)
);

exprData = expression.parse("foo()[bar]");
deepEqual(exprData,
new expression.Bracket(
new expression.Lookup('bar'),
new expression.Call( new expression.Lookup('@foo'), [], {} )
)
);
});

test("Bracket expression", function(){
// ["bar"]
var expr = new expression.Bracket(
new expression.Literal("bar")
);
var compute = expr.value(
new Scope(
new CanMap({bar: "name"})
)
);
equal(compute(), "name");

// [bar]
expr = new expression.Bracket(
new expression.Lookup("bar")
);
var compute = expr.value(
new Scope(
new CanMap({bar: "name", name: "Kevin"})
)
);
equal(compute(), "Kevin");

// foo["bar"]
expr = new expression.Bracket(
new expression.Literal("bar"),
new expression.Lookup("foo")
);
var compute = expr.value(
new Scope(
new CanMap({foo: {bar: "name"}})
)
);
equal(compute(), "name");

// foo[bar]
expr = new expression.Bracket(
new expression.Lookup("bar"),
new expression.Lookup("foo")
);
var compute = expr.value(
new Scope(
new CanMap({foo: {name: "Kevin"}, bar: "name"})
)
);
equal(compute(), "Kevin");

// foo()[bar]
expr = new expression.Bracket(
new expression.Lookup("bar"),
new expression.Call( new expression.Lookup("@foo"), [], {} )
);
var compute = expr.value(
new Scope(
new CanMap({foo: function() { return {name: "Kevin"}; }, bar: "name"})
)
);
equal(compute(), "Kevin");

// foo()[bar()]
expr = new expression.Bracket(
new expression.Call( new expression.Lookup("@bar"), [], {} ),
new expression.Call( new expression.Lookup("@foo"), [], {} )
);
var compute = expr.value(
new Scope(
new CanMap({
foo: function() { return {name: "Kevin"}; },
bar: function () { return "name"; }
})
)
);
equal(compute(), "Kevin");
});
19 changes: 19 additions & 0 deletions test/stache-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4960,6 +4960,25 @@ function makeTest(name, doc, mutation) {
QUnit.equal( lis[1].innerHTML, "2", "is 2");
});

test("Bracket expression", function () {
var template;
var div = doc.createElement('div');

template = stache("<p>{{ foo[bar] }}</p>");

var data = new CanMap({
bar: "name",
foo: {
name: "Kevin"
}
});
var dom = template(data);
div.appendChild(dom);
var p = div.getElementsByTagName('p');

equal(innerHTML(p[0]), 'Kevin', 'correct value for foo[bar]');
});

// PUT NEW TESTS RIGHT BEFORE THIS!

}