How can I output the peoples names below? e.g. Martin and Tabitha?
people:
- martin:
job: Developer
skills:
- python
- perl
- pascal
- tabitha:
job: Developer
skills:
- lisp
- fortran
- erlang
Here's the loop:
{{#each people}}
{{ this }}
{{/each}}
I've found the answer to be:
{{#each people}}
{{#key}}: {{this}}
{{/each}}
You can create a block helper list to iterate over people and use the current index as a private variable to obtain the name which is the first key in the object: Object.keys(context[i])[0].
Code:
var people = [{"martin": {"job": "Developer","skills": ["python","perl","pascal"]}}, {"tabitha": {"job": "Developer","skills": ["lisp","fortran","erlang"]}}];
// Register list helper
Handlebars.registerHelper('list', function (context, options) {
var out = '<ul>',
data;
if (options.data) {
data = Handlebars.createFrame(options.data);
}
for (var i = 0, l = context.length; i < l; i++) {
if (data) {
data.index = Object.keys(context[i])[0];
}
out += '<li>' + options.fn(context[i], {
data: data
}) + '</li>';
}
out += '</ul>';
return out;
});
// The main template
var main = Handlebars.compile($('#main').html());
// Register the list partial that 'main' uses
Handlebars.registerPartial('list', $('#list').html());
// Render the list
$('#output').html(main({
people: people
}));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js"></script>
<script id="list" type="x-handlebars-template">
{{#list people}}
{{#index}}
{{/list}}
</script>
<script id="main" type="x-handlebars-template">
{{> list}}
</script>
<div id="output"></div>
Note: I have converted the yaml to an array for the code example.
Related
I have an array of objects that i have received from my web API, and i am using handlebars to create a template and iterate over these objects. However i am unsure on how i iterate over my array of objects.
JS:
$(document).ready(function(){
$.get("/api/CaseStudies/top", function (data) {
var caseStudies = [];
for (var i = 0, caseStudie; (caseStudie = data[i]); ++i) {
caseStudies.push(caseStudie);
}
//console.log(caseStudies);
renderTemplate(caseStudies);
});
function renderTemplate(caseStudies) {
var template = $('#test').html();
var templateScript = Handlebars.compile(template);
var html = templateScript(caseStudies);
$("#output").append(html);
}
});
HTML:
<script id="test" type="text/x-handlebars-template">
<div>
{{#each caseStudies}}
The title is {{Title}}And my name is{{Name}}<br />
{{/each}}
</div>
</script>
<div id="output"></div>
The array of objects i receive from my api looks like this:
I am trying to link the data from foos and selectedFoos. I wish to list the selectedFoos and show the name from the foos object. The fooid in the selectedFoos would be linked to the foos id.
EDIT: I dont want to alter the structure of foos or selectedFoos.
fiddle is here
Html, Template
<div id="content"></div>
<script id="content_gen" type="x-jsrender">
<ul> {^{for sf}}
<li > {{: fooid}} - {{: code}} {{foo.name}} </li>
{{/for}}
</ul>
</script>
JS
var foos = [{
"id": 1,
"name": "a"
}, {
"id": 2,
"name": "b"
}, {
"id": 3,
"name": "c"
}];
var selectedFoos = [{
"fooid": 1,
"code": "z"
}, {
"fooid": 3,
"code": "w"
}];
var app = {
sf: selectedFoos,
f: foos
};
var templ = $.templates("#content_gen");
templ.link("#content", app);
You could add a view converter to lookup the name by id.
Like this - http://jsfiddle.net/Fz4Kd/11/
<div id="content"></div>
<script id="content_gen" type="x-jsrender">
<ul> {^{for sf}}
<li>{{id2name:fooid ~root.f }} - {{: code}} </li>
{{/for}}
</ul>
</script>
js
var app = {
sf: selectedFoos,
f: foos
};
$.views.converters("id2name", function (id, foos) {
var r = $.grep(foos, function (o) {
return o.id == id;
})
return (r.length > 0) ? r[0].name : '';
});
var templ = $.templates("#content_gen");
templ.link("#content", app);
Scott's answer is nice. But since you are using JsViews - you may want to data-link so you bind to the name and code values. Interesting case here, where you want to bind while in effect traversing a lookup...
So there are several possible approaches. Here is a jsfiddle: http://jsfiddle.net/BorisMoore/7Jwrd/2/ that takes a modified version of Scott's fiddle, with a slightly simplified converter approach, but in addition shows using nested {{for}} loops, as well as two different examples of using helper functions.
You can modify the name or the code, and see how the update works. You'll see that code updates in all cases, but to get the name to update is more tricky given the lookup.
You'll see that in the following two approaches, even the data-binding to the name works too.
Nested for loops
Template:
{^{for sf }}
{^{for ~root.f ~fooid=fooid ~sf=#data}}
{{if id === ~fooid}}
<li>{^{:name}} - {^{:~sf.code}} </li>
{{/if}}
{{/for}}
{{/for}}
Helper returning the lookup object
Helper:
function getFoo(fooid) {
var r = $.grep(foos, function (o) {
return o.id == fooid;
})
return r[0] || {name: ""};
}
Template:
{^{for sf}}
<li>{^{:~getFoo(fooid).name}} - {^{:code}} </li>
{{/for}}
See the many topics and samples here
http://www.jsviews.com
such as the following:
http://www.jsviews.com/#converters
http://www.jsviews.com/#helpers
http://www.jsviews.com/#fortag
http://www.jsviews.com/#iftag
http://www.jsviews.com/#samples/data-link/for-and-if
You should iterate over selectedFoos and lookup the name with fooid by iterating over foos. Then combine that data before rendering.
function getNameById(id) {
for (var i = 0; i < foos.length; i++)
if (foos[i].id == id)
return foos[i].name;
return '';
}
This function will return the name when given the id.
Usage:
alert(getNameById(2)); // alerts "b"
Summary:
Child categories nested inside of Parent Categories are not getting rendered in a Meteor template.
Details:
Consider a data model for 'Category' as such:
// Model Schema
Category {
idCategory : 20, (id of the category itself)
idCategoryParent : 0, (idCategory of our parent category)
defaultLabel : "Movies" (our label)
}
There are parent categories and child categories. Parent categories have an idCategoryParent property value of 0. Child categories store the idCategory of their parents as their idCategoryParent property. I'm trying to loop through a collection of these Categories and render them in the following way:
<b>Movies</b> // parent category is in bold
<ul> // child categories are rendered as an unordered list
<li>Horror</li>
<li>Comedy</li>
<li>Action</li>
<li>Drama</li>
</ul>
<b>Music</b>
<ul>
<li>Rock</li>
<li>Classical</li>
<li>Ambient</li>
</ul>
However, this is what I actually get:
<b>Movies</b>
<ul> // empty...
</ul>
<b>Music</b>
<ul>
</ul>
Source Code:
// How we get the 'categories_parents' data
Template.content.categories_parents = function (){
/*
* Get all parent categories (categories with an idCategoryParent of 0)
*/
var parents = Categories.find({idCategoryParent:0});
var pCount = parents.count();
for (var i = 0; i < pCount; i++){
var pId = parents.db_objects[i]['idCategory'];
/*
* Get all child categories of the parent (categories with
* an idCategoryParent equal to value of parent category's idCategory).
*/
var children = Categories.find({idCategoryParent:pId});
var cCount = children.count();
/*
* Assign the child categories array as a property of the parent category
* so that we can access it easily in the template #each expression
*/
parents.db_objects[i]['children'] = children;
}
return parents;
}
// This is our template
<template name="content">
<h1>Categories</h1>
{{#each categories_parents}}
<b>{{defaultLabel}}</b><br />
<ul>
{{#each children}}
<li>{{defaultLabel}}</li>
{{/each}}
</ul>
{{/each}}
</template>
Other template configurations I have tried in troubleshooting:
{{#each children}}
<li>A Child Exists Here</li> // Even this never rendered... no children?
{{/each}}
Any clues as to why this is happening would be appreciated.
Your model is kind of iffy... Consider
{name:"Category name", parent:"_id of parent category"}
Okay, that's a lot simpler. To create a category.
var moviesId = Categories.insert({name:"Movies"});
Categories.insert({name:"Horror",parent:moviesId});
That was easy enough. Now, to render in a way that {{#each}} works:
Template.categories.categories = function(parent) {
if (parent) {
return Categories.find({parent:parent}).fetch();
} else {
return Categories.find({parent:{$exists:false}});
}
}
You might be seeing where this is going...
<template name="categories">
{{#each categories}}
<ul>{{name}}
{{#each categories _id}}
<li>{{name}}</li>
{{/each}}
</ul>
{{/each}}
</template>
Now I'm not sure if the {{#each}} block helper can take a function argument when it calls another helper. If it doesn't...
Template.categories.categories = function() {
return Categories.find({parent:{$exists:false}}).map(function(parentCategory) {
return _.extend(parentCategory,
{children:Categories.find({parent:parentCategory._id}).fetch()});
});
}
That's a real doozy. It returns parent categories with a new "children" list property, that contains all the children categories. Now you can do:
<template name="categories">
{{#each categories}}
<ul>{{name}}
{{#each children}}
<li>{{name}}</li>
{{/each}}
</ul>
{{/each}}
</template>
Clever, eh?
I don't know about db_objects, when I try and access that property on a cursor (which is what find() returns), it's null.
You could fetch the items that matches your query instead, and then do your iteration:
Template.content.categories_parents = function (){
var parents = Categories.find({idCategoryParent:0}).fetch(); // Returns an array.
for (var i = 0; i < parents.length; i++){
var pId = parents[i]['idCategory'];
var children = Categories.find({idCategoryParent:pId});
// No need for array here, cursor is fine.
parents.db_objects[i]['children'] = children;
}
return parents;
}
I'm new at this myself, so maybe there's a more efficient way of doing it, but I don't know it currently.
Update after Eric's comment.
The js file looks like this:
Categories = new Meteor.Collection("categories");
if (Meteor.isClient) {
Template.categories.categories = function () {
var parents = Categories.find({idCategoryParent:0}).fetch();
for (var i = 0; i < parents.length; i += 1) {
var pId = parents[i]['idCategory'];
var children = Categories.find({idCategoryParent:pId});
parents[i]['children'] = children;
}
return parents;
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
Categories.remove({});
var data = [
{idCategoryParent: 0, idCategory: 1, label: "Movies"},
{idCategoryParent: 1, idCategory: 0, label: "Science Fiction"},
{idCategoryParent: 1, idCategory: 0, label: "Drama"},
{idCategoryParent: 0, idCategory: 2, label: "Music"},
{idCategoryParent: 2, idCategory: 0, label: "Jazz"},
{idCategoryParent: 2, idCategory: 0, label: "Piano"}
];
for (var i = 0; i < data.length; i += 1) {
Categories.insert(data[i]);
}
});
}
The html file looks like this:
<head>
<title>nested_template</title>
</head>
<body>
{{> categories}}
</body>
<template name="categories">
<h1>Categories</h1>
{{#each categories}}
<b>{{label}}</b>
<ul>
{{#each children}}
<li>{{label}}</li>
{{/each}}
</ul>
{{/each}}
</template>
It works just fine for me.
Solved.
My solution was to remove the {{#each}} logic from the template and replace it with a single handlebars helper expression, and pass back the needed html all at once. The html is generated from data in the cursor of the Categories collection.
Not so sure about having all this html in my logic though -- is this bad design? If so I'll defer to a better answer.
// This is the new template
<template name="content">
<h1>Categories</h1>
{{listCategories}}
</template>
// Handlebars helper
Handlebars.registerHelper('listCategories', function() {
var parents = Categories.find({idCategoryParent:0});
var countParents = parents.count();
var string = '';
// iterate over each parent category
for(m = 0; m < countParents; m++){
// Get the parents category id
var pId = parents.db_objects[m].idCategory;
var children = Categories.find({idCategoryParent:pId});
var count = children.count();
/*
* Build the Parent Category html
* Example: <b>Movies</b><ul>
*/
string = string + '<b>' + parents.db_objects[m].defaultLabel + '</b><ul>';
// iterate over each child category
for(var i = 0; i < count; i++){
/*
* Build the child category html
* Example: <li>Horror</li>
*/
string = string + '<li>' + children.db_objects[i]['defaultLabel'] + '</li>';
}
// Close up the unordered list
string = string + '</ul>';
}
// Return the string as raw html
return new Handlebars.SafeString(string);
});
// Rendered out the result correctly like so:
<b>Movies</b>
<ul>
<li>Horror</li>
<li>Comedy</li>
<li>Action</li>
<li>Drama</li>
</ul>
<b>Music</b>
<ul>
<li>Rock</li>
<li>Classical</li>
<li>Ambient</li>
</ul>
I have a product block in a template like this :
<script type="text/x-handlebars-template" id="tmpl-person">
<div class="product">
<!-- Product details here -->
</div>
</script>
What I want to do is if from the array of person I get as data , after every three persons , I want to insert a container called <div class="row-fluid"></div> and three persons inside it.. then a row-fluid container and three persons inside it. How can I achieve this using helpers ? Thanks for help.
You could use something like this
Handlebars.registerHelper('each', function(context, block) {
var ret = "";
for(var i=0, j=context.length; i<j; i++) {
ret = ret + "<li>" + block(context[i]) + "</li>";
}
if( i % 3 == 0)
ret = ret + <div class="row-fluid"></div>
return ret;
});
And you could define your custom iterator like following
<script type="text/x-handlebars-template" id="tmpl-person">
{{#each productInfo}}
<div class="product">
<!-- Product details here -->
</div>
{{/each}}
</script>
I created a little example for myself to test some stuff with Meteor. But right now it looks like I can't subscribe to a collection, I published on the server side. I hope somebody can tell me where the bug is.
server/model.js
Test = new Meteor.Collection("test");
if (Test.find().count() < 1) {
Test.insert({id: 1,
name: "test1"});
Test.insert({id: 2,
name: "test2"});
}
Meteor.publish('test', function () {
return Test.find();
});
client/test.js
Meteor.subscribe("test");
Test = new Meteor.Collection("test");
Template.hello.test = function () {
console.log(Test.find().count());//returns 0
return Test.findOne();
}
Template.hello.events = {
'click input' : function () {
// template data, if any, is available in 'this'
if (typeof console !== 'undefined')
console.log("You pressed the button");
}
};
client/test.html
<head>
<title>test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
<h1>Hello World!</h1>
{{#with test}}
ID: {{id}} Name: {{name}}
{{/with}}
<input type="button" value="Click" />
</template>
EDIT 1
I want to change the object test, findOne() returns. Let's say for adding an attribute avg which contains the average value of two numbers (test.number1 and test.number2). In my opinion this should look like the following code. But javascript is not synchronous, so this won't work.
Template.hello.test = function () {
var test = Test.findOne();
test.avg = (test.number1 + test.number2) / 2;
return test;
}
EDIT 2
This code worked for me. Now I have to rethink why this solution with 'if (test)' just works with findOne() without a selector in my original project.
Template.hello.test = function () {
var avg = 0, total = 0, cursor = Test.find(), count = cursor.count();
cursor.forEach(function(e)
{
total += e.number;
});
avg = total / count;
var test = Test.findOne({id: 1});
if (test) {
test.avg = avg;
}
return test;
}
The latency the client db uses to replicate data might cause the situation wherein the cursor reckons no results. This especially occurs when the template is immediately rendered as the app loads.
One workaround is to observe query documents as they enter the result set. Hence, something like the following for example happens to work pretty well:
Meteor.subscribe("Coll");
var cursor = Coll.find();
cursor.observe({
"added": function (doc) {
... something...
}
})
Try to surround {{#with test}}...{{/with}} with {{#if}}...{{/if}} statement (because in first data push test does not have id and name fields):
<head>
<title>test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
<h1>Hello World!</h1>
{{#if test}}
{{#with test}}
ID: {{id}} Name: {{name}}
{{/with}}
{{/if}}
<input type="button" value="Click" />
</template>
As a result:
UPDATE:
This code performs calculation of average of field number in all records:
model.js:
Test = new Meteor.Collection("test");
Test.remove({});
if (Test.find().count() < 1)
{
Test.insert({id: 1,
name: "test1",
number: 13});
Test.insert({id: 2,
name: "test2",
number: 75});
}
test.js
Test = new Meteor.Collection("test");
Template.hello.test = function () {
var avg = 0, total = 0, cursor = Test.find(), count = cursor.count();
cursor.forEach(function(e)
{
total += e.number;
});
avg = total / count;
return { "obj": Test.findOne(), "avg": avg };
}
UPDATE 2:
This code snippet works for me:
var test = Test.findOne();
if (test)
{
test.rnd = Math.random();
}
return test;
Maybe you should try to wrap assignment code into if statement too?