How to render Marionette 3 CollectionView into a table column? - backbone.js

I'm currently working on a legacy program with symfony 1.4 and a bunch of outdated technologies. Recently we decided to add Backbone/Marionette to project in order to make things a bit easier.
The problem I'm facing is rendering a collection view into a table column. The table already exists and is functional with more than 2k LOC behind it, which makes it impossible for me to rewrite it ATM.
<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
<th>...</th>
<th></th> <!-- This is the column -->
<th>Total price</th>
</tr>
</thead>
<tbody>
...
</tbody>
</table>
All other columns, except the one specified by comment, are already rendered into the page. I've followed the documentations to Marionette attachHtml and attachBuffer but couldn't implement a working solution.

https://jsfiddle.net/cbzs67ar/
var collection = new Backbone.Collection([
{ id: 1, text: 'one' },
{ id: 2, text: 'two' },
{ id: 3, text: 'three' },
{ id: 4, text: 'four' },
{ id: 5, text: 'five' },
{ id: 6, text: 'six' },
{ id: 7, text: 'seven' }
])
var MyChildView = Mn.View.extend({
initialize() {
this.render();
},
template: _.template(': <%- text %>')
});
var CollectionView = Mn.CollectionView.extend({
el: '.tbody-hook',
childView: MyChildView,
buildChildView(model, ChildView) {
var view = new ChildView({
el: '.td-' + model.id,
model: model
});
return view;
},
attachHtml: _.noop,
attachBuffer: _.noop
});
var myRegion = new Marionette.Region({ el: '#some-region' });
myRegion.show(new CollectionView({ collection: collection }));
Use a collectionview to manage the pre-rendered child by a column selector. You'll need to disable the collectionview from attaching the children to it's own container and you'll need to force the children to render themselves after they're instantiated.

Related

creating sub views(table inside another table) using backbone JS

Trying to create a table inside a table with backbone but could not able to find out a way. Can anybody help me out with an example to achieve this?
My Collection:
this.collection = new Backbone.Collection([
{ first: 'John', last: 'Doe', desig: 'E1', location: 'C' },
{ first: 'Mary', last: 'Jane', desig: 'E3', location: 'C' },
{ first: 'Billy', last: 'Bob', desig: 'E2', location: 'C' },
{ first: 'Dexter', last: 'Morgan', desig: 'E1', location: 'P' },
{ first: 'Walter', last: 'White', desig: 'E2', location: 'P' },
{ first: 'Billy', last: 'Bobby', desig: 'E1', location: 'B' }
]);
Normal View: Achieved this using a table view. refer code here
first last desig location
----------------------------------
Billy Bobby E1 B
Walter White E2 P
Dexter Morgan E1 P
Billy Bob E2 C
Marry Jane E3 C
John Doe E1 C
Want to group by location then want to render as new view like below
location first last desig
----------------------------------
C Billy Bob E2
Marry Jane E3
John Doe E1
P Walter White E2
Dexter Morgan E1
B Billy Bobby E1
Using underscore we can do grouping but after that, I am struggling to render that object in the above view
_.groupby(this.collection, "location");
is giving me an object which has the required result.
Every row in the table should be represented in a Backbone.View.
The grouping you want is mostly a rowspan feature of standard HTML tables.
See the snippet:
var collection = new Backbone.Collection([{
first: 'John',
last: 'Doe',
desig: 'E1',
location: 'Chennai'
}, {
first: 'Mary',
last: 'Jane',
desig: 'E3',
location: 'Chennai'
}, {
first: 'Billy',
last: 'Bob',
desig: 'E2',
location: 'Chennai'
}, {
first: 'Dexter',
last: 'Morgan',
desig: 'E1',
location: 'Pune'
}, {
first: 'Walter',
last: 'White',
desig: 'E2',
location: 'Pune'
}, {
first: 'Billy',
last: 'Bobby',
desig: 'E1',
location: 'Bangalore'
}]);
var GroupRowView = Backbone.View.extend({
tagName: 'tr',
initialize: function(options) {
this.groupBy = options.groupBy;
this.index = options.index;
this.total = options.total;
},
render: function() {
this.$el.empty();
if (this.index == 0) {
this.$el.append('<td rowspan="' + this.total + '">' + this.model.get(this.groupBy) + '</td>');
}
_.each(this.model.omit(this.groupBy), function(value, key) {
this.$el.append('<td>' + value + '</td>');
}, this);
return this;
}
});
var SimpleRowView = Backbone.View.extend({
tagName: 'tr',
render: function() {
this.$el.empty();
//this.$el.append('<td>' + this.model.get('location') + '</td>')
_.each(this.model.values(), function(value) {
this.$el.append('<td>' + value + '</td>');
}, this);
return this;
}
})
var TableView = Backbone.View.extend({
tagName: 'table',
render: function() {
/*var self = this;
self.$el.empty();
self.collection.each(function(rowModel) {
self.$el.append(_.template('<tr><td><%= location %></td><td><%= first %></td><td><%= last %></td><td><%= desig %></td></tr>')(rowModel.attributes))
});*/
var self = this;
self.$el.empty();
self.collection.each(function(model) {
var row = new SimpleRowView({
model: model
});
self.$el.append(row.render().el);
});
return this;
},
groupCollection: function() {
var self = this;
var groups = self.collection.groupBy('location');
self.$el.empty();
_.each(groups, function(group) {
var length = group.length;
_.each(group, function(model, i) {
var row = new GroupRowView({
model: model,
groupBy: 'location',
index: i,
total: length
});
self.$el.append(row.render().el);
})
})
}
});
var table = new TableView({
collection: collection
});
$('#table-container').append(table.render().el);
$('#sortBtn').click(function(e) {
table.groupCollection();
})
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
<p>What the table should be:</p>
<table border="1">
<thead>
<tr>
<th>Location</th>
<th>First</th>
<th>Last</th>
<th>Desig</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="3">C</td>
<td>Billy</td>
<td>Bob</td>
<td>E2</td>
</tr>
<tr>
<td>Marry</td>
<td>Jane</td>
<td>E3</td>
</tr>
<tr>
<td>John</td>
<td>Doe</td>
<td>E1</td>
</tr>
<tr>
<td rowspan="2">P</td>
<td>Walter</td>
<td>White</td>
<td>E2</td>
</tr>
<tr>
<td>Dexter</td>
<td>Morgan</td>
<td>E1</td>
</tr>
<tr>
<td rowspan="1">B</td>
<td>Billy</td>
<td>Bobby</td>
<td>E1</td>
</tr>
</tbody>
</table>
<p>What the resulting table is:</p>
<button id="sortBtn">Sort!</button>
<div id="table-container">
</div>
Updated fiddle with 2 different views using user input
jsfiddle.net/WebDev81/ddbf9ckv/10
Let me know anybody has a better approach to achieve this.

How to filter in ngTables

I am using ng-table to generate my table.
but my data has two column, the first one is an object.
My function in controller :
$scope.allServers = function() {
$http.get("/volazi/getServers").success(function(data) {
$scope.serversDTO = data;
$scope.tableParams = new NgTableParams({}, {
dataset: data
});
});
}
So my data will be like:
[{
server {
name: "ser1",
date: "..",
group: {
name: "G1",
created: ".."
}
},
status
}, ...]
how i can use filter in html
<tr ng-repeat="sr in $data">
<td title="'Name'" filter="{server.name: 'text'}" sortable="'server.name'">
{{ sr.server.name }}
</td>
</tr>
Its not working like that
You should apply the filter to the loop:
<tr ng-repeat="sr in $data | filter: { server.name: 'text' }">
I solved th proble by adding ''
i replace
filter="{server.name: 'text'}"
by
filter="{'server.name': 'text'}"
This will be really very helpful :LINK

Using foreign key in ng-repeat

I have two tables:
A jobs table with 3 fields: id, client_id, name.
A clients table with 2 fields: id, name.
Using Angular 1.5, I'm iterating over the jobs:
controller('JobsController', ['$scope', 'Job', 'Client', function($scope, Job, Client) {
$scope.jobs = Job.query();
$scope.clients = Client.query();
}]);
HTML:
<tr ng-repeat="job in jobs">
<td>
{{clients[job.client_id].name}}
</td>
<td>
{{job.name}}
</td>
</tr>
In the HTML the first column should be the client name. As it is, this isn't working, because $scope.clients is an array of objects that look a bit like this:
[{'id':4, 'name':'test_name'}, {'id':7, 'name':'another client'}]
Is there a way to pick from this clients array by id, in my ng-repeat loop?
$scope.jobs looks like:
[{'id':100, 'client_id': 4, 'name': 'a job'}, ...]
To begin with, it may be easier to do the join on the server side, to where your $scope.jobs would look more like:
[
{
'id': 100,
'name': 'a job',
'client': {
'id': 4
'name': 'test_name'
}
}
...
]
If you need to do it on the front end, what I would do is add a method to your controller to get the client for a specified job. Something like this:
$scope.getClientName = function(job) {
//to prevent errors if $scope.clients is not loaded yet
if (!$scope.clients) {
return;
}
for (var c = 0; c < $scope.clients.length; c++) {
var client = $scope.clients[c];
if (client.id = job.client_id) {
return client.name;
}
}
}
Then instead of {{clients[job.client_id].name}} call your function and pass in the job:
{{getClientName(job)}}
You can do it in front end as below
$scope.clients = [{'id':1, 'name':'test_name'}, {'id':4, 'name':'another client'}];
$scope.jobs = [{'id':100, 'client_id': 4, 'name': 'a job4'}, {'id':100, 'client_id': 1, 'name': 'a job1'}, {'id':100, 'client_id': 24, 'name': 'a job24'}];
function fillJobsByClient(jobs, client){
for(var i=0; i<jobs.length; i++){
if(jobs[i].client_id == client.id){
jobs[i].client = client;
delete jobs[i].client_id;
}
}
}
$scope.clients.forEach(function(client){
fillJobsByClient($scope.jobs, client);
});
console.log($scope.jobs);
I know this is late, but I figured it might help point someone else on the right direction. This is how you could do it on the front end, you should load your array OnInit, then use an if statement to select the correct detail field.
<tr *ngFor='let j of jobs'>
<td>
<div *ngFor='let c of clients'>
<div *ngIf='c.id== j.client_id'>{{j.name}}</div>
</div>
</td>
</tr>

Angular object property value change not propagated into view using ng-repeat

I'm trying to generate a table using ng-repeat.
Use case
The data to generate the table from looks as follows:
$scope.data = [
{
name : 'foo1',
group : 1
},
{
name : 'foo2',
group : 1
},
{
name : 'foo3',
group : 1
},
{
name : 'foo4',
group : 1
},
{
name : 'foobar',
group : 2
},
{
name : 'foobarbar',
group : 3
}
];
The html generated should look like this:
<tr>
<th>Group</th>
<th>Name</th>
</tr>
<tr>
<td rowspan="4">1</td>
<td>foo1</td>
</tr>
<tr>
<td>foo2</td>
</tr>
<tr>
<td>foo3</td>
</tr>
<tr>
<td>foo4</td>
</tr>
<tr>
<td rowspan="1">2</td>
<td>foobar</td>
</tr>
<tr>
<td rowspan="1">2</td>
<td>foobarbar</td>
</tr>
Implementation
I know the easiest way would probably be to pre-process the data and group the items per group in a new array of arrays. However, I chose a different approach:
<td
ng-if = "isDifferentFromPrev(items, $index, groupingData)"
rowspan = "{{item._groupSize}}"
>
with
$scope.isDifferentFromPrev = function(array, index, groupingData){
if(index === 0){
groupingData.startI = 0;
groupingData.counter = 1;
array[0]._groupSize = 1;
return true;
}
var eq = equalsMethod(array[index], array[index-1]);
if(eq){
groupingData.counter++;
array[groupingData.startI]._groupSize = groupingData.counter;
}
else{
groupingData.startI = index;
groupingData.counter = 1;
array[index]._groupSize = 1;
}
return !eq;
};
Problem
For some reason the rendered value for rowspan is always 1.
The attribute is only set for the first td of the first tr of a group, as intended, but the value for it is 1.
If I put a breakpoint inside isDifferentFromPrev(), the values seem to be updated correctly. This does not reflect in the html though.
Solution?
It occured to me that maybe ng-repeat renders each step sequentially, without returning to it. So maybe the _groupSize values for the first item of each group do get properly updated, but since they are updated after that item has already been rendered by ng-repeat, the update isn't processed anymore.
I have no idea if this reasoning is correct, nor about how to solve it. Any suggestions please?
This solution, even if a bit orthodox, does work:
var app = angular.module("myApp", []);
app.controller("myController", function($scope) {
$scope.data = [{
name: 'foo1',
group: 1
}, {
name: 'foo2',
group: 1
}, {
name: 'foo3',
group: 1
}, {
name: 'foo4',
group: 1
}, {
name: 'foobar',
group: 2
}, {
name: 'foobarbar',
group: 3
}];
$scope.itemHasRowspan = function(item) {
return typeof item === "object" && item.hasOwnProperty("rowspan");
};
var groupData = {},
currentGroup = null,
addGroup = function(firstItem) {
currentGroup = firstItem.group;
groupData[firstItem.group] = {
"firstItem": firstItem,
"count": 1
};
};
angular.forEach($scope.data, function(item, index) {
if (item.group !== currentGroup) {
addGroup(item);
} else {
groupData[item.group].count++;
}
});
angular.forEach(groupData, function(group, index) {
group.firstItem["rowspan"] = group.count;
});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="myController">
<table>
<thead>
<tr>
<th>Group</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in data">
<td ng-if="itemHasRowspan(item)" rowspan="{{ item.rowspan }}" valign="top">
{{ item.group }}
</td>
<td>
{{ item.name }}
</td>
</tr>
</tbody>
</table>
</div>
</div>

ng-repeat a single element over nested objects

Say I have an object with keys corresponding to products and values corresponding to objects which in turn have keys corresponding to price points at which those products have sold, and values corresponding to amount sold.
For example, if I sold 10 widgets at $1 and 5 widgets at $2, I'd have the data structure:
{ 'widget': {'1': 10, '2': 5} }
I'd like to loop over this structure and generate rows in a table such as this one:
thing price amount
---------------------
widget $1 10
widget $2 5
In Python it's possible to nest list comprehensions to traverse lists data structures like this. Would such a thing be possible using ng-repeat?
How about this?
http://plnkr.co/edit/ZFgu8Q?p=preview
Controller:
$scope.data = {
'widget1': {
'1': 10,
'2': 5
},
'widget2': {
'4': 7,
'6': 6
}
};
View:
<div ng-controller="MyCtrl">
<table>
<thead>
<tr>
<td>thing</td>
<td>price</td>
<td>amount</td>
</tr>
</thead>
<tbody ng-repeat="(productName, productData) in data">
<tr ng-repeat="(price, count) in productData">
<td>{{productName}}</td>
<td>{{price|currency}}</td>
<td>{{count}}</td>
</tr>
</tbody>
</table>
</div>
Output:
thing price amount
----------------------
widget1 $1.00 10
widget1 $2.00 5
widget2 $4.00 7
widget2 $6.00 6
This would output a tbody per product (thanks to Sebastien C for the great idea).
If needed, you can differentiate between the first, middle and last tbody (using ng-repeat's $first, $middle and $last) and style them with ng-class (or even native CSS selectors such as :last-child -- I would recommend ng-class though)
ng-repeat does not currently have a possible way to complex iterate inside objects (the way it's possible in python). Check out the ng-repeat source code and note that the regex expression matched is:
(key, value) in collection - and that they push into the key array and assign to the value list, and so you cannot possibly have a complex ng-repeat sadly...
There are basically 2 types of solutions which were already answered here:
Nested ng-repeat like the first answer suggested.
Rebuilding your data object to fit 1 ng-repeat like the second answer suggested.
I think solution 2 is better as I like to keep my sorting & coding logic inside the controller, and not deal with it in the HTML document. This will also allow for more complex sorting (i.e based on price, amount, widgetName or some other logic).
Another thing - the second solution will iterate over possible methods of a dataset (as hasOwnProperty wasn't used there).
I've improved the solution in this Plunker (based on the finishingmove Plunker) in order to use angular.forEach and to show that the solution is rather simple but allows for complex sorting logic.
$scope.buildData = function() {
var returnArr = [];
angular.forEach($scope.data, function(productData, widget) {
angular.forEach(productData, function( amount, price) {
returnArr.push( {widgetName: widget, price:price, amount:amount});
});
});
//apply sorting logic here
return returnArr;
};
$scope.sortedData = $scope.buildData();
and then in your controller:
<div ng-controller="MyCtrl">
<table>
<thead>
<tr>
<td>thing</td>
<td>price</td>
<td>amount</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in sortedData">
<td>{{ item.widgetName }}</td>
<td>{{ item.price|currency }}</td>
<td>{{ item.amount }} </td>
</tr>
</tbody>
</table>
</div>
Just transform your object to an array... it's pretty easy in JS.
Something like:
$scope.data = { 'widget': { '1': 10, '2': 5 } };
var tableData = [];
for (item in $scope.data) {
var thing = item;
for (subitem in $scope.data[thing]) {
tableData.push({
thing: thing,
price: subitem,
amount: $scope.data[thing][subitem]
});
}
}
I've created a jsfiddle with this example: http://jsfiddle.net/b7TYf/
I used a simple directive which has a recursive function to loop over my nested object and create nested elements. This way you can keep your nested object structure.
Code:
angular.module('nerd').directive('nestedItems', ['$rootScope', '$compile', function($rootScope, $compile) {
return {
restrict: 'E',
scope: false,
link: function(scope, element, attrs, fn) {
scope.addElement = function(elem, objAr) {
var ulElement = angular.element("<ul></ul>");
if (objAr == undefined || objAr.length == 0) {
return [];
}
objAr.forEach(function(arrayItem) {
var newElement = angular.element("<li>"+arrayItem.val+"</li>");
ulElement.append(newElement);
scope.addElement(newElement,arrayItem.sub);
});
elem.append(ulElement);
};
scope.addElement(element,scope.elements);
}
};
}]);
My Object :
$scope.elements = [
{
id: 1,
val: "First Level",
sub: [
{
id: 2,
val: "Second Level - Item 1"
},
{
id: 3,
val: "Second Level - Item 2",
sub: [{
id: 4,
val: "Third Level - Item 1"
}]
}
]
}
];
My HTML
<nested-items></nested-items>

Resources