diff --git a/packages/spacebars/spacebars.js b/packages/spacebars/spacebars.js index e7d561d8c3..497553a877 100644 --- a/packages/spacebars/spacebars.js +++ b/packages/spacebars/spacebars.js @@ -1384,6 +1384,14 @@ var optimize = function (tree) { return optTree; }; +var builtInComponents = { + 'content': '__content', + 'elseContent': '__elseContent', + 'if': 'UI.If2', + 'unless': 'UI.Unless2', + 'with': 'UI.With2' +}; + var replaceSpecials = function (node) { if (UI.isComponent(node)) { return node; @@ -1416,8 +1424,15 @@ var replaceSpecials = function (node) { var path = tag.path; var compCode = codeGenPath2(path); - if (path.length === 1) - compCode = '(Template[' + toJSLiteral(path[0]) + '] || ' + compCode + ')'; + if (path.length === 1) { + var compName = path[0]; + if (builtInComponents.hasOwnProperty(compName)) { + compCode = builtInComponents[compName]; + } else { + compCode = ('(Template[' + toJSLiteral(path[0]) + + '] || ' + compCode + ')'); + } + } var includeArgs = codeGenInclusionArgs(tag); @@ -1437,6 +1452,17 @@ var replaceSpecials = function (node) { var codeGenInclusionArgs = function (tag) { var args = null; + if ('content' in tag) { + args = (args || {}); + args.__content = ( + 'UI.block(' + Spacebars.compile2(tag.content) + ')'); + } + if ('elseContent' in tag) { + args = (args || {}); + args.__elseContent = ( + 'UI.block(' + Spacebars.compile2(tag.elseContent) + ')'); + } + _.each(tag.args, function (arg) { var argType = arg[0]; var argValue = arg[1]; @@ -1466,8 +1492,9 @@ var codeGenInclusionArgs = function (tag) { if (arg.length > 2) { // keyword argument (represented as [type, value, name]) + var name = arg[2]; args = (args || {}); - args[toJSLiteral(arg[2])] = argCode; + args[toJSLiteral(name)] = argCode; } else { // positional argument // XXX deal with >1 posArgs for #foo helpers @@ -1483,6 +1510,7 @@ var codeGenInclusionArgs = function (tag) { }; Spacebars.include = function (kindOrFunc, args) { + args = args || {}; if (typeof kindOrFunc === 'function') { // function block helper var func = kindOrFunc; @@ -1495,13 +1523,27 @@ Spacebars.include = function (kindOrFunc, args) { } } + var result; if ('data' in args) { var data = args.data; data = (typeof data === 'function' ? data() : data); - return func(data, { hash: hash }); + result = func(data, { hash: hash }); } else { - return func({ hash: hash }); + result = func({ hash: hash }); } + // In `{{#foo}}...{{/foo}}`, if `foo` is a function that + // returns a component, attach __content and __elseContent + // to it. + if (UI.isComponent(result) && + (('__content' in args) || ('__elseContent' in args))) { + var extra = {}; + if ('__content' in args) + extra.__content = args.__content; + if ('__elseContent' in args) + extra.__elseContent = args.__elseContent; + result = result.extend(extra); + } + return result; } else { // Component var kind = kindOrFunc; @@ -1677,7 +1719,7 @@ var codeGenMustache = function (tag, mustacheType) { (argCode ? ', ' + argCode.join(', ') : '') + ')'; }; -Spacebars.compile2 = function (input) { +Spacebars.compile2 = function (input, options) { var tree; // Accept string or output of Spacebars.parse @@ -1690,7 +1732,19 @@ Spacebars.compile2 = function (input) { tree = replaceSpecials(tree); - var code = '(function () { var self = this; return '; + // is this a template, rather than a block passed to + // a block helper, say + var isTemplate = (options && options.isTemplate); + + var code = '(function () { var self = this; '; + if (isTemplate) { + // support `{{> content}}` and `{{> elseContent}}` with + // lexical scope by creating a local variable in the + // template's render function. + code += 'var __content = self.__content, ' + + '__elseContent = self.__elseContent; '; + } + code += 'return '; code += UI.toCode(tree); code += '; })'; diff --git a/packages/templating/plugin/html2_scanner.js b/packages/templating/plugin/html2_scanner.js index 927cdfd544..23736e3ed8 100644 --- a/packages/templating/plugin/html2_scanner.js +++ b/packages/templating/plugin/html2_scanner.js @@ -150,10 +150,9 @@ html2_scanner = { var renderFuncCode = Spacebars.compile2( contents, { - sourceName: 'Template "' + name + '"', - // XXX MESSY HACK - make only Templates expose - // `content` and `elseContent` - preamble: '\n var _local_content = this.content = this.__content;\n var _local_elseContent = this.elseContent = this.__elseContent' }); + isTemplate: true, + sourceName: 'Template "' + name + '"' + }); results.js += "\nTemplate[" + JSON.stringify(name) + "] = UI.Component.extend({kind: " + diff --git a/packages/ui/components.js b/packages/ui/components.js index cf36e02ece..aacf387f00 100644 --- a/packages/ui/components.js +++ b/packages/ui/components.js @@ -56,6 +56,84 @@ UI.If = Component.extend({ } }); +UI.If2 = Component.extend({ + kind: 'If', + init: function () { + // XXX this probably deserves a better explanation if this code is + // going to stay with us. + this.condition = this.data; + + // content doesn't see the condition as `data` + delete this.data; + // XXX I guess this means it's kosher to mutate properties + // of a Component during init (but presumably not before + // or after)? + }, + render: function (buf) { + var self = this; + return function () { + var condition = getCondition(self); + + // `__content` and `__elseContent` are passed by + // the compiler and are *not* emboxed, they are just + // Component kinds. + return condition ? self.__content : self.__elseContent; + }; + } +}); + +// Acts like `!! self.condition()` except: +// +// - Empty array is considered falsy +// - The result is Deps.isolated (doesn't trigger invalidation +// as long as the condition stays truthy or stays falsy +var getCondition = function (self) { + return Deps.isolateValue(function () { + // `condition` is emboxed; it is always a function, + // and it only triggers invalidation if its return + // value actually changes. We still need to isolate + // the calculation of whether it is truthy or falsy + // in order to not re-render if it changes from one + // truthy or falsy value to another. + var cond = self.condition(); + + // empty arrays are treated as falsey values + if (cond instanceof Array && cond.length === 0) + return false; + else + return !! cond; + }); +}; + +UI.Unless2 = Component.extend({ + kind: 'Unless', + init: function () { + this.condition = this.data; + delete this.data; + }, + render: function (buf) { + var self = this; + return function () { + var condition = getCondition(self); + return (! condition) ? self.__content : self.__elseContent; + }; + } +}); + +UI.With2 = Component.extend({ + kind: 'With', + init: function () { + this.condition = this.data; + }, + render: function (buf) { + var self = this; + return function () { + var condition = getCondition(self); + return condition ? self.__content : self.__elseContent; + }; + } +}); + UI.Unless = Component.extend({ kind: 'Unless', init: function () { diff --git a/packages/ui/render2.js b/packages/ui/render2.js index d3fb92f370..57df0828f9 100644 --- a/packages/ui/render2.js +++ b/packages/ui/render2.js @@ -693,3 +693,7 @@ UI.body2 = UI.Component.extend({ // XXX revisit how body works. INSTANTIATED: false }); + +UI.block = function (renderFunc) { + return UI.Component.extend({ render: renderFunc }); +};