How would I control dynamic layout with ember/handlebars?


#1

Let’s say I have a column field associated with many posts, and I want to change the layout when the column number increments. It is not always sequential. In PHP I would use a counter and compare that with the last value to see if the column has changed, so I can insert the markup. Is there a good way to do this in ember?


#2

Are you talking about a table layout, and when you add another column field it’ll also add another column? It’s a little hard to picture what you’re describing.


#3

No, I am trying to layout via floats, but I need to way to delineate when a column should be created based on dynamic data, or rather, when the markup should be inserted to float a new div. In an {{#each}} loop, I don’t see a way of testing the {{col}} field to see if the column should be incremented.

So in PHP, i would do something like this:

foreach($rows as $row){
    if($row['col']==$lastcol){
        // this is the markup for a new column to begin
        echo '</div><div class="col">';
    }
    echo 'column inner content';
    $lastcol = $col;
}

#4

I’m still a little confused as to what you’re trying to do, but this might be a perfect time to use Handlebars helpers. Recreating your snippet of php (in somewhat pseudo code) might look something like:

// helper
Handlebars.registerHelper('table', function(rows, options) {
  var lastCol,
      out = "<ul class='col'>";
  for(var i=0, l=rows.length; i<l; i++) {
    if(rows[i].col == lastCol){
      out += "</ul><ul class='col'>";
    }
    out += "<li>" + options.fn(rows[i]) + "</li>";
    lastCol = rows[i].col;
  }
  return out + "</ul>";
});

// in app
{{#table rows}}{{blah}}{{/table}}

#5

This is how I understand you situation. You have an array of items, which each have a col property. You want to render a <div class="col"> element for each unique value of col and inside it have some content for each item. To do this elegantly in handlebars, you first need to organize your data structure.

In the controller for your view, you create a computed property that groups your items/rows by the name of the column:

App.ColumnController = Ember.Controller.extend({
  items: [], //Your items (what you called $rows) goes here
  cols: function() {
    var colMap = {}, //We need this map to make sure that we only create one
        cols = []; //This is the array of columns with nested items that we will return
    this.get('items').forEach(function(item) {
      var col = colMap[item.colName];
      if (!col) { //Make sure only to create one `col` object per colName
        col = colMap[item.colName] = {
          items: []
        };
        cols.push(col);
      }
      col.items.push(item);
    });
    return cols;
  }.property('items.@each', 'items.@each.col')
});

Then, in your handlebars template, you iterate the data structure that cols returns by using two each helpers:

{{#each col in cols}}
  <div class="col">
    {{#each item in col.items}}
      Column inner content<br>
    {{/each}}
  </div>
{{/each}}

I hope this helps :slight_smile:

Sebastian


#6

I think this is exactly what I am looking for. I suspected it might be in a helper function, but had no idea. Thanks.


#7

So @Seilund, how would I get the model data into items? I have a Plot model with Cards in it, and I want to make columns of the cards, so does your above code go in the PlotController or the ColumnController? I’ve tried setupController and setting a property like this:

items: function(){
    return: App.Plot.find()
}

#8

A computed property like this would make sense:

App.ColumnController = Ember.Controller.extend({
  items: function() {
    return App.Plot.find();
  }.property(),
  cols: function() {
    //Same code as before
  }.property('items.@each', 'items.@each.col')
});

If that doesn’t help you, can you then send a larger piece of your app code?


#9

Thanks for the reply. I am still doing something not right. So, the context for this is I am on the plots/#/1 route, and the cards were displaying until I tried to add this controller method.

Here is what I’ve got so far:

//  Router
App.Router.map(function(){
    this.resource('plots');
    this.resource('plot',{path: ':plot_id'});
});

// Routes
App.ApplicationRoute = Ember.Route.extend({
setupController: function(){
	this.controllerFor('cards').set('model',App.Card.find());
}
});

App.IndexRoute = Ember.Route.extend({
redirect: function(){
	this.transitionTo('plots');
}
});

App.PlotsRoute = Ember.Route.extend({
model: function(){
	return App.Plot.find();
}
});

App.PlotRoute = Ember.Route.extend({
model: function(params){
	return App.Plot.find(params.plot_id);
}
});

// Controllers
App.PlotController = Ember.ArrayController.extend({
cards: function(){
	return App.Plot.find()
}.property(),
cols: function(){	
	var colMap = {};
	cols = [];
	this.get('cards').forEach(function(card){
		var col = colMap[card.col];
		if(!col){
			col = colMap[card.col] = {
				cards: []
			};
			cols.push(col);
		}
		col,cards.push(card);
	});
	return cols;
}.property('cards.@each','cards.@each.col')
});

// Models
App.Plot = DS.Model.extend({
    title: DS.attr('string'),
    cards: DS.hasMany('App.Card')
});

App.Card = DS.Model.extend({
    content: DS.attr('string'),
    color: DS.attr('string'),
    col: DS.attr('number'),
    row: DS.attr('number')
});

// using standard fixtures...

#10

I’m not really sure what you’re trying to achieve here. But some things do look a little mysterious:

this.controllerFor('cards').set('model',App.Card.find());

You don’t have any App.CardsController, so this shouldn’t have any effect.

cards: function(){
    return App.Plot.find()
}.property()

cards returning App.Plots?

Could you maybe set up a JSBin or JSFiddle?