Ember.js - conditionally display template content based on current model item index - javascript

I have an Ember.js app that requires me to render content in groups of 6. For example, I have a model that contains 18 'activities.' I need to group the activities in sets of six, so I'll have three groupings of six each. I know I can't use non-boolean conditionals in Handlebars, so does anyone have an idea on how to best implement the following concept?
<script type="text/x-handlerbars" data-template-name="categories">
{{#each activity in parentController.activity}}
// For every sixth item, start a new grouping
{{#if activity.index() % 6 == 0}}
<div class="activityBlock">
// Render views 1 - 6 the first time, 7 - 12 the second time, and 13 - 18 the third time
{{view "activity"}}
</div>
{{/if}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="activity">
{{item.title}}
</script>

Expanding on what #MatthewBlancarte said in the comments, you can do the following:
App.IndexController = Ember.ArrayController.extend({
categorySlices: function(){
var model = this.get('model')
var slice1 = model.slice(0, 6);
var slice2 = model.slice(6, 12);
var slice3 = model.slice(12);
return [slice1, slice2, slice3];
}.property('model')
})
And then, your template might look like this:
<script type='text/x-handlebars' id='index'>
{{#each slice in categorySlices}}
{{#each item in slice}}
{{ item }}<br>
{{/each}}
<p>
{{/each}}
</script>
See a working solution here

Related

Escaping curly braces in a label in handlebars

I have handlebar code that prints out a list of labels.
This values are:
{{#each theObj.Options}}
{{#if #last}}
{{this.valueLabel}}
{/if}}
{{#unless #last}}
{{this.valueLabel},
{{/unless}}
{{/each}}.
</p>{{/compare}}
The valueLabels are:
Value A
Value B
{Test}
The handlebar code prints out:
The values are: Value A, Value B, .
I need to get the last {Test} to print out. How do I do that? The printout should be:
The values are: Value A, Value B, {Test}.
Thanks.
Setting this up in a quick Code Pen seemed to yield the desired result. This is with three caveats:
I added an extra '{' to line 7 of your HTML, previously: '{/if}}'.
I removed the closing '{{/compare}}' tag as there was no opening one included in your example.
I created an object 'theObj' with a guess at a reasonable example of what should be included in there.
Could you clarify what is in the 'theObj' object so we can troubleshoot that?
HTML
<div id="app"></div>
<script id="entry-template" type="text/x-handlebars-template">
This values are:
{{#each theObj.Options}}
{{#if #last}}
{{this.valueLabel}}
{{/if}}
{{#unless #last}}
{{this.valueLabel}},
{{/unless}}
{{/each}}.
</script>
JavaScript
var theObj = {
Options: [{valueLabel: 'Value A'}, {valueLabel: 'Value B'}, {valueLabel: '{Test}'}]
};
var source = document.getElementById('entry-template').innerHTML;
var template = Handlebars.compile(source);
var context = {theObj: theObj};
var html = template(context);
document.getElementById('app').innerHTML = html;
console.log(html);
You can check out the Code Pen example here:
See the Pen Escaping curly braces in a label in handlebars by Nicholas Lawrence (#nicholaslawrencestudio) on CodePen.

Meteor / Blaze: updating list properties with minimal redraw

I'm encountering serious performance issues related to Blaze redraw. I know that my data structure is at fault but I can't find a way to fix it. This is a bit of a long one: I'll do my best to lay it out clearly.
The goal / the problem
I want to create a schedule view where users can schedule tests. Users should be able to drag and drop test slots, but the resulting redraw is so slow that the thing is unworkable.
The data
I have a Sittings collection which stores details about testing times. Each sitting has a list of scheduleGroups (i.e. exam1 and exam2 happen on Tuesday, exam3 and exam4 happen on Wednesday) The structure looks more or less like this:
var sitting = {
"_id" : "sittingId",
"name" : "Amazing Sitting",
"locationIds" : [
"room1_id",
"room2_id"
],
"startDate" : ISODate("2015-07-01T04:00:00Z"),
"endDate" : ISODate("2015-07-02T04:00:00Z"),
"scheduleGroups" : [
{
"_id" : "dEb3mC7H8RukAgPG3",
"name" : "New group",
"booths" : [
{
"boothNumber" : 1,
"slots" : [
{
"examId" : "exam1",
"dayNumber" : 1,
"startTime" : {
"hour" : 8,
"minutes" : 0
},
"endTime" : {
"hour" : 9,
"minutes" : 30
},
"candidateId" : "odubbD42kHDcR8Egc",
"examinerIds" : [
"GQmvyXbcmMckeNKmB",
"GfoBy4BeQHFYNyowG"
]
}
]
}
]
"locationId" : "room1_id"
}],
"examIds" : [
"exam1",
"exam2",
"exam3"
]
};
The HTML
Sitting is defined as global context in Router.current().data hook. This is part of the redraw problem. Anyway, the following HTML produces the following layout (img below).
<template name='scheduler'>
<div class='container'>
<div class='tabpanel'>
<div class='tab-content' id='scheduler-content'>
{{#each getScheduleGroups}}
<div class='container-fluid schedule-wrapper'>
<div class='row'>
<div class='scheduler-inner-box placeholder-box'></div>
{{#each booths}}
<div class='group-column-head scheduler-inner-box'>{{boothNumber}}</div>
{{/each}}
</div>
<!-- Sittings can take place over several days, normally 2 days -->
{{#each sittingDays}}
<div class='day-wrapper'>
<h3 class='text-center'>Day {{this}}</h3>
<!-- This helper returns a list of start times: [June 1 9AM, June 1 915AM....] -->
{{#each getScheduleTimes ..}}
<div class='row time-row'>
<div class='group-row-head scheduler-inner-box'>
<span class='box-content'>{{this}}</span>
</div>
<!-- Loop through each booth, so that each slot has a square for that boot
i.e. June 1 9AM has space for booth 1, and booth 2, and booth 3, etc
-->
{{#each ../../booths}}
<!-- Paper starting now tells us if there is a slot scheduled to begin at this time or wher its empty-->
<div class='scheduler-inner-box {{#unless paperStartingNow ../.. ..}}open-booth{{/unless}}'>
{{#with paperStartingNow ../.. .. boothNumber}}
<!--
getSlotStyle calculates height and colour, depending on how long the paper is and whether
it's ready to go: i.e. does it have a candidate and the right number of examiners?
-->
<div class='paper-tile tile' {{getSlotStyle this ../boothNumber 'paper'}}>
<span class='slot-name'>{{getSlotName}}</span>
</div>
{{#if slotHasCandidate}}
<div class='candidate-tile tile' {{getSlotStyle this ../boothNumber 'candidate'}}>
<span class='slot-name candidate-name'>{{getCandidateDisplay}}</span>
</div>
{{/if}}
{{#each slotExaminers}}
<div class='examiner-tile tile' {{getSlotStyle .. ../../boothNumber 'examiner' index}}>
<span class='slot-name examiner-name'>{{profile.name}}</span>
</div>
{{/each}}
{{/with}}
</div>
{{/each}}
</div>
{{/each}}
</div>
{{/each}}
</div>
{{/each}}
</div>
</div>
</div>
The Issue
When a user drops a slot (purple tile) onto an empty space, or drags a candidate or examiner to another tile, I update the collection and let Meteor redraw. However the whole thing redraws and I can't find a way to isolate.
Here's the code for moving a whole slot, candidates examiners and all.
moveSlot: function (originalBoothNumber, originalSlot, dropBoothNumber, newSlot) {
check( originalBoothNumber, Number );
check( originalSlot, Object );
check( dropBoothNumber, Number );
check( newSlot, Object );
//pull the old slot
var originalBooth = _.findWhere( this.booths, {boothNumber: originalBoothNumber} );
originalBooth.slots = _.without( originalBooth.slots, originalSlot );
//insert the new one
var dropBooth = _.findWhere( this.booths, {boothNumber: dropBoothNumber} );
dropBooth.slots.push( newSlot );
var modifier = {
$set: {}
};
modifier["$set"]["scheduleGroups." + this.getIndex() + ".booths"] = this.booths;
return Sittings.update({_id: this.getSitting()._id }, modifier);
},
Do I need to change the way I store Sitting.booths? If anyone can recommend a better data structure that will trigger a redraw of just the slot, I'd really appreciate it. I've found a hack that puts every slot into a session variable but there's got to be another way.
Thanks for your patience and your help,
db
Problem
It's because you store the whole set of slots inside one document of booth. Once you update a slot, the whole document is consider updated.
Solutions
IMHO, I don't think you need to change the structure, unless you have build a feature to search for slot or booth. You can set it up by create a series of reactiveVariable or a reactiveDictionary, your template should depends only on that reactive variable. Every time you updated, consider to sync between two of them, either manually or automatically.
PS. I'm not doing any affiliate with this site: https://www.sportsplus.me. But if you sign up -> home -> mlb -> anycontest -> click on plus, you can see their masterpiece infinite scroll with partly redraw. (Open your dev-console and watch the changes in element). They do exactly the same thing you're trying to do with minimum redraw.

Combining 2 Collection in 1 Template Meteor.js

I'm trying to make a category system and i cant seem to figure out how to make it work.
here's a mock js and html to demonstrate what im trying to accomplish
test.js
Categories = new Meteor.Collection('categories');
Rooms = new Meteor.Collection('rooms');
if (Meteor.isClient) {
Template.Category_System.categories = function () {
return Categories.find({});
};
Template.Category_System.rooms = function () {
return Rooms.find({}); //here i want to return the rooms and users in the Rooms collection but only the rooms that fall under the category and subcategory of the Categories collection
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
Categories.insert({category:"category1",subcategories:[{subcategory:"subcategory1",rooms:[]},{subcategory:"subcategory2",rooms:[]}]};
Categories.insert({category:"category2",subcategories:[{subcategory:"subcategory1",rooms:[]},{subcategory:"subcategory2",rooms:[]}]};
Rooms.insert({category:"category1",subcategory:"subcategory1",room:'Room_01',users:[a,b,c]});
Rooms.insert({category:"category1",subcategory:"subcategory1",room:'Room_02',users:[d,e,f,g,h]});
Rooms.insert({category:"category1",subcategory:"subcategory2",room:'Room_03',users:[i]});
Rooms.insert({category:"category2",subcategory:"subcategory1",room:'Room_01',users:[j,k]});
Rooms.insert({category:"category2",subcategory:"subcategory2",room:'Room_02',users:[l,m,n]});
Rooms.insert({category:"category2",subcategory:"subcategory2",room:'Room_03',users:[o,p,q,r]});
});
}
test.html -> just the template
<template name="Category_System">
{{#each categories}}
{{category}}
{{#each subcategories}}
{{subcategory}}
{{#each rooms}}
{{room}}{{users}}
{{/each}}
{{/each}}
{{/each}}
</template>
the outcome i'm trying to achieve
category1
-subcategory1
-Room_01 a,b,c
-Room_02 d,e,f,g,h
-subcategory2
-Room_03 i
category2
-subcategory1
-Room_01 j,k
-subcategory2
-Room_02 l,m,n
-Room_03 o,p,q,r
thanks in advance
Based on your mock code, you could do something like the following:
category.js
Categories = new Meteor.Collection('categories');
Rooms = new Meteor.Collection('rooms');
if (Meteor.isClient) {
Template.Category_System.categories = function () {
return Categories.find();
};
Template.Category_System.rooms = function ( cat ) {
var _subcat = this.subcategory,
_cat = cat;
return Rooms.find({ category: _cat, subcategory: _subcat });
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
if ( !Categories.find().count() ) {
Categories.insert({category:"category1",subcategories:[{subcategory:"subcategory1",rooms:[]},{subcategory:"subcategory2",rooms:[]}]});
Categories.insert({category:"category2",subcategories:[{subcategory:"subcategory1",rooms:[]},{subcategory:"subcategory2",rooms:[]}]});
}
if ( !Rooms.find().count() ) {
Rooms.insert({category:"category1",subcategory:"subcategory1",room:'Room_01',users:["a","b","c"]});
Rooms.insert({category:"category1",subcategory:"subcategory1",room:'Room_02',users:["d","e","f","g","h"]});
Rooms.insert({category:"category1",subcategory:"subcategory2",room:'Room_03',users:["i"]});
Rooms.insert({category:"category2",subcategory:"subcategory1",room:'Room_01',users:["j","k"]});
Rooms.insert({category:"category2",subcategory:"subcategory2",room:'Room_02',users:["l","m","n"]});
Rooms.insert({category:"category2",subcategory:"subcategory2",room:'Room_03',users:["o","p","q","r"]});
}
});
}
category.html
<head>
<title>Category System Test</title>
</head>
<body>
{{> Category_System}}
</body>
<template name="Category_System">
<ul>
{{#each categories}}
<li>
{{category}}
<ul>
{{#each subcategories}}
<li>
{{subcategory}}
<ul>
{{#each rooms ../category }}
<li>{{room}} - {{users}}</li>
{{/each}}
</ul>
</li>
{{/each}}
</ul>
</li>
{{/each}}
</ul>
</template>
Key thing to note is the passing of ../category in the block helper for rooms and the template helper for rooms which accepts the category parameter and also makes use of the current subcategory data context to filter the Room collection.
If you run this under Meteor 0.8.0, you should see the following output...
You could make this easier by completing the reference to the rooms array in each of your Category docs. Either do this through a UI or capture the insertId for each Room doc and update the appropriate Category doc as you seed.
By the way, you'll probably want to throw in a check when you are seeding your collections (see the code above)...otherwise, you'll wind up with lots and lots of records every time you make a change and the app restarts.

Computed.alias not updating bind-attr

I recently started using Ember.js. In my small application I currently have problems regarding Ember.computed.alias, because an {{#if}}-section is updated properly, but the bind-attr helper in the same template is not updated accordingly.
The application controller and the action influencing the value look as follows:
App.ApplicationController = Ember.ObjectController.extend({
isEditing: false,
actions: {
toggleEdit: function() {
var a = this.get('isEditing');
this.set('isEditing', !a);
}
}
});
The controller taking care of the template causing problems:
App.CategoriesController = Ember.ArrayController.extend({
needs: ['application'],
isEditing: Ember.computed.alias('controllers.application.isEditing'),
general: function() { // example depending on the alias
var result = this.filterBy('type', 1);
if (!this.get('isEditing')) {
result = result.filterBy('isHidden', false);
}
return result;
}.property('#each.type', '#each.isHidden', 'isEditing'),
// ......
The related template:
<ul id="categories">
{{#if isEditing}}YES!{{else}}NO!{{/if}}
{{#each general}}
<li {{bind-attr class=":general isEditing:editing"}}>
{{name}}
</li>
{{/each}}
</ul>
When the action toggleEdit is triggered, the {{#if}} section is updated and swaps between YES! and NO!, but the editing class is not applied to the list element. I tried encapsulated the alias into another property of the controller depending on the alias, but without success.
I assume it's a beginners mistake, but I can't figure out what I am overlooking.
Thanking you in anticipation.
isEditing is no longer in scope, use controller.isEditing, sorry phone response
Here's an example that would keep it in scope, but I'm fully qualifying it just to show you.
{{#each item in general}}
<li {{bind-attr class=":general controller.isEditing:editing"}}>
{{item.name}}
</li>
{{/each}}

handlebars - is it possible to access parent context in a partial?

I've got a handlebar template that loads a partial for a sub-element.
I would need to access a variable from the parent context in the calling template, from within the partial. .. doesn't seem to resolve to anything inside the partial.
Simplified code goes like this:
the template
{{#each items}}
{{> item-template}}
{{/each}}
the partial
value is {{value}}
(obviously the real code is more complicated but it's the same principle, within the partial .. appears to be undefined.)
To show it's undefined, I've used a very simple helper whatis like this:
Handlebars.registerHelper('whatis', function(param) {
console.log(param);
});
and updated the above code to this:
updated template
{{#each items}}
{{whatis ..}} <-- Console shows the correct parent context
{{> item-template}}
{{/each}}
updated partial
{{whatis ..}} <-- Console shows "undefined"
value is {{value}}
Is there a way to go around that issue? Am I missing something?
EDIT: There's an open issue relating to this question on handlebars' github project
Just in case anyone stumbles across this question. This functionality exists now in Handlebars.
Do this:
{{#each items}}
{{! Will pass the current item in items to your partial }}
{{> item-template this}}
{{/each}}
Working fiddle (inspired by handlebars pull request #385 by AndrewHenderson)
http://jsfiddle.net/QV9em/4/
Handlebars.registerHelper('include', function(options) {
var context = {},
mergeContext = function(obj) {
for(var k in obj)context[k]=obj[k];
};
mergeContext(this);
mergeContext(options.hash);
return options.fn(context);
});
Here's how you'd setup the parent template:
{{#each items}}
{{#include parent=..}}
{{> item-template}}
{{/include}}
{{/each}}
And the partial:
value is {{parent}}
As of 2.0.0 partials now supports passing in values.
{{#each items}}
{{> item-template some_parent_var=../some_parent_var}}
{{/each}}
Took me awhile to find this, hope it's useful for someone else too!
The easiest way to pass the parent context to the partial is to do the loop inside the partial. This way the parent context is passed by default and when you do the loop inside the partial the {{../variable}} convention can access the parent context.
example fiddle here.
The Data
{
color: "#000"
items: [
{ title: "title one" },
{ title: "title two" },
]
}
The Template
<div class="mainTemplate">
Parent Color: {{color}}
{{> partial}}
</div>
The Partial
<div>
{{#each items}}
<div style="color:{{../color}}">
{{title}}
</div>
{{/each}}
</div>
You can use some of the proposed solutions on the comments from the link to github:
https://github.com/wycats/handlebars.js/issues/182#issuecomment-4206666
https://github.com/wycats/handlebars.js/issues/182#issuecomment-4445747
They create helpers to pass the info to the partial.
I created an each Helper function that includes the parent key/values within the subcontext under the key parentContext.
http://jsfiddle.net/AndrewHenderson/kQZpu/1/
Note: Underscore is a dependency.
Handlebars.registerHelper('eachIncludeParent', function ( context, options ) {
var fn = options.fn,
inverse = options.inverse,
ret = "",
_context = [];
$.each(context, function (index, object) {
var _object = $.extend({}, object);
_context.push(_object);
});
if ( _context && _context.length > 0 ) {
for ( var i = 0, j = _context.length; i < j; i++ ) {
_context[i]["parentContext"] = options.hash.parent;
ret = ret + fn(_context[i]);
}
} else {
ret = inverse(this);
}
return ret;
});
To be used as follows:
{{#eachIncludeParent context parent=this}}
{{> yourPartial}}
{{/eachIncludeParent}}
Access parent context values in your partial using {{parentContext.value}}
I needed dynamic form attributes for something like this...
{{#each model.questions}}
<h3>{{text}}</h3>
{{#each answers}}
{{formbuilder ../type id ../id text}}
{{/each}}
{{/each}}
and a helper like so...
Handlebars.registerHelper('formbuilder', function(type, id, qnum, text, options)
{
var q_type = options.contexts[0][type],
a_id = options.contexts[1].id,
q_number = options.contexts[0][qnum],
a_text = options.contexts[1].text;
return new Handlebars.SafeString(
'<input type=' + q_type + ' id=' + a_id + ' name=' + q_number + '>' + a_text + '</input><br/>'
);
});
Which produces...
<input type="checkbox" id="1" name="surveyQ0">First question</input>
My model is a big blob of arrays and objects mixed together. What's noteworthy is that using '../' like so '../type', passes in the parent model as the context, and without it, such as with 'id', it passes in the current model as the context.
To get specifically the parent of the partial (where you may be several partials deep) then follow the other answers like SeanWM.
If you know that the parent is the main template then you can use #root which resolves to the top-most context no matter how deep you are.
e.g. {{#root.rootObject.rootProperty}}
It is a pity that ../../.. does not go up past a partial.

Categories

Resources