Is it possible to know the HTML element which is invoking a global helper?
I have this blaze template:
<template name="tools">
<div>
<a id="pencil" class="{{toolIsActive}}">Pencil</a>
<a id="shape" class="{{toolIsActive}}">Shape</a>
<a id="poly" class="{{toolIsActive}}">Polygon</a>
<a id="arrow" class="{{toolIsActive}}">Arrow</a>
</div>
</template>
and so it'd be useful a helper like this:
UI.registerHelper('toolIsActive', function() {
return (this.id === currentTool) ? 'active' : '';
});
where I want this to be the invoking HTML element instead of template's data context.
Is there a way to access the element? I know I could use this.$('#pencil') but it's useless since the id it's exactly what I want to know.
You can work around this problem by passing the tool name as an argument for the helper:
<a id="pencil" class="{{toolIsActive 'pencil'}}">Pencil</a>
UI.registerHelper('toolIsActive', function(tool) {
return (tool === currentTool) ? 'active' : '';
});
Since this sort of helper is useful in many different parts of the application, you can make an universal one instead:
<a id="pencil" class="{{classIfEqual 'pencil' currentTool 'active'}}">Pencil</a>
UI.registerHelper('classIfEqual', function(a, b, className) {
return (a === b) ? className : '';
});
Another approach, which could make it easier to add more tools in the future:
<template name="tools">
<div>
{{#each tools}}
<a id="{{id}}" class="{{toolIsActive}}">{{humanName}}</a>
{{/each}}
</div>
</template>
Template.tools.helpers({
tools: [
{id: "pencil", humanName: "Pencil"},
{id: "shape", humanName: "Shape"},
{id: "poly", humanName: "Polygon"},
{id: "arrow", humanName: "Arrow"}
],
toolIsActive: function() {
return (this.id === currentTool) ? "active" : ""
}
});
You could potentially use that tools structure in multiple places, and then if you want to add more tools, you only have to add it in one place.
Related
I'd like to re-use a Meteor template as an inclusion (i.e. using {{> }}) in two different contexts. I know I can pass in different data with the inclusion by using {{> templateName data1=foo data2=bar}}, but I'm struggling to figure out how I can provide different helpers based on the context. Here's the template in question:
<template name="choiceQuestion">
<div class="choice-grid" data-picks="{{numberOfPicks}}" data-toggle="buttons">
{{! Provide for the user to make multiple selections from the multiple choice list }}
{{#if hasMultiplePicks}}
{{#unless canPickAll}}<span class="help-block text-center">Pick up to {{numberOfPicks}}</span>{{/unless}}
{{#each choices}}
<label class="btn btn-default"><input type="checkbox" class="choice" name="{{this}}" autocomplete="off" value="{{this}}" checked="{{isChecked}}"> {{this}}</label>
{{/each}}
{{/if}}{{! hasMultiplePicks}}
{{#if hasSinglePick}}
{{#each choices}}
<label class="btn btn-default"><input type="radio" class="choice" name="{{this}}" id="{{this}}" autocomplete="off" value="{{this}}" checked="{{isChecked}}"> {{this}}</label>
{{/each}}
{{/if}}{{! hasSinglePick}}
</div>
</template>
and here's how I've reused it:
{{> choiceQuestion choices=allInterests picks=4}}
The key component of the template is a checkbox. In one context, it will never be checked. In another, it may be checked based on the contents of a field in the user document. I've added checked={{isChecked}} to the template. I've read this boolean attribute will be omitted if a falsey value is returned from the helper which should work well for my purposes.
The template's JS intentionally does not have an isChecked helper. I had hoped I could provide one on the parent where the template is included in the other context in order to conditionally check the box by returning true if the checked conditions are met, but the template doesn't acknowledge this helper.
Here's the template's JS:
Template.choiceQuestion.helpers({
hasSinglePick: function() {
return this.picks === 1;
},
hasMultiplePicks: function() {
return this.picks > 1 || !this.picks;
},
numberOfPicks: function() {
return this.picks || this.choices.length;
},
canPickAll: function() {
return !this.picks;
},
});
and the parent's JS:
Template.dashboard.helpers({
postsCount: function() {
var count = (Meteor.user().profile.posts||{}).length;
if (count > 0) {
return count;
} else {
return 0;
}
},
isChecked: function() {
return (((Meteor.user() || {}).profile || {}).contentWellTags || []).indexOf(this) > -1 ? 'checked' : null;
}
});
Template.dashboard.events({
'click .js-your-profile-tab': function(){
facebookUtils.getPagesAssumeLinked();
}
});
I've tried a few other approaches as well. I tried passing the helper to the template along with the other context (i.e. {{> templateName data1=foo data2=bar isChecked=isChecked}}. This kinda works, but it calls the helper immediately. This breaks these since I need to use a value from the context to determine what to return from my helper. Since this value doesn't exist when the function returns, the function always returns undefined.
If I return a function from this helper rather than the value and then pass the helper into the template inclusion along with the data context, I get better results. In fact, my console logs show the desired output, but I still don't end up with the checked box I expect.
Here's what that looks like. Returning a function:
isChecked: function() {
var self = this;
return function() {
return (((Meteor.user() || {}).profile || {}).contentWellTags || []).indexOf(this) > -1 ? 'checked' : null;
};
}
and passing that to the template:
{{> choiceQuestion choices=allInterests picks=4 isChecked=isChecked}}
Is there an established pattern for overriding template helpers from the parent or for including helpers on the parent that are missing from the child template? How can I achieve this?
<template name="SideNav">
<ul class='block-items white'>
{{#each blocks}}
<li class='block-item'>
<i class="fa fa-fw fa-folder"></i>
<i class="fa fa-fw fa-folder-open"></i>
<a href='#' class='block-item-link'>
{{name}}
...
{{/each blocks}}
</template>
Given this, I can access each block-item's id when it's clicked by doing
Template.SideNav.events({
"click .block-item": function (e, tem) {
//var blockItemId = this._id;
}
});
How can I borrow the same feature from other places, like onRendered()? Take a look at the following example:
Template.SideNav.onRendered(function() {
this.$('.block-items').sortable({
update: function (e, ui) {
_.each($('.block-item'), function (blockItem) {
// How do I get blockId?
})
console.log("block item rearranged");
}
});
update is a callback function that's invoked when there was a change in the order of block items in the ul list. I need a way to iterate through all the block items and get their corresponding Mongo id's. How can I do it?
Related Documents:
The context of "this" in Meteor template event handlers (using Handlebars for templating)
Update: the "Meteor way"
If you are looking for how Blaze gets this data context for events and helpers, it turns out there is a magical Blaze.getData() function which takes a Blaze view or a DOM object and returns its data context. As far as I could tell by looking at the code, it seems to be the tool Blaze uses for providing data contexts to helpers and events.
So in your case, you could use:
Template.SideNav.onRendered(function() {
this.$('.block-items').sortable({
update: function (e, ui) {
_.each($('.block-item'), function (blockItem) {
var context = Blaze.getData(blockItem.get(0));
var blockId = context._id;
})
console.log("block item rearranged");
}
});
Original answer
An easy way to get the id of a document when working with DOM manipulation (other than blaze events) is to explicitly set it as an attribute in your template, such as:
<template name="SideNav">
<ul class='block-items white'>
{{#each blocks}}
<li class='block-item' id='{{_id}}'>
<i class="fa fa-fw fa-folder"></i>
<i class="fa fa-fw fa-folder-open"></i>
<a href='#' class='block-item-link'>
{{name}}
...
{{/each blocks}}
</template>
This way, you can just fetch the id using jquery's attr method:
Template.SideNav.onRendered(function() {
this.$('.block-items').sortable({
update: function (e, ui) {
_.each($('.block-item'), function (blockItem) {
var blockId = blockItem.attr('id');
})
console.log("block item rearranged");
}
});
In Meteor, I have an app where I make a list of items grouped by tags, where non-tagged items come first and then tagged items are hidden under a "tag header" drop down.
I haven't touched this app since 0.8 came out, and I was using a block helper in a template which worked fine in pre-0.8...
See working jsfiddle here
Handlebars.registerHelper('eachItem', function(context, options) {
var ret = "";
for(var i=0, j=context.length; i<j; i++) {
if(!context[i].tag){
ret = ret + options.fn(context[i]);
} else {
if(i===0||!context[i-1].tag ||context[i-1].tag !== context[i].tag){
ret = ret + '<li class="list-group-item"><a data-toggle="collapse" data-target="#'+ context[i].tag +'"> Items tagged '+context[i].tag + '</a></li><div id="'+context[i].tag+'" class="collapse">';
}
ret = ret + options.fn(context[i]);
if(i+1<j && context[i+1].tag !== context[i].tag){
ret = ret + '</div>';
}
}
}
return ret;
});
But I'm struggling a bit to translate this into post-0.8 Meteor
The inserted HTML must consist of balanced HTML tags. You can't, for example,
insert " </div><div>" to close an existing div and open a new one.
One idea I had was to render the non-tagged items and also the containers in a vanilla {{#each}} loop (with 2 different templates), and then do something like this
Template.myListContainer.rendered = function(){
_.each(tags, function(tag){
var tagged_items = _.filter(items, function(item){ return item.tag == tag; });
_.each(tagged_items, function(item){
UI.insert(UI.RenderWithData(listItemTemplate, { item : item }), tagContainer);
});
});
}
Is there a simpler way to do this ? If the items come from a collection, will they keep their reactivity ?
Many thanks in advance !
If you haven't already, it's worth reading the Meteor wiki on migrating to blaze.
Anyhow, this one seems difficult to implement as a straight iteration.
If you don't need them in a specific order, I would just list items w/ and w/o tags, eg:
Template.example.helpers({
dataWithoutTags: function(){
return items.find({tag:{exists: false}});
},
tagList: function(){
// create a distinct list of tags
return _.uniq(items.find({tag:{exists: true}}, {tag: true}));
},
dataForTag: function(){
// use `valueOf` as `this` is a boxed-string
return items.find({tag: this.valueOf()});
}
});
Template:
<template name="example">
<div class='panel panel-default'>
<ul class='list-group'>
{{#each dataWithoutTags}}
<li class='list-group-item'>{{name}}</li>
{{/each}}
{{#each tagList}}
<li class="list-group-item">
<a data-toggle="collapse" data-target="#{{this}}"> Items tagged {{this}}</a>
</li>
<div id="{{this}}" class="collapse">
{{#each dataForTag}}
<li class='list-group-item'>{{name}}</li>
{{/each}}
</div>
{{/each}}
</ul>
</div>
</template>
If you do need them in a specific order - eg. grouping only consecutive items (as per your example). The only option would be to pre-process them.
eg:
Template.example.helpers({
itemsGrouped: function(){
dataGroups = [];
currentGroup = null;
items.find({}, {sort: {rank: 1}}).forEach(function(item){
if (currentGroup && (!item.tag || currentGroup.tag != item.tag)){
dataGroups.push(currentGroup);
currentGroup = null;
}
if (item.tag){
if (!currentGroup){
currentGroup = {
group: true,
tag: item.tag,
items: [item]
};
} else {
currentGroup.items.push(item);
}
} else {
dataGroups.push(item);
}
});
if (currentGroup){ dataGroups.push(currentGroup); }
return dataGroups;
}
});
Template:
<template name="example">
<div class='panel panel-default'>
<ul class='list-group'>
{{#each itemsGrouped}}
{{#if group}}
<li class="list-group-item">
<a data-toggle="collapse" data-target="#{{tag}}"> Items tagged {{tag}}</a>
</li>
<div id="{{tag}}" class="collapse">
{{#each items}}
<li class='list-group-item'>{{name}}</li>
{{/each}}
</div>
{{else}}
<li class='list-group-item'>{{name}}</li>
{{/if}}
{{/each}}
</ul>
</div>
</template>
If you log the value you are returning from the eachItem helper, you will see you are not closing the last div element. Fixing that may get it working again. However, you have div elements as direct children of the ul element, which you are not supposed to according to the HTML5 specification:
Permitted contents: Zero or more li elements
Also, I think it would be a better option for you to prepare your context in a format that makes it easier to let simple templates take care of the rendering. For example, using the same data from your jsfiddle, suppose you had it in the following format:
tagGroups = [{
names: ["item1", "item2"]
},{
tag: "red",
names: ["item3", "item4"]
},{
tag: "blue",
names: ["item5", "item6", "item7"]
}]
The following templates would give you the expected result in a valid html format. The ending result is a bootstrap panel containing collapsible list-groups:
<template name="accordion">
<div class="panel panel-default">
{{#each tagGroups}}
{{> tagGroup}}
{{/each}}
</div>
</template>
<template name="tagGroup">
{{#if tag}}
{{> namesListCollapse}}
{{else}}
{{> namesList}}
{{/if}}
</template>
<template name="namesList">
<ul class="list-group">
{{#each names}}
<li class="list-group-item">{{this}}</li>
{{/each}}
</ul>
</template>
<template name="namesListCollapse">
<div class="panel-heading"><a data-toggle="collapse" data-target="#{{tag}}">Items tagged {{tag}}</a></div>
<ul id="{{tag}}" class="panel-collapse collapse list-group">
{{#each names}}
<li class="list-group-item">
{{this}}
</li>
{{/each}}
</ul>
</template>
And here is an example of a helper to transform your data so you can make a quick test to see it working:
Template.accordion.tagGroups = function () {
var data = [{
name : 'item1'
},{
name : 'item2'
},{
name : 'item3',
tag : 'red'
},{
name : 'item4',
tag : 'red'
},{
name : 'item5',
tag : 'blue'
},{
name : 'item6',
tag : 'blue'
},{
name : 'item7',
tag : 'blue'
}];
data = _.groupBy(data, 'tag');
data = _.map(data, function (value, key) {
var obj = {};
if (key !== 'undefined') {
obj.tag = key;
}
var names = _.map(value, function (item) {
return item.name;
});
obj.names = names;
return obj;
});
//console.dir(data);
return data;
};
And yes, if the items come from a collection, you will get reactivity by default.
Ok, so I found this post: Meteor breadcrumb
But lets say I have the following:
<template name="somePage">
<h1>Page Title</h1>
{{> breadcrumb}}
</template>
<template name="breadcrumb">
<ul class="breadcrumb">
<li>
Home
</li>
{{#each path}}
<li>
{{this}}
</li>
</ul>
</template>
Helper:
Template.breadcrumb.helpers({
path: function() {
return Router.current().path.split( "/" );
}
});
Ok so the linked question at the top got me the basics. I'm trying to understand how to do a few more things here that should be obvious. I want the first to be for the home page, and the result returned from the path: function() includes an empty "", "page", "page", etc. in the beginning of it.
I'd like to be able to incorporate the proper paths. To be clear, I'd love to pull this off:
<template name="breadcrumb">
<ul class="breadcrumb">
<li>
Home
</li>
~ pseudo logic
{{#each path that isn't current page}}
<li>
{{this}}
</li>
{{/each}}
<li>
{{ currentPage }}
</li>
</ul>
</template>
Has anyone done this or found a reference that I haven't stumbled across yet?
I'll give you my own recipe for breadcrumbs using iron:router.
It works by supplying additional options to your routes in order to establish a hierarchy between them, with parent-children relations. Then we define a helper on the Router to give us a list of parent routes (up to home) for the current route. When you have this list of route names you can iterate over them to create your breadcrumbs.
First, we need to define our breadcrumbs template which is actually very similar to your pseudo-code. I'm using bootstrap and font-awesome, as well as some newly introduced iron:router#1.0.0-pre features.
<template name="breadcrumbs">
<ol class="breadcrumb">
<li>
{{#linkTo route="home"}}
<i class="fa fa-lg fa-fw fa-home"></i>
{{/linkTo}}
</li>
{{#each intermediateRoutes}}
<li>
{{#linkTo route=name}}
<strong>{{label}}</strong>
{{/linkTo}}
</li>
{{/each}}
<li class="active">
<strong>{{currentRouteLabel}}</strong>
</li>
</ol>
</template>
The {{#linkTo}} block helper is new in iron:router#1.0.0-pre, it simply outputs an anchor tag with an href attribute which value is {{pathFor "route"}}.
Let's define the helpers from our breadcrumbs template:
Template.breadcrumbs.helpers({
intermediateRoutes: function() {
if (!Router.current()) {
return;
}
// get rid of both the first item, which is always assumed to be "home",
// and the last item which we won't display as a link
var routes = Router.parentRoutes().slice(1, -1);
return _.map(routes, function(route) {
// extract name and label properties from the route
return {
name: route.getName(),
label: route.options.label
};
});
},
currentRouteLabel: function() {
// return the label property from the current route options
return Router.current() && Router.current().route.options.label;
}
});
Notice that we rely on the existence of a special option named 'label' which represents what we're going to put in our anchors, we could also have used the name for testing purpose.
The parentRoutes method is something we need to extend the Router with:
_.extend(Router, {
parentRoutes: function() {
if (!this.current()) {
return;
}
var routes = [];
for (var route = this.current().route; !_.isUndefined(route); route = this.routes[route.options.parent]) {
routes.push(route);
}
return routes.reverse();
}
});
Again, this function assumes that every route (except "home") has a parent property which contains the name of its parent route, we then iterate to traverse the route hierarchy (think of a tree, like a file system structure) from the current route up to the root route, collecting each intermediate route in an array, along with the current route.
Finally, don't forget to declare your routes with our two additional properties that our code relies on, along with a name which is now mandatory as routes are indexed by name in the Router.routes property:
Router.route("/", {
name: "home"
});
Router.route("/nested1", {
name: "nested1",
parent: "home"
});
Router.route("/nested1/nested2", {
name: "nested2",
parent: "nested1"
});
// etc...
This example is pretty basic and certainly doesn't cover every use case, but should give you a solid start in terms of design logic toward implementing your own breadcrumbs.
Inspired by #saimeunt I created a meteor breadcrumb plugin which can be found here: https://atmospherejs.com/monbro/iron-router-breadcrumb. You also specify a parent route and a title for the route itself.
I used saimeunt answer but had to make small changes to the template and the template helpers because I have parameters in some of my route paths. Here are my changes.
Template changes: add data=getParameter to #linkTo for intermediate routes
<template name="breadcrumbs">
<ol class="breadcrumb">
<li>
{{#linkTo route="dashboard"}}
<i class="fa fa-lg fa-fw fa-home"></i>
{{/linkTo}}
</li>
{{#each intermediateRoutes}}
<li>
{{#linkTo route=name data=getParameters}}
<strong>{{label}}</strong>
{{/linkTo}}
</li>
{{/each}}
<li class='active'>
<strong>{{currentRouteLabel}}</strong>
</li>
</ol>
</template>
Template helper changes: add helper function getParameters to get parameters from current route.
Template.breadcrumbs.helpers({
intermediateRoutes: function () {
if (!Router.current()) {
return;
}
var parentRoutes = Router.parentRoutes();
var routes = parentRoutes.slice(1, -1);
var intermediateRoutes = _.map(routes, function (route) {
return {
name: route.getName(),
label: route.options.label
};
});
return intermediateRoutes;
},
currentRouteLabel: function () {
var currentRouteLabel = Router.current() && Router.current().route.options.label;
return currentRouteLabel;
},
getParameters: function(){
var currentRoute = Router.current();
var parameters = currentRoute.params;
return parameters;
}
});
I'm running into a template context situation that I'm having a hard time finding a way around.
Here's the template in question:
{{#each votes}}
<h3>{{question}}</h3>
<ul>
{{#each participants}}
<li>
<p>{{email}}</p>
<select name="option-select">
{{#each ../options}}
<option value="{{option}}" class="{{is_selected_option}}">{{option}}</option>
{{/each}}
</select>
</li>
{{/each}}
</ul>
</div>
{{/each}}
And here's an example of a vote document:
{
_id: '32093ufsdj90j234',
question: 'What is the best food of all time?'
options: [
'Pizza',
'Tacos',
'Salad',
'Thai'
],
participants: [
{
id: '2f537a74-3ce0-47b3-80fc-97a4189b2c15'
vote: 0
},
{
id: '8bffafa7-8736-4c4b-968e-82900b82c266'
vote: 1
}
]
}
And here's the issue...
When the template drops into the #each for participants, it no longer has access to the vote context, and therefore doesn't have access to the available options for each vote.
I can somewhat get around this by using the ../options handlebars path to jump back into the parent context, but this doesn't affect the context of the template helper, so this in Template.vote.is_selected_option refers to the current participant, not to the current vote or option, and has no way of knowing which option we are currently iterating through.
Any suggestions on how to get around this, without resorting to DOM manipulation and jQuery shenanigans?
This is a templating issue that has come up multiple times for me. We need a formal way of reaching up the template context hierarchy, in templates, template helpers, and template events.
It seems since Spacebars (Meteor's new template engine), you have access to the parent context within {{#each}} blocks using ../.
In Meteor 0.9.1, you can also write a helper and use Template.parentData() in its implementation.
It's not particularly pretty, but I've done something like this:
<template name='forLoop'>
{{#each augmentedParticipants}}
{{> participant }}
{{/each}}
</template>
<template name='participant'>
...
Question: {{this.parent.question}}
...
</template>
// and in the js:
Template.forLoop.helpers({
augmentedParticipants: function() {
var self = this;
return _.map(self.participants,function(p) {
p.parent = self;
return p;
});
}
});
It's similar to the approach that AVGP suggested, but augments the data at the helper level instead of the db level, which I think is a little lighter-weight.
If you get fancy, you could try to write a Handlebars block helper eachWithParent that would abstract this functionality. Meteor's extensions to handlebars are documented here: https://github.com/meteor/meteor/wiki/Handlebars
I don't know the formal way (if there is one), but to solve your issue, I would link the participants with the parent ID like this:
{
_id: "1234",
question: "Whats up?",
...
participants: [
{
_id: "abcd",
parent_id: "1234",
vote: 0
}
]
}
and use this parent_id in helpers, events, etc. to jump back to the parent using findOne.
That is obviously a sub optimal thing to do, but it's the easiest way that comes to my mind as long as there is no way of referencing the parent context.
Maybe there is a way but it is very well hidden in the inner workings of Meteor without mention in the docs, if so: Please update this question if you find one.
It's a long shot, but maybe this could work:
{{#with ../}}
{{#each options}}
{{this}}
{{/each}}
{{/with}}
This should make life easier.
// use #eachWithParent instead of #each and the parent._id will be passed into the context as parent.
Handlebars.registerHelper('eachWithParent', function(context, options) {
var self = this;
var contextWithParent = _.map(context,function(p) {
p.parent = self._id;
return p;
});
var ret = "";
for(var i=0, j=contextWithParent.length; i<j; i++) {
ret = ret + options.fn( contextWithParent[i] );
}
return ret;
});
Go ahead and change
p.parent = self._id;
to whatever you want to access in the parent context.
Fixed it:
// https://github.com/meteor/handlebars.js/blob/master/lib/handlebars/base.js
// use #eachWithParent instead of #each and the parent._id will be passed into the context as parent.
Handlebars.registerHelper('eachWithParent', function(context, options) {
var self = this;
var contextWithParent = _.map(context,function(p) {
p.parent = self._id;
return p;
});
return Handlebars._default_helpers.each(contextWithParent, options);
});
This works :) with no error
Simply register a global template helper:
Template.registerHelper('parentData',
function () {
return Template.parentData(1);
}
);
and use it in your HTML templates as:
{{#each someRecords}}
{{parentData.someValue}}
{{/each}}
======= EDIT
For Meteor 1.2+, you shold use:
UI.registerHelper('parentData', function() {
return Template.parentData(1);
});
I was stuck in a similar way and found that the Template.parentData() approach suggested in other answers currently doesn't work within event handlers (see https://github.com/meteor/meteor/issues/5491). User Lirbank posted this simple workaround:
Pass the data from the outer context to an html element in the inner context, in the same template:
{{#each companies}}
{{#each employees}}
Do something
{{/each}}
{{/each}}
Now the company ID can be accessed from the event handler with something like
$(event.currentTarget).attr('companyId')
"click .selected":function(e){
var parent_id = $(e.currentTarget).parent().attr("uid");
return parent_id
},
<td id="" class="staff_docs" uid="{{_id}}">
{{#each all_req_doc}}
<div class="icheckbox selected "></div>
{{/each}}
</td>
{{#each parent}}
{{#each child}}
<input type="hidden" name="child_id" value="{{_id}}" />
<input type="hidden" name="parent_id" value="{{../_id}}" />
{{/each}}
{{/each}}
The _id is NOT the _did of the thing, it's the id of the parent!