In a Meteor project, I have a collection of documents which can have one of two formats. To simplify, a given document may either have a type property or a label property. I would like to display this is a multiple select element, using whichever property they have to identify them.
Here's a working example:
HTML
<body>
{{> test}}
</body>
<template name="test">
<select name="elements" size="2">
{{#each elements}}
<option value="{{id}}">{{id}}: {{type}}{{label}}</option>
{{/each}}
</select>
</template>
JavaScript
Template.test.helpers({
elements: function () {
return [
{ id: 1, type: "type" }
, { id: 2, label: "label" }
]
}
})
This happily displays:
1: type
2: label
However, this is a simplification. The actual data that I want to display is more complex than this. I would rather not put all the concatenation logic into the HTML template. I would much rather call a JavaScript function, passing it the current document, and get the string to display. Something like:
<option value="{{id}}">{{functionToCall_on_this}}</option>
How could I do this with Meteor?
Answering my own question, I have found that I can change my template to include this line...
<option value="{{id}}">{{id}}: {{string}}</option>
And my helpers entry to use the map method:
Template.test.helpers({
elements: function () {
data = [
{ id: 1, type: "type" }
, { id: 2, label: "label" }
]
return data.map(function (item, index, array) {
var string = item.type || item.label
return { id: item.id, string: string }
})
}
})
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?
Relatively new to handlebars.js, but not templating. I am using two loops to output markup in specific order. Both collections.intro and collections.elements are created by https://github.com/segmentio/metalsmith-collections.
Javascript
var order = ['intro', 'elements'];
collections.intro = [
{ title: 'One' },
{ title: 'Two' }
];
collections.elements = [
{ title: 'One' },
{ title: 'Two' }
];
Handlebars
{{#each order}}
{{this}} /* intro, elements */
{{#each ../collections.[this]}}
{{this.title}}
{{/each}}
{{/each}}
Is there anyway to use this from the order loop to access the correct collections. Both collections[intro] and collections[elements] work when hard coded.
Managed to resolve this with a custom helper:
Javascript
Handlebars.registerHelper( 'collection', function ( slug, options ) {
return options.data.root.collections[slug];
});
Handlebars
{{#each order}}
{{#each (collection this)}}
{{this.title}}
{{/each}}
{{/each}}
I want to fetch the last item of my JSON file. So I took a look at Getting the last element from a JSON array in a Handlebars template and tried to implement it. So far it gives me the number of the last entry but I need the options as well but dont know how to do it?
This is from the example mentioned
Handlebars.registerHelper("last", function(array, options) {
return array[array.length-1];
});
I tried to do:
Handlebars.registerHelper("last", function(array, options) {
if (array[array.length-1]) return options.fn(this);
return options.inverse(this);
});
My JSON files structure is:
releases: [{
"title" : "some title",
"releaseDate" : "2014-08-04"
},
"services": [{
"name" : "spotify",
"link" : "some link"
},
{
"name" : "itunes",
"link" : "some link"
}]
]
so my Handlebars template looks like:
{{#each releases}}
{{#last releaseDate}}
{{#each services}}
{{#equal name "Spotify" }}
{{/equal}}
{{#equal name "Itunes" }}
{{/equal}}
{{/each}}
{{/last}}
{{/each}}
But its not working, it displays an empty DIV
please help?
Handlebars already has #first and #last pseudo-variables. See docs on iterations and built-in helpers.
Example use case:
textArray = ["First", "N-1", "Last"]
<span>{{#each textArray}}{{#if #last}}{{this}}{{/if}}{{/each}}</span>
Result: <span>Last</span>
I would use an iterator helper for this.
Handlebars.registerHelper('layoutReleases', function (rows, options) {
var buffer = [], lastRow;
if (rows && rows.length > 0) {
lastRow = rows[rows.length-1];
buffer.push(options.fn(lastRow));
}
return buffer.join('');
});
Template:
{{#layoutReleases releases}}
{{releaseDate}}
{{#each services}}
{{#equal name "Spotify" }}
{{/equal}}
{{#equal name "Itunes" }}
{{/equal}}
{{/each}}
{{/each}}
How to pass a default value for a field in 'insert' form?
I'm using Meteor's packages: Autoform, Collections2, and Simple-Schema.
My process is:
A User selects some value in a list on a page, then
The 'insert' from opens, and I want that one field to be initialized with the value the user chose on a previous step.
Can't figure out how to pass a parameter withing URL (or any other way).
The problem is how to initialize form with the value.
Suppose I have an URL:
http://localhost:3000/activity/new/Sport
===============
router.js:
...
Router.map(function () {
...
this.route('newActivity', {
path: '/activity/new/:tag',
data: function() {
Session.set('tag', this.params.tag);
return null;
}
});
...
===============
models/activity.js
...
Activities = new Meteor.Collection("activities", {
schema: {
title: {
type: String,
label: 'название'
},
...
tag: {
type: String,
label: 'тэг'
}
}
});
================
templates/avtibity.js
...
Template.newActivity.helpers({
defaultTag: function() {
return Session.get('tag');
}
});
...
================
templates/activity.html
...
<template name="newActivity">
<h1>Create new Activity!</h1>
{{#autoForm collection="Activities" id="insertActivityForm" type="insert"}}
{{> afQuickField name="title"}}
...
{{> afQuickField name="tag" value=" ?????? "}} // ? {{defaultTag}}
ho ho ho {{defaultTag}}
{{/autoForm}}
</template>
```
Thanks to Eric Dobbertin:
You could set value equal to a helper that returns the desired value ({{> afQuickField name="tag" value=valueHelper}})
List item You could set doc to an object that has the value set to what you want. Just like you would for an update form.
https://github.com/aldeed/meteor-autoform/issues/210
When I use the built-in block helper #each, book templates are rerendered individually when changed:
users =
_id: 'foo'
books: [
{name: 'book1'}
{name: 'book2'}
]
<template name="user">
{{#each books}}
{{> book}}
{{/each}}
</template>
<template name="book">
<div>{{name}}</div>
</template>
When the data is changed - the first book name is set to 'bookone' instead of 'book1' - only the book template (the div containing 'book1') is rerendered. This is the desired behavior. When I use a custom block helper, the behavior is different:
<template name="user">
{{#each_with_id}}
{{> book}}
{{/each}}
</template>
<template name="book">
<div data-id="{{_id}}">{{name}}</div>
</template>
Templates.user.each_with_id = (options) ->
html = "
for book, i in this.books
this.name = book.name
html += Spark.labelBranch i.toString(), ->
options.fn this
html
Now when the name of the first book changes, the whole user template is rerendered.
It does not work as you expect, because the implementation of built-in each is based on the cursor.observeChanges feature. You will not be able to achieve the same exact result without using an auxiliary collection of some sort. The idea is quite simple. It seems that you don't have a "books" collection but you can create a client-side-only cache:
Books = new Meteor.Collection(null);
where you will need to put some data dynamically like this:
Users.find({/* probably some filtering here */}).observeCanges({
added: function (id, fields) {
_.each(fields.books, function (book, index) {
Books.insert(_.extend(book, {
owner: id,
index: index,
}));
}
},
changed: function (id, fields) {
Books.remove({
owner:id, name:{
$nin:_.pluck(fields.books, 'name')
},
});
_.each(fields.books, function (book, index) {
Books.update({
owner : id,
name : book.name,
}, {$set:_.extend(book, {
owner : id,
index : index,
})}, {upsert:true});
}
},
removed: function (id) {
Books.remove({owner:id});
},
});
Then instead of each_with_id you will be able to the built-in each with appropriate cursor, e.g.
Books.find({owner:Session.get('currentUserId')}, {sort:{index:1}});
You may also look at this other topic which basically covers the same problem you're asking about.