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 @@
+
+ {{> testTemplate1 data}}
+
+
+
+ {{> testTemplate2 data}}
+
+
+
+ {{testInstance}}{{testData}}
+
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;
+ }
+});