I just got started on using backbone.js. I have a view ListingListView that refreshes a table with new content when fetch() is called.
Problem: This table contains some <th> elements. If I were to do a $(this.el).empty(); and this.render() during the update of the table contents, the <th> elements will be removed. How can I prevent this? I want the <th> elements to remain. Thanks!
JS Code
// Views
window.ListingListView = Backbone.View.extend({
el: '#listing_list table',
initialize: function() {
this.model.bind('reset', this.refreshList, this);
this.model.bind('add', function(listing) {
$(this.el).append(new ListingListItemView({ model: listing }).render().el);
}, this);
},
render: function() {
_.each(this.model.models, function(listing) {
$(this.el).append(new ListingListItemView({ model: listing }).render().el);
}, this);
return this;
},
close: function() {
$(this.el).unbind();
$(this.el).empty();
},
refreshList: function() {
$(this.el).empty();
this.render();
}
});
HTML Code
<div id="listing_list">
<table class="table table-bordered table table-striped">
<th>Address</th>
<th>Beds</th>
<th>Baths</th>
<th>Price</th>
</table>
</div>
You could add some structure to your table, using thead and tbody:
<div id="listing_list">
<table class="table table-bordered table table-striped">
<thead>
<tr>
<th>Address</th>
<th>Beds</th>
<th>Baths</th>
<th>Price</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
And target the tbody in your render and refreshList functions:
render: function() {
var $tbody=this.$("tbody"); // or $(this.el).find("tbody")
_.each(this.model.models, function(listing) {
$tbody.append(new ListingListItemView({ model: listing }).render().el);
}, this);
return this;
},
refreshList: function() {
this.$("tbody").empty();
// or $(this.el).find("tbody").empty() if you prefer
this.render();
}
Notes:
don't forget you can use a collection as a special option instead of a model : http://backbonejs.org/#View-constructor It could be a bit clearer in the end.
Backbone proxies Underscore functions on collections, _.each(this.model.models... can be written as this.model.each (this.collection.each if you apply the note above)
Related
I have a huge project in C# MVC. In one of the views, in index.cshtml, I have a code like this:
#model IEnumerable<Library.Models.Customer>
#{
ViewBag.Title = "Customers";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Klienci</h2>
<p>
#Html.ActionLink("Nowy klient", "New", "Customers", null, new { #class = "btn btn-primary" })
</p>
<table id ="customers" class="table table-bordered table-hover">
<thead>
<tr>
<th>Klient</th>
<th>Typ Czlonkostwa</th>
<th>Usun</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
#section scripts
{
<script>
$(document).ready(function () {
var table = $("#customers").DataTable({
ajax: {
url: "/api/customers",
dataSrc: ""
},
columns: [
{
data: "Name",
render: function(data, type, customer) {
return "<a href='/customers/edit/" + customer.Id + "'>" + customer.Name + "</a>";
}
},
{
data: "MembershipType.Name"
},
{
data: "Id",
render: function(data) {
return "<button class='btn-link js-delete' data-customer-id=" + data + ">Delete</button>";
}
}
]
});
$("#customers").on("click", ".js-delete",
function () {
var button = $(this);
if (confirm("Na pewno chcesz usunac?")) {
$.ajax({
url: "/api/customers/" + button.attr("data-customer-id"),
method: "DELETE",
success: function () {
//datatable methods - row, remove and draw
table.row(button.parents("tr")).remove().draw();
}
});
}
});
});
</script>
}
It works like a charm. But in another view, I have the code like this:
<table id="customers" class="table table-bordered table-hover">
<thead>
<tr>
<th>Ksiazka</th>
<th>Gatunek</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
#foreach (var book in Model)
{
<tr>
<td>#Html.ActionLink(book.Name, "Edit", "Books", new { id = book.Id }, null)</td>
<td>#book.Genre.Name</td>
<td>
<button class="btn-link" js-delete>Delete</button>
</td>
</tr>
}
</tbody>
#section scripts
{
<script>
$(document).ready(function() {
$("#customers .js-delete").on("click",
function() {
confirm("Sure?");
});
});
</script>
}
and it doesn't work. I mean, it compiles without any errors or warnings, but when I click Delete button nothing happens (should pop up confirm box).
What am I doing wrong?
If needed, I can provide whole code from these both views.
.js-delete means that "js-delete" is assumed to be a css class, while in the html it is an attribute. To search for elements with a particular attribute, you need "has attribute" selector:
$("#customers [js-delete]")
When i trigger a click on a cell with backbone event im not getting the cell view, im getting the view of the row.
I'm sending the models and the week atributes to my UserView.
So what i want is that every cell is a unique backbone view. Can anyone help me?
Backbone view
app.types.UserView = Backbone.View.extend({
tagName: 'tr',
$sidebar: $('#userView'),
template: _.template($('#user-template').html()),
events:
{
//"click .test": "open",
"click td.test": "useri",
},
initialize: function(options)
{
this.options = options;
console.log("this.options", this.options.object.tjedan);
console.log("this.model: ", this.model);
this.$sidebar.append(this.render());
this.tableClick();
//this.open();
//this.getWeeksInMonth();
//console.log("this.tjedan: ", this.tjedan);
},
render: function()
{
this.$el.html(this.template(_.extend(this.model.attributes, {model_cid: this.model.cid, tjedan: this.options.object.tjedan})));
console.log("this.render: ", this);
return this.$el;
},
useri: function()
{
console.log("this.model", this);
}
});
HTML
<div class="container-fluid">
<div class="row">
<div id="collection" class="col-md-12 sidebar">
<table class="table">
<thead id="weekView">
<script type="text/template" id="week-template">
<th>Users</th>
<% for(var i=0;i<tjedan.length;i++)%> {
<th class="list-group week" scope="row"><%= tjedan[i].dan + " " + tjedan[i].datum + "." + tjedan[i].mjesecBrojevi + "." %></th>
}%>
</script>
</thead>
<tbody id="userView">
</tbody>
<script type="text/template" id="user-template">
<th class="list-group model" scope="row" data-cid="<%= model_cid %>"><%= username %></th>
<% for(var i=0;i<tjedan.length;i++)%> {
<td id="<%= id + tjedan[i].dan + tjedan[i].datum %>" class="list-group" scope="row" data-id="<%= model_cid + '_' + tjedan[i].dan + tjedan[i].datum + tjedan[i].mjesecBrojevi %>"></td>
}%>
</script>
</table>
</div>
</div>
</div>
You need to create a view which will serve as the cell view and render each cell creating a new view.
Cell view
var CellView = Backbone.View.extend({
tagName: 'td',
className: 'list-group',
template: _.template('stuff inside your `td`'),
attributes: function() {
return {
id: this.model.get('your_choice'),
scope: 'row',
"data-id": this.whatever
};
},
events: {
"click": "onClick",
},
render: function() {
this.$el.empty().append(this.template(this.model.toJSON()));
return this;
},
onClick: function(e) {
// click on cell
}
});
Row view
var RowView = Backbone.View.extend({
tagName: 'tr',
template: _.template('<th class="list-group model" scope="row" data-cid="<%= model_cid %>"><%= username %></th>'),
initialize: function(options) {
this.options = _.extend({ /* default options */ }, options);
this.childViews = [];
},
render: function() {
// when using extend, the first object is modified, `toJSON` returns
// a shallow copy of the attributes, avoiding modifying them.
var data = _.extend(this.model.toJSON(), {
model_cid: this.model.cid,
tjedan: this.options.object.tjedan
});
this.$el.empty().append(this.template(data));
this.collection.each(this.renderCell, this);
return this; // render always return this for chaining.
},
renderCell: function(model) {
var view = new CellView({
model: model
});
this.childViews.push(view);
this.$el.append(view.render().el);
},
remove: function() {
// explicitely remove the views to avoid memory leaks with listeners.
_.invoke(this.childViews, 'remove');
}
});
This is just an example of how to render a view per cell, it doesn't properly take into account your models and every attributes in the templates.
I've been struggling to make an interactive form, in which a viewmodel has a collection of items. I want to dynamically add/remove items from that collection.
I've found it difficult to find examples that go to this depth and most of them usually stay on a more straight forward implementation, however I've come across This post which pretty much explains what i'm doing with this brilliant jsfiddle, in which a json is pulled using the knockout mapping pluggin and then mapped.
var company;
function PersonViewModel(data) {
var personMapping = {
'ignore': ['twitter', 'webpage'],
'copy': ['age'],
'lastName': {
'create': function (options) {
return ko.observable(options.data.toUpperCase());
}
}
};
ko.mapping.fromJS(data, personMapping, this);
this.fullName = ko.computed(function () {
return this.firstName() + ' ' + this.lastName();
}, this);
}
function CompanyViewModel(data) {
var companyMapping = {
'ignore': ['address', 'website'],
'name': {
'create': function (options) {
return ko.observable(options.data.toUpperCase());
}
},
'employees': {
key: function (data) {
return ko.utils.unwrapObservable(data.personId);
},
create: function (options) {
return new PersonViewModel(options.data);
}
}
};
ko.mapping.fromJS(data, companyMapping, this);
}
What i don't know how to achieve is how and where exactly to add the 'addEmployee' and 'removeEmployee' functions? and how to bind them to a button?.
Thank you in advance!
The logical place to add these would be to your CompanyViewModel. For example, something like this:
function CompanyViewModel(data) {
var self = this;
var companyMapping = {
// ...as before
};
self.addEmployee = function () {
// as an example, we are just adding a static new employee
self.employees.push(new PersonViewModel({
lastName: "new",
firstName: "employee",
age: 10
}));
}
// important, with how we are binding the function, we expect the
// argument, e, to be the employee to remove
self.removeEmployee = function (e) {
self.employees.remove(e);
}
ko.mapping.fromJS(data, companyMapping, this);
}
Add to bind, you can do something like this:
<div id="company">
<h1 data-bind="text: name"></h1>
<h2>Employees</h2>
<input type="button" value="add" data-bind="click: addEmployee" />
<table>
<thead>
<tr>
<th>Full name</th>
<th>Last name</th>
<th>First name</th>
<th>Age</th>
</tr>
</thead>
<tbody data-bind="foreach: employees">
<tr>
<td data-bind="text: fullName"></td>
<td data-bind="text: lastName"></td>
<td data-bind="text: firstName"></td>
<td data-bind="text: age"></td>
<td>
<input type="button" value="x" data-bind="click: $parent.removeEmployee" />
</td>
</tr>
</tbody>
</table>
</div>
Which will add an add button as well as a remove x button to each employee which calls the removeEmployee function on the parent CompanyViewModel passing in the current employee.
Here's an updated fiddle
You would add those functions to your CompanyViewModel.
CompanyViewModel
...
this.addEmployee = function () {
this.employees.push(new PersonViewModel({
firstName: 'New',
lastName: 'Employee',
age: 777,
}));
};
this.removeEmployee = function () {
this.employees.pop();
};
HTML
....
<div data-bind="click: addEmployee">Add Employee</div>
<div data-bind="click: removeEmployee">Remove Employee</div>
...
http://jsfiddle.net/HBKYP/198/
I looping through a collection in an attempt to add a row to a table on every loop. Here is the code that loops the collection, and build the single view,
App.Views.OrganisationMembersTab = Backbone.View.extend({
el: '#members',
template: _.template( $('#tpl-members-tab-panel').html() ),
events: {
},
initialize: function() {
this.$el.html( this.template() );
this.render();
},
render: function() {
this.addAll();
},
addAll: function() {
this.collection.each( this.addOne, this);
},
addOne: function(model) {
console.log(model);
var tableRow = new App.Views.OrganisationsMemberRow({
model: model
});
tableRow.render();
}
});
The single view that gets called to build the row looks like this,
App.Views.OrganisationsMemberRow = Backbone.View.extend({
el: '.members-list tbody',
template: _.template($('#tpl-organisation-member-row').html() ),
events: {
},
initialize: function() {
},
render: function() {
this.$el.prepend( this.template({
member: this.model.toJSON()
}));
return this;
}
});
The model that is being used once it has been parsed to JSON using toJSON() looks like this,
email: "john.doe#email.com"
first_name: "John"
last_name: "Doe"
The template for the row looks like this,
<script type="text/template" id="tpl-members-tab-panel">
<table class="table table-striped members-list">
<thead>
<tr>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4"><button class="btn btn-success btn-sm pull-right">Add +</button></td>
</tr>
</tbody>
</table>
</script>
The above builds the main table components, and the next template is actually for a data row.
<script type="text/template" id="tpl-organisation-member-row">
<tr>
<td>#</td>
<td><%= first_name %> <%= last_name %></td>
<td>Admin <input type="checkbox" /></td>
<td>Remove</td>
</tr>
</script>
All I get output the the main table and then in the main tbody I get either nothing prepended or an empty <tr> why is this?
The problem is with your template which doesn't use member property, just use whole model instead.
You need replace
this.$el.prepend( this.template({
member: this.model.toJSON()
}));
with
this.$el.prepend( this.template(
this.model.toJSON()
));
working example
Your current implementation is a little confused. Your row view has no tagName so by default you'll be appending divs to your tbody.
The first thing I'd do is take the <tr> tag out of your tpl-organisation-member-row template and then alter your row view like so:
App.Views.OrganisationsMemberRow = Backbone.View.extend({
tagName: 'tr',
template: _.template($('#tpl-organisation-member-row').html() ),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
Member row template:
<script type="text/template" id="tpl-organisation-member-row">
<td>#</td>
<td><%= first_name %> <%= last_name %></td>
<td>Admin <input type="checkbox" /></td>
<td>Remove</td>
</script>
Then I'd prefer to control appending the rows from your App.Views.OrganisationMembersTab view. So in your addOne method do the following:
addOne: function(){
var tableRow = new App.Views.OrganisationsMemberRow({
model: model
});
this.$('tbody').append(tableRow.render().el);
}
I want to render collection in handlebars precompiled templates,
this.courses.fetch() is working
handlebars view is rendering correctly...(only each loop not rendering collection data)
here is my code...
router.js
App.Router = Backbone.Router.extend({
routes: {
'master/courses' : 'courses'
},
initialize: function(){
this.courses = new App.Collections.Courses();
this.list = new App.Views.List( {collection: this.courses} );
},
courses: function() {
$('#page').html(this.list.render().el);
}
});
var app = new App.Router();
Backbone.history.start();
courses.js
App.Collections.Courses = Backbone.Collection.extend({
model: App.Models.Course,
url: '/api/courses'
});
list.js
p.Views.List = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
},
render:function(){
this.$el.html( Handlebars.templates.list(this.collection) );
return this;
}
});
list.handlebars
<h1>Courses</h1>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
{{#each models}}
<td>{{attributes.id}}</td>
<td>{{attributes.name}}</td>
{{/each}}
</tr>
</tbody>
</table>
this is my first attempt on backbone project kindly suggest good practice also
I don't see here the point where collection fetching is fired. The first line in your courses probably should be this.collection.fetch({ reset: true });
As I see in provided HTML structure, each should be before tr tag. Like this:
<h1>Courses</h1>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{{#each models}}
<tr>
<td>{{attributes.id}}</td>
<td>{{attributes.name}}</td>
</tr>
{{/each}}
</tbody>
</table>