ItemView events not Triggering - backbone.js

What i want to do:
Render a select dropdown with option tags inside, and when user selects an option in the dropdown, get the newly selected model and do stuff with it.
Problem:
I'm having a hard time to get the change event to be triggered in an ItemView that's been called through a CompositeView.
For some reason the CompositeView:change (log: holy moses) is being triggered, however it doesn't help me much, since it won't give me the selected model.
I've tried a ton of stuff but nothing really worked.
any help would be greatly appreciated!
code:
Configurator.module('Views.Ringsizes', function(Views, Configurator, Backbone, Marionette, $, _) {
Views.DropdownItem = Marionette.ItemView.extend({
tagName: 'option',
template: "#dropdown-item",
modelEvents: {
'change': 'modelChanged'
},
onRender: function(){
console.log('tnt');
this.$el = this.$el.children();
this.setElement(this.$el);
},
modelChanged: function(model) {
console.log("holy mary");
}
});
Views.DropdownView = Marionette.CompositeView.extend({
template: "#dropdown-collection",
className: 'configurator-ringsizes-chooser',
itemView: Views.DropdownItem,
itemViewContainer: '.product_detail_ring_sizes',
events: {
"change": "modelChanged"
},
initialEvents: function(){},
initialize: function(){
console.log(this.model);
this.collection = new Backbone.Collection(this.model.getRingsizes());
},
modelChanged: function(model) {
console.log("holy moses");
}
});
Views.List = Marionette.CollectionView.extend({
className: 'configurator-ringsizes',
itemView: Views.DropdownView
});
});
template code: (if needed)
<script type="text/template" id="dropdown-item">
<option value="<#- code #>" <# if(current) { #> selected="selected" <#}#> ><#- name #> </option>
</script>
<script type="text/template" id="dropdown-collection">
<div class="accordionContent accordionContent_ringsizes">
<div class="configurator-ringsizes-chooser-ringsizes-region">
<select class="product_detail_ring_sizes"></select>
</div>
</div>
</script>

A "change" event won't trigger on a option when you select it, instead it will fire on the select when you change the choosen option (that's why it triggers on the composite view).
So you should use this in your itemView:
events: {
'click' : 'modelChanged'
}

Okay, i finally got this to work.
I'm a bit dissapointed that i have to rely on a data- attribute for this,
but this is the only way i found. took me long enough already :)
Here's how i did it now:
Template code:
<script type="text/template" id="dropdown-item">
<option data-cid="<#- cid #>" value="<#- code #>" <# if(current) { #> selected="selected" <#}#> ><#- name #></option>
</script>
<script type="text/template" id="dropdown-collection">
<div class="configurator-ringsizes-chooser-ringsizes-region">
<select class="product_detail_ring_sizes"></select>
</div>
</script>
Code:
Configurator.module('Views.Ringsizes', function(Views, Configurator, Backbone, Marionette, $, _) {
Views.DropdownItem = Marionette.ItemView.extend({
tagName: 'option',
template: "#dropdown-item",
serializeData: function() {
var data = {
cid: this.model.cid,
code: this.model.get('code'),
name: this.model.get('name'),
current: this.model.get('current')
};
return data;
},
onRender: function(){
this.$el = this.$el.children();
this.setElement(this.$el);
}
});
Views.DropdownView = Marionette.CompositeView.extend({
template: "#dropdown-collection",
className: 'configurator-ringsizes-chooser',
itemView: Views.DropdownItem,
itemViewContainer: '.product_detail_ring_sizes',
events: {
"change select": "modelChanged"
},
initialEvents: function(){},
initialize: function(){
this.collection = new Backbone.Collection(this.model.getRingsizes());
},
modelChanged: function(e) {
var cid = $(e.currentTarget+"option:selected").data('cid');
var currentModel = this.collection.find(function(elem) {
return elem.get('current');
});
var model = this.collection.find(function(elem) {
return elem.cid === cid;
});
currentModel.set({
current: false
});
model.set({
current: true
});
// AND here i'm doing my stuff, getting the overall model through this.model, the collection of options through this.collection and the currently selected model through currentModel.
}
});
Views.List = Marionette.CollectionView.extend({
className: 'configurator-ringsizes',
itemView: Views.DropdownView,
model: this.model
});
});

Related

backbone-froms multiple views submit a form

I have a master view App.MainView (table view), a App.TaskView (row view) and using backbone forms to add and edit the records. Here is my code:
App.MainView = Backbone.View.extend({
template: $("#home-template").html(),
initialize: function() {
_.bindAll(this, 'render');
this.listenTo(tasks, 'add change', this.addOne);
this.subViews = [];
},
addOne: function(model) {
var view = new App.TaskView({model: model});
this.$('tbody').append(view.render().el);
},
render: function() {
var template = _.template(this.template);
this.$el.append(template);
//this.$el.find("#filter").append(this.createSelect());
var self = this;
this.collection.fetch({
wait: true,
success: function(model, response) {
var TasksLen = model.length;
for (var i = 0; i < TasksLen; i++) {
var taskView = new App.TaskView({
model: model.models[i]
});
//self.subViews.push(taskView);
$('#record-list-table', this.el).append(taskView.render().el);
}
}
});
},
Now my TaskView:
App.TaskView = Backbone.View.extend({
tagName: 'tr',
template: _.template($('#record-template').html()),
initialize: function() {
var self = this;
},
events: {
"click .edit": "editrecord",
},
render: function() {
this.$el.html(this.template(this.model.toJSON()))
return this;
},
editrecord: function() {
form.setValue(this.model.toJSON());
}
and my Form and submit button
var form = new Backbone.Form({
model: task
});
window.form = form;
$('#form').html(form.render().el);
$('#submit-btn').click(function() {
var data = form.getValue();
form.commit();
task.save(data, {wait: true,
success: function(task, response) {
tasks.add(task);
}
});
Templates:
<script type="text/template" id="home-template">
<table id="recordtable">
<input type='reset' id="reset-btn" onclick="reset()" class="add-new" value='' name='reset' /></table></script>
<script type="text/template" id="record-template">
<td id="edit-name" class="edit"> <%- name %></td>
<td class="edit date" class="edit"> <%- due_date %></td>
</script>
I have two issues:
On model change,it adds another model (However it does the put request and updates in database but on front end backbone adds a new row in the table instead of updating the previous row). But after page refresh it shows correct data. Looks like some issue with change event.
I want to create direct route to a task i.e task/id and want to highlight that task in the table and edit it in the form. Right Now I am able to edit any model on click in the form but I want to have direct route too.
I think now I found the problem of your code. You are doing
tasks.add(task);
in success method of model save. But I think you need to re render the task.
1. You can bind a change event in taskview so that it will render task with updated value.
2. You can add a function in your taskview like this
refresh: function(){
this.render();
}
and you can call this function after model.save success found.

Proper way to create a collection list view in Backbone

I'm currently learning Backbone.js and I'm having a hard time learning how to properly use Views (since I have experienced when it comes to MVC), so here is what I'm trying to do:
templates:
<script type="text/template" id="todolist-template">
<ul></ul>
</script>
<script type="text/template" id="todo-template">
<li>
<%= item.name %>
<%= item.description %>
<%= item.priority %>
</li>
</script>
html:
<div id="container"></div>
Views:
var TodoView = Backbone.View.extend({
tagName: 'li',
className: 'todo',
initialize: function() {
this.template = _.template($('#todo-template').html());
this.render();
},
render: function() {
this.$el.html(this.template({item: this.model}));
return this;
}
});
var TodoListView = Backbone.View.extend({
el: '#container',
tagName: 'ul',
className: 'todolist',
initialize: function() {
this.template = _.template($('#todolist-template').html());
this.render();
},
render: function() {
that = this;
this.$el.empty();
this.$el.append(this.template());
this.collection.each(function(model) {
that.$el.append(new TodoView({model: model.toJSON()}));
});
return this;
}
});
Models and Collections:
var Todo = Backbone.Model.extend({
defaults : {
name : '',
priority: '',
description: ''
}
});
var TodoList = Backbone.Collection.extend({
model: Todo
});
var todoList = new app.TodoList([
new Todo({
name: 'unclog the sink',
priority: '10',
description: 'FIX THE SINK!!!'
}),
new Todo({
name: 'get bread',
priority: '0',
description: 'We are out of bread, go get some'
}),
new Todo({
name: 'get milk',
priority: '2',
description: 'We are out of milk, go get some'
})
]);
"misc":
$(function() {
new HeaderView();
new TodoListView({collection: todoList});
router = new AppRouter();
Backbone.history.start();
});
What I'm trying to do is to create a ul which will then get populated with lis that contain the collection's data. I've been trying to fix/debug this code for a while now (at least 3 hours) but I'm constantly hitting errors or wrong results, so please someone explain to me the proper way of implementing this.
edit (resulting HTML):
<div id="container">
<ul></ul>
</div>
At least one problem lies here:
that.$el.append(new TodoView({model: model.toJSON()}));
Should be
that.$el.append(new TodoView({model: model.toJSON()}).render().el);
Since you can't append a view to $el, but rather you should be appending the rendered html
You don't need <li> in your template as your view already wraps the template in those tags. If it still doesn't work, check the DOM and post it here. Same goes for <ul>...
Also, I don't see where you add your ListView to the DOM. render only operates on a local element which isn't part of the DOM yet. Once rendered, you have to add it to the DOM.

Nested views in Backbonejs, a post & comments relation

Array Of Objects
The data is received from server
var Updates = [
{"post_id":"1","post_desc":"This is my first post",
"comments":[{"id":1,"comment":"some comments","like":7},
{"id":9,"comment":"some comments","like":3}
]
},
{"post_id":"2","post_desc":"This is my second post",
"comments":[{"id":5,"comment":"some comments","like":5}]
}]
Model:
var Update = Backbone.Model.extend({
defaults:{
photo: "default.png"
}
});
Collection:
var latestUpdates = Backbone.Collection.extend({
model: Update
});
Single View:
var UpdateView = Backbone.View.extend({
tagName: "div",
className: "post-container",
template: $("#postTemplate").html(),
render: function () {
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
}
});
Master view:
var UpdatesView = Backbone.View.extend({
el: $("#postContainer"),
initialize: function () {
this.collection = new latestUpdates(Updates);
this.render();
},
render: function () {
var that = this;
_.each(this.collection.models, function (item) {
that.renderUpdates(item);
}, this);
},
renderUpdates: function (item) {
var updateView = new UpdateView({
model: item
});
this.$el.append(updateView.render().el);
}
});
//create app instance
var wallUpdates = new UpdatesView();
How can I render comments section under each post?
Trying to achieve layout similar to facebook post-comment system
I'd use a CommentListView, owned by your UpdateView. tagName: "ul", className: "post-comments"
Then have a CommentView owned by the CommentListView. CommentView's render should not append anything to the DOM, but return its $el.
CommentListView would tell each of the CommentView's to render, appending each of their $el's to the CommentListView's $el.
For the containers, I'd use:
<div class="post-container" data-post-id="<%= YourPostId %>">
<div class="post-body">
<!--Your post can go in here-->
</div>
<ul class="post-comments">
<!--Append your comments in here-->
</ul>
</div>

Event triggered in all instances of Backbone.View

I'm displaying a table of categories with Backbone. I created two views:
RowView (containing a single tr)
TableView (containing table structure)
The definitions:
RowView = Backbone.View.extend({
el: "#content table tbody",
initialize: function() {
this.render();
},
render: function(){
var params = { name: this.model.get('name'), route: this.options.route };
var template = _.template( $("#rowTemplate").html(), params);
this.$el.append(template);
},
events: {
"click #name": "clickHandler"
},
clickHandler: function( event ) {
console.log('Browse subcategories of ' + this.model.get('name'));
}
});
TableView = Backbone.View.extend({
el: "#content",
initialize: function(){
this.render();
},
render: function(){
var row = new this.collection();
var that = this;
row.fetch({
success: function() {
console.log('Collection fetch succeeded');
var params = { title: that.options.title,
counter: row.at(0).get('counter'),
route: that.options.route
};
var template = _.template( $("#tableTemplate").html(), params);
that.$el.html( template );
// RowView's are created by iteration here
for(var x = 1; x < row.length; x++) {
var params = { model: row.at(x), route: that.options.route };
var view = new RowView(params);
}
}
});
}
});
As you can see, I've attached a click event at the RowView.
RowView template:
<script type="text/template" id="rowTemplate">
<tr>
<td id="name" class="fill"><%= name %></td>
<td>Editar</td>
</tr>
</script>
Clicking any #name triggers the handler in all instance of the view. So when clicking one category I get:
Browse subcategories of category1 127.0.0.1:68
Browse subcategories of category2 127.0.0.1:68
etc...
As far as I know, that's because all RowView's are delegated to the same el.
The first thing I though about was adding the category name to the rowTemplate and compare the value in the DOM with the value in the view to see which one actually triggers the event.
But that solutions look really ugly. What's the correct way of accomplishing this in Backbone?
EXTRA: Is it considered better if I only create one view, and iterate in the template to generate the rows?
EDIT: I think the provided code is enough. Otherwise I can add them.
you can modify RowView like this :
RowView = Backbone.View.extend({
container: '#content table tbody',
tagName: 'tr',
initialize: function() {
this.render();
},
render: function() {
var params = {
name: this.model.get('name'),
route: this.options.route
};
var template = _.template($("#rowTemplate").html(), params);
this.$el.html(template).appendTo(this.container);
},
events: {
"click .fill": "clickHandler"
},
clickHandler: function(event) {
console.log('Browse subcategories of ' + this.model.get('name'));
}
});
and RowView template:
<script type="text/template" id="rowTemplate">
<td class="fill"><%= name %></td>
<td>Editar</td>
</script>
Backbone.js will create a tr element. then this.$el.html(template).appendTo(this.container) fill the tr element with template and append to #content table tbody.
just like that, RowView's events be delegated on RowView's el, not #content table tbody.
You have more than one element with the same id on your page, due to all of your rows having the
<td id="name" class="fill"> element.
Element IDs should be unique within your document.
One solution would be to distinguish the rows in your template, and use events as a function to set the proper ID.
Template:
<script type="text/template" id="rowTemplate">
<tr>
<td id="name-<%= name %>" class="fill"><%= name %></td>
<td>Editar</td>
</tr>
Events function:
events: function(){
_events = {};
_events["click #name-" + this.model.get('name')] = "clickHandler";
return _events;
}
Try this
RowView = Backbone.View.extend({
container: '#content table tbody',
tagName: 'tr',
// initialize: function() {
// this.render();
// },
render: function() {
var params = {
name: this.model.get('name'),
route: this.options.route
};
var template = _.template($("#rowTemplate").html(), params);
this.$el.append(this.template);
},
events: {
"click .name": "clickHandler"
},
clickHandler: function(event) {
console.log('Browse subcategories of ' + this.model.get('name'));
}
});
RowView template (no need for identifying each row view):
<script type="text/template" id="rowTemplate">
<td class="name"><%= name %></td>
<td>Editar</td>
</script>
Then the table view:
...
that.$el.html( template );
// RowView's are created by iteration here
for(var x = 1; x < row.length; x++) {
var params = { model: row.at(x), route: that.options.route };
var view = new RowView(params);
that.$el.find('tbody').append(view.el);
view.render()
}
...

Event does not bind when adding view dynamically

I have two simple views, one with a button that creates a view and append it to the page. The new view consists of a single list item with a link and an event to I need to bind to each list item. I think the problem here is the el object: What I have been reading the el object should be created automatically when it's not defined on construction of the view? See this fiddle
HTML:
<div id="main">
<button type="button" class="add">Add view</button>
<ul id="tasks" />
</div>
<script id="view-template-new-task" type="text/html">
<li>Task</li>
</script>
​
JS:
var TaskView = Backbone.View.extend({
events: {
'click a.fire': 'fire'
},
fire: function() {
alert('fire');
},
initialize: function() {
this.template = _.template($('#view-template-new-task').html());
},
render: function() {
$('#tasks').append(this.template());
}
});
var View = Backbone.View.extend({
events: {
'click button.add': 'addView'
},
addView: function(e) {
var task = new TaskView();
task.render();
}
});
$(function() {
var view = new View({
el: $('#main')
});
});​
Backbone automatically delegates events to the view element. As is, the el in your TaskView would point to an unattached div (the default el created by Backbone) and not to an element in your list.
The cure is simple : create your subview with its el set to a correct DOM node by setting a tagName to li and appending this element in your main view.
var TaskView = Backbone.View.extend({
tagName: 'li',
events: {
'click a.fire': 'fire'
},
fire: function() {
alert('fire');
},
initialize: function() {
this.template = _.template($('#view-template-new-task').html());
},
render: function() {
this.$el.html(this.template());
return this;
}
});
var View = Backbone.View.extend({
events: {
'click button.add': 'addView'
},
addView: function(e) {
var task = new TaskView();
this.$('#tasks').append(task.render().el);
}
});
And an updated Fiddle http://jsfiddle.net/BLP6J/31/

Resources