diff --git a/README.md b/README.md index 3f5eadb..ac3699c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ A smart package for Meteor that allows you to: * inherit the helpers from another template. * inherit the events from another template. * extend abstract templates and overwrite their events/helpers. +* use `template.parent(numLevels)` to access a parent template instance. +* use `template.get(fieldName)` to access the first field named `fieldName` in the current or ancestor template instances. +* pass a function to `Template.parentData(fun)` to get the first data context which passes the test. ## Prerequisites @@ -178,7 +181,32 @@ Template.bar.helpers({ In this example, we defined "foo" and "bar" templates that get their HTML markup, events, and helpers from a base template, `abstract_foo`. We then override the `images` helper for "foo" and "bar" to provide template-specific images provided by different Meteor methods. +## template.parent(numLevels) + +On template instances you can now use `parent(numLevels)` method to access a parent template instance. +`numLevels` is the number of levels beyond the current template instance to look. Defaults to 1. + +## template.get(fieldName) + +To not have to hard-code the number of levels when accessing parent template instances you can use +`get(fieldName)` method which returns the value of the first field named `fieldName` in the current +or ancestor template instances, traversed in the hierarchical order. This pattern makes it easier to +refactor templates without having to worry about changes to number of levels. + +## Template.parentData(fun) + +`Template.parentData` now accepts a function which will be used to test each data context when traversing +them in the hierarchical order, returning the first data context for which the test function returns `true`. +This is useful so that you do not have to hard-code the number of levels when accessing parent data contexts, +but you can use a more logic-oriented approach. For example, search for the first data context which contains +a given field. Or: + +```js +Template.parentData(function (data) {return data instanceof MyDocument;}); +``` + ## Contributors * @aldeed ([Support via Gratipay](https://gratipay.com/aldeed/)) * @grabbou +* @mitar diff --git a/package.js b/package.js index a28b54e..274c4a1 100644 --- a/package.js +++ b/package.js @@ -6,11 +6,25 @@ Package.describe({ }); Package.on_use(function(api) { + api.versionsFrom('METEOR@1.0'); api.use([ - 'templating@1.0.0', - 'blaze@2.0.0', - 'jquery@1.0.0' - ]); + 'templating', + 'blaze', + 'jquery', + 'tracker' + ], 'client'); api.add_files(['template-extension.js'], 'client'); }); + +Package.on_test(function(api) { + api.use([ + 'aldeed:template-extension', + 'templating', + 'tinytest', + 'test-helpers', + 'ejson' + ], 'client'); + + api.add_files(['tests.html', 'tests.js'], 'client'); +}); diff --git a/template-extension.js b/template-extension.js index 08e33b7..73a5fc2 100644 --- a/template-extension.js +++ b/template-extension.js @@ -149,6 +149,76 @@ Template.prototype.copyAs = function (newTemplateName) { newTemplate.inheritsEventsFrom(name); }; +// Allow easy access to a template instance field when you do not know exactly +// on which instance (this, or parent, or parent's parent, ...) a field is defined. +// This allows easy restructuring of templates in HTML, moving things to included +// templates without having to change everywhere in the code instance levels. +// It also allows different structures of templates, when once template is included +// at one level, and some other time at another. Levels do not matter anymore, just +// that the field exists somewhere. +Blaze.TemplateInstance.prototype.get = function (fieldName) { + var template = this; + while (template) { + if (fieldName in template) { + return template[fieldName]; + } + template = template.parent(); + } +}; + +// Access parent template instance. "height" is the number of levels beyond the +// current template instance to look. +Blaze.TemplateInstance.prototype.parent = function(height) { + // If height is null or undefined, we default to 1, the first parent. + if (height == null) { + height = 1; + } + + var i = 0; + var template = this; + while (i < height && template) { + var view = template.view.parentView; + while (view && !view.template) { + view = view.parentView; + } + if (!view) { + return null; + } + // Body view has template field, but not templateInstance, + // which more or less signals that we reached the top. + template = typeof view.templateInstance === 'function' ? view.templateInstance() : null; + i++; + } + return template; +}; + +// Allow to specify a function to test parent data for at various +// levels, instead of specifying a fixed number of levels to traverse. +var originalParentData = Blaze._parentData; +Blaze._parentData = function (height, _functionWrapped) { + // If height is not a function, simply call original implementation. + if (typeof height !== 'function') { + return originalParentData(height, _functionWrapped); + } + + var theWith = Blaze.getView('with'); + var test = function () { + return height(theWith.dataVar.get()); + }; + while (theWith) { + if (Tracker.nonreactive(test)) break; + theWith = Blaze.getView(theWith, 'with'); + } + + // _functionWrapped is internal and will not be + // specified with non numeric height, so we ignore it. + if (!theWith) return null; + // This registers a Tracker dependency. + return theWith.dataVar.get(); +}; + +Template.parentData = Blaze._parentData; + /* PRIVATE */ function parseName(name) { diff --git a/tests.html b/tests.html new file mode 100644 index 0000000..85c9c68 --- /dev/null +++ b/tests.html @@ -0,0 +1,11 @@ + + + + + diff --git a/tests.js b/tests.js new file mode 100644 index 0000000..e69ca10 --- /dev/null +++ b/tests.js @@ -0,0 +1,79 @@ +var testingInstance = false; +var testingData = false; + +Template.testTemplate.created = function () { + this._testTemplateField = 42; +}; + +Template.testTemplate.helpers({ + data: function () { + return _.extend({}, this, {data1: 'foo'}); + } +}); + +Template.testTemplate1.created = function () { + this._testTemplateField1 = 43; +}; + +Template.testTemplate1.helpers({ + data: function () { + // We add data2, but remove data1. + return _.omit(_.extend({}, this, {data2: 'bar'}), 'data1'); + } +}); + +Template.testTemplate2.created = function () { + this._testTemplateField3 = 44; +}; + +Template.testTemplate2.helpers({ + testInstance: function () { + if (testingInstance) return EJSON.stringify(Template.instance().get(this.fieldName)); + }, + + testData: function () { + if (testingData) return EJSON.stringify(Template.parentData(this.numLevels)); + } +}); + +// Tests both get and parent because get uses parent. +Tinytest.add('template-extension - get', function (test) { + testingInstance = true; + try { + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {fieldName: '_testTemplateField'}), '42'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {fieldName: '_testTemplateField1'}), '43'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {fieldName: '_testTemplateField3'}), '44'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {fieldName: '_nonexistent'}), ''); + } + finally { + testingInstance = false; + } +}); + +Tinytest.add('template-extension - parentData', function (test) { + testingData = true; + try { + // Testing default behavior. + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {}), '{"data1":"foo"}'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: undefined}), '{"data1":"foo"}'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: null}), '{"numLevels":null,"data1":"foo"}'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: 0}), '{"numLevels":0,"data2":"bar"}'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: 1}), '{"numLevels":1,"data1":"foo"}'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: 2}), '{"numLevels":2}'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: 3}), 'null'); + + // Testing a function. + var hasField = function (fieldName) { + return function (data) { + return fieldName in data; + }; + }; + + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: hasField('data1')}), '{"data1":"foo"}'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: hasField('data2')}), '{"data2":"bar"}'); + test.equal(Blaze.toHTMLWithData(Template.testTemplate, {numLevels: hasField('data3')}), 'null'); + } + finally { + testingData = false; + } +});