Fixes #36 Deprecated invokeBody in favor of renderBody

This commit is contained in:
Patrick Steele-Idem 2015-02-23 16:35:30 -07:00
parent 6a2f603ea9
commit 79aa92254f
13 changed files with 186 additions and 33 deletions

View File

@ -1383,13 +1383,13 @@ exports.render = function(input, out) {
} }
``` ```
If, and only if, a tag has nested content, then a special `invokeBody` method will be added to the `input` object. If a renderer wants to render the nested body content then it must call the `invokeBody` method. For example: If, and only if, a tag has nested content, then a special `renderBody` method will be added to the `input` object. If a renderer wants to render the nested body content then it must call the `renderBody` method. For example:
```javascript ```javascript
exports.render = function(input, out) { exports.render = function(input, out) {
out.write('BEFORE BODY'); out.write('BEFORE BODY');
if (input.invokeBody) { if (input.renderBody) {
input.invokeBody(); input.renderBody(out);
} }
out.write('AFTER BODY'); out.write('AFTER BODY');
} }
@ -1531,12 +1531,12 @@ Marko supports this by leveraging JavaScript closures in the compiled output. A
"tags": { "tags": {
"ui-tabs": { "ui-tabs": {
"renderer": "./tabs-tag", "renderer": "./tabs-tag",
"var": "tabs" "body-function": "getTabs(__tabsHelper)"
}, },
"ui-tab": { "ui-tab": {
"renderer": "./tab-tag", "renderer": "./tab-tag",
"import-var": { "import-var": {
"tabs": "tabs" "tabs": "__tabsHelper"
}, },
"attributes": { "attributes": {
"title": "string" "title": "string"
@ -1557,15 +1557,21 @@ var templatePath = require.resolve('./template.marko');
var template = require('marko').load(templatePath); var template = require('marko').load(templatePath);
exports.render = function(input, out) { exports.render = function(input, out) {
var nestedTabs = []; var nestedTabs;
if (input.getTabs) {
nestedTabs = [];
// Invoke the body function to discover nested <ui-tab> tags
input.getTabs({ // Invoke the body with the scoped "tabs" variable
addTab: function(tab) {
tab.id = tab.id || ("tab" + tabs.length);
nestedTabs.push(tab);
}
});
} else {
nestedTabs = input.tabs || [];
}
// Invoke the body function to discover nested <ui-tab> tags
input.invokeBody({ // Invoke the body with the scoped "tabs" variable
addTab: function(tab) {
tab.id = tab.id || ("tab" + tabs.length);
nestedTabs.push(tab);
}
});
// Now render the markup for the tabs: // Now render the markup for the tabs:
template.render({ template.render({
@ -1596,7 +1602,7 @@ _components/tabs/template.marko:_
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div id="${tab.id}" class="tab-pane" for="tab in data.tabs"> <div id="${tab.id}" class="tab-pane" for="tab in data.tabs">
<invoke function="tab.invokeBody()"/> <invoke function="tab.renderBody(out)"/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -134,7 +134,7 @@ CodeWriter.prototype = {
thisObj = arguments[2]; thisObj = arguments[2];
} }
this.incIndent(delta); this.incIndent(delta);
func.call(thisObj); func.call(thisObj, this);
this.decIndent(delta); this.decIndent(delta);
} else if (typeof arguments[0] === 'string') { } else if (typeof arguments[0] === 'string') {
this.code(this._indent + arguments[0]); this.code(this._indent + arguments[0]);
@ -264,8 +264,8 @@ TemplateBuilder.prototype = {
var newWriter = new CodeWriter(this.concatWrites, oldWriter.indentStr()); var newWriter = new CodeWriter(this.concatWrites, oldWriter.indentStr());
try { try {
this.writer = newWriter; this.writer = newWriter;
func.call(thisObj); var value = func.call(thisObj);
return newWriter.getOutput(); return value == null ? newWriter.getOutput() : value;
} finally { } finally {
this.writer = oldWriter; this.writer = oldWriter;
} }

View File

@ -88,6 +88,7 @@ Taglib.Tag = makeClass({
this.nestedVariables = {}; this.nestedVariables = {};
this.importedVariables = {}; this.importedVariables = {};
this.patternAttributes = []; this.patternAttributes = [];
this.bodyFunction = null;
}, },
inheritFrom: function (superTag) { inheritFrom: function (superTag) {
var subTag = this; var subTag = this;
@ -182,6 +183,12 @@ Taglib.Tag = makeClass({
var key = transformer.path; var key = transformer.path;
transformer.taglibId = this.taglibId; transformer.taglibId = this.taglibId;
this.transformers[key] = transformer; this.transformers[key] = transformer;
},
setBodyFunction: function(name, params) {
this.bodyFunction = {
name: name,
params: params
};
} }
}); });

View File

@ -7,7 +7,6 @@ try {
} }
var ok = require('assert').ok; var ok = require('assert').ok;
var nodePath = require('path'); var nodePath = require('path');
var Taglib = require('./Taglib'); var Taglib = require('./Taglib');
@ -18,6 +17,9 @@ var tagDefFromCode = require('./tag-def-from-code');
var resolve = require('../util/resolve'); // NOTE: different implementation for browser var resolve = require('../util/resolve'); // NOTE: different implementation for browser
var propertyHandlers = require('property-handlers'); var propertyHandlers = require('property-handlers');
var safeVarName = /^[A-Za-z_$][A-Za-z0-9_]*$/;
var bodyFunctionRegExp = /^([A-Za-z_$][A-Za-z0-9_]*)(?:\(([^)]*)\))?$/;
function exists(path) { function exists(path) {
try { try {
require.resolve(path); require.resolve(path);
@ -192,6 +194,29 @@ function buildTag(tagObject, path, taglib, dirname) {
name: value name: value
}); });
}, },
bodyFunction: function(value) {
var parts = bodyFunctionRegExp.exec(value);
if (!parts) {
throw new Error('Invalid value of "' + value + '" for "body-function". Expected value to be of the following form: <function-name>([param1, param2, ...])');
}
var functionName = parts[1];
var params = parts[2];
if (params) {
params = params.trim().split(/\s*,\s*/);
for (var i=0; i<params.length; i++) {
if (params[i].length === 0) {
throw new Error('Invalid parameters for body-function with value of "' + value + '"');
} else if (!safeVarName.test(params[i])) {
throw new Error('Invalid parameter name of "' + params[i] + '" for body-function with value of "' + value + '"');
}
}
} else {
params = [];
}
tag.setBodyFunction(functionName, params);
},
vars: function(value) { vars: function(value) {
if (value) { if (value) {
value.forEach(function(v, i) { value.forEach(function(v, i) {

View File

@ -25,8 +25,8 @@
"char-props": "~0.1.5", "char-props": "~0.1.5",
"events": "^1.0.2", "events": "^1.0.2",
"htmlparser2": "^3.7.2", "htmlparser2": "^3.7.2",
"marko-async": "^1.2.12", "marko-async": "^2.0.0",
"marko-layout": "^1.1.0", "marko-layout": "^2.0.0",
"minimatch": "^0.2.14", "minimatch": "^0.2.14",
"property-handlers": "^1.0.0", "property-handlers": "^1.0.0",
"raptor-args": "^1.0.0", "raptor-args": "^1.0.0",

View File

@ -7,6 +7,8 @@ var attrs = require('raptor-util/attrs');
var forEach = require('raptor-util/forEach'); var forEach = require('raptor-util/forEach');
var markoRegExp = /\.marko(.xml|.html)?$/; var markoRegExp = /\.marko(.xml|.html)?$/;
var req = require; var req = require;
var arrayFromArguments = require('raptor-util/arrayFromArguments');
var logger = require('raptor-logging').logger(module);
function notEmpty(o) { function notEmpty(o) {
if (o == null) { if (o == null) {
@ -20,6 +22,8 @@ function notEmpty(o) {
return true; return true;
} }
var WARNED_INVOKE_BODY = 0;
module.exports = { module.exports = {
s: function(str) { s: function(str) {
return (str == null) ? '' : str; return (str == null) ? '' : str;
@ -132,13 +136,27 @@ module.exports = {
/** /**
* Invoke a tag handler render function * Invoke a tag handler render function
*/ */
t: function (out, renderFunc, input, body) { t: function (out, renderFunc, input, body, hasOutParam) {
if (!input) { if (!input) {
input = {}; input = {};
} }
if (body) { if (body) {
input.invokeBody = body; input.renderBody = body;
input.invokeBody = function() {
if (!WARNED_INVOKE_BODY) {
WARNED_INVOKE_BODY = 1;
logger.warn('invokeBody(...) deprecated. Use renderBody(out) instead.', new Error().stack);
}
if (!hasOutParam) {
var args = arrayFromArguments(arguments);
args.unshift(out);
body.apply(this, args);
} else {
body.apply(this, arguments);
}
};
} }
renderFunc(input, out); renderFunc(input, out);

View File

@ -34,7 +34,15 @@ function getPropsStr(props, template) {
template.indent(function () { template.indent(function () {
forEachEntry(props, function (name, value) { forEachEntry(props, function (name, value) {
if (typeof value === 'function') { if (typeof value === 'function') {
value = value(); value = template.captureCode(function() {
return value(template);
});
if (!value) {
throw new Error('Invalid value for property "' + name + '"');
}
value = template.makeExpression(value);
} }
if (template.isExpression(value)) { if (template.isExpression(value)) {
@ -51,7 +59,6 @@ function getPropsStr(props, template) {
}); });
}); });
if (propsArray.length) { if (propsArray.length) {
return '{\n' + propsArray.join(',\n') + '\n' + template.indentStr() + '}'; return '{\n' + propsArray.join(',\n') + '\n' + template.indentStr() + '}';
} else { } else {
@ -102,16 +109,24 @@ TagHandlerNode.prototype = {
}, },
doGenerateCode: function (template) { doGenerateCode: function (template) {
template.addStaticVar('__renderer', '__helpers.r'); template.addStaticVar('__renderer', '__helpers.r');
var _this = this;
var rendererPath = template.getRequirePath(this.tag.renderer); // Resolve a path to the renderer relative to the directory of the template var rendererPath = template.getRequirePath(this.tag.renderer); // Resolve a path to the renderer relative to the directory of the template
var handlerVar = addHandlerVar(template, rendererPath); var handlerVar = addHandlerVar(template, rendererPath);
var tagHelperVar = template.addStaticVar('__tag', '__helpers.t'); var tagHelperVar = template.addStaticVar('__tag', '__helpers.t');
var bodyFunction = this.tag.bodyFunction;
this.tag.forEachImportedVariable(function (importedVariable) { this.tag.forEachImportedVariable(function (importedVariable) {
this.setProperty(importedVariable.targetProperty, template.makeExpression(importedVariable.expression)); this.setProperty(importedVariable.targetProperty, template.makeExpression(importedVariable.expression));
}, this); }, this);
var _this = this; if (bodyFunction) {
this.setProperty(bodyFunction.name, function(template) {
template.code('function(' + bodyFunction.params + ') {\n').indent(function () {
_this.generateCodeForChildren(template);
}).indent().code('}');
});
}
var variableNames = []; var variableNames = [];
_this.tag.forEachVariable(function (nestedVar) { _this.tag.forEachVariable(function (nestedVar) {
var varName; var varName;
@ -165,14 +180,32 @@ TagHandlerNode.prototype = {
template.code(getPropsStr(_this.getProperties(), template)); template.code(getPropsStr(_this.getProperties(), template));
} }
if (_this.hasChildren()) { if (_this.hasChildren() && !_this.tag.bodyFunction) {
var bodyParams = []; var bodyParams = [];
var hasOutParam = false;
variableNames.forEach(function (varName) { variableNames.forEach(function (varName) {
if (varName === 'out') {
hasOutParam = true;
}
bodyParams.push(varName); bodyParams.push(varName);
}); });
template.code(',\n').line('function(' + bodyParams.join(',') + ') {').indent(function () {
var params;
if (hasOutParam) {
params = bodyParams.join(',');
} else {
params = 'out' + (bodyParams.length ? ',' + bodyParams.join(',') : '');
}
template.code(',\n').line('function(' + params + ') {').indent(function () {
_this.generateCodeForChildren(template); _this.generateCodeForChildren(template);
}).indent().code('}'); }).indent().code('}');
if (hasOutParam) {
template.code(',\n').code(template.indentStr() + '1');
}
} }
}); });
}); });

View File

@ -60,7 +60,7 @@ function testRender(path, data, done, options) {
} }
describe('marko/marko' , function() { describe('marko/render' , function() {
beforeEach(function(done) { beforeEach(function(done) {
// for (var k in require.cache) { // for (var k in require.cache) {

View File

@ -4,12 +4,26 @@
<test-tab title="Tab 1"> <test-tab title="Tab 1">
Tab 1 content Tab 1 content
</test-tab> </test-tab>
<test-tab title="Tab 2"> <test-tab title="Tab 2">
Tab 2 content Tab 2 content
</test-tab> </test-tab>
<test-tab title="Tab 3" if="showConditionalTab"> <test-tab title="Tab 3" if="showConditionalTab">
Tab 3 content Tab 3 content
</test-tab> </test-tab>
</test-tabs> </test-tabs>
<test-tabs-new>
<test-tab-new title="Tab 1">
Tab 1 content
</test-tab-new>
<test-tab-new title="Tab 2">
Tab 2 content
</test-tab-new>
<test-tab-new title="Tab 3" if="showConditionalTab">
Tab 3 content
</test-tab-new>
</test-tabs-new>

View File

@ -1 +1 @@
<div class="tabs"><ul class="nav nav-tabs"><li class="active"><a href="#tab0" data-toggle="tab">Tab 1</a></li><li><a href="#tab1" data-toggle="tab">Tab 2</a></li></ul><div class="tab-content"><div id="tab0" class="tab-pane active">Tab 1 content</div><div id="tab1" class="tab-pane">Tab 2 content</div></div></div> <div class="tabs"><ul class="nav nav-tabs"><li class="active"><a href="#tab0" data-toggle="tab">Tab 1</a></li><li><a href="#tab1" data-toggle="tab">Tab 2</a></li></ul><div class="tab-content"><div id="tab0" class="tab-pane active">Tab 1 content</div><div id="tab1" class="tab-pane">Tab 2 content</div></div></div><div class="tabs"><ul class="nav nav-tabs"><li class="active"><a href="#tab0" data-toggle="tab">Tab 1</a></li><li><a href="#tab1" data-toggle="tab">Tab 2</a></li></ul><div class="tab-content"><div id="tab0" class="tab-pane active">Tab 1 content</div><div id="tab1" class="tab-pane">Tab 2 content</div></div></div>

View File

@ -26,6 +26,19 @@
"title": "string" "title": "string"
} }
}, },
"test-tabs-new": {
"renderer": "./tabs-new-tag.js",
"body-function": "buildTabs(__tabsHelper)"
},
"test-tab-new": {
"renderer": "./tab-new-tag.js",
"import-var": {
"tabs": "__tabsHelper"
},
"attributes": {
"title": "string"
}
},
"test-dynamic-attributes": { "test-dynamic-attributes": {
"renderer": "./dynamic-attributes-tag.js", "renderer": "./dynamic-attributes-tag.js",
"attributes": { "attributes": {

View File

@ -0,0 +1,4 @@
exports.render = function(input, out) {
var tabs = input.tabs;
tabs.addTab(input);
};

View File

@ -0,0 +1,33 @@
var marko = require('../../');
exports.render = function(input, out) {
var tabs = [],
activeFound = false;
if (input.buildTabs) {
input.buildTabs({
addTab: function(tab) {
if (tab.active) {
tab.activeFound = true;
}
tab.id = "tab" + tabs.length;
tabs.push(tab);
}
});
}
if (!activeFound && tabs.length) {
tabs[0].active = true;
}
tabs.forEach(function(tab) {
tab.liClass = tab.active ? "active" : "";
tab.divClass = tab.active ? "tab-pane active" : "tab-pane";
});
marko.render(require.resolve('./tabs.marko'), {
tabs: tabs
}, out);
};