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

Added few extra template helpers #12

Merged
merged 3 commits into from
Feb 7, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
22 changes: 18 additions & 4 deletions package.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
70 changes: 70 additions & 0 deletions template-extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions tests.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template name="testTemplate">
{{> testTemplate1 data}}
</template>

<template name="testTemplate1">
{{> testTemplate2 data}}
</template>

<template name="testTemplate2">
{{testInstance}}{{testData}}
</template>
79 changes: 79 additions & 0 deletions tests.js
Original file line number Diff line number Diff line change
@@ -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;
}
});