backbone events triggered arbitrary number of times - javascript

New to backbone, I'm approaching it with trivial example but i'm stuck on events triggering.
In my page i GET some data from the server and initialize a view with them.
Then i can change the data through a form, they are saved to the server and the template is updated soon after with the new data. The data are fetched, saved and the view is updated but I can see through the console that the render function is called sometime twice, sometime even 4 times! And the call to the server, too, is made up to for time as GET, PUT, GET, GET.
What am i doing wrong?
<script type="text/x-handlebars-template" id="modelTpl">
<ul>
<li><strong>Nome:</strong> {{nome}}</li>
<li><strong>Cogome:</strong> {{cognome}}</li>
</ul>
<input type="text" id="nome" />
<input type="text" id="cognome" />
<a id="changeBtn">Modifica</a>
</script>
Model = Backbone.Model.extend({
urlRoot: 'server/model.php/model',
defaults: {
nome: "",
cognome: ""
}
});
var model = new Model({id: 1});
View = Backbone.View.extend({
el: '#wrapper',
template: Handlebars.compile($('#modelTpl').html()),
events: {
"click #changeBtn": "change"
},
initialize: function(){
this.render();
this.listenTo(this.model, "change", this.render);
},
render: function(){
console.log('render');
var self = this;
this.model.fetch({
error: function(model, response, options) { },
success: function(model, response, options) {
var data = model.toJSON();
self.$el.html(self.template(data));
}
});
return this;
},
change: function() {
var n = this.$('#nome').val();
var c = this.$('#cognome').val();
this.model.save({ nome: n, cognome: c }, {
error: function(model, response, options) { },
success: function(model, response, options) { }
});
}
});
var view = new View({ model: model });

I know its late to answer this question now. However it may help other, so try _.debounce
this.change = _.debounce(this.change, 300);

Related

Updating collection and view in Backbonejs

I've created a search bar, but when the data is gathered from the user, it displays the default data over again rather then the users new search criteria.
I'm resetting the collection and giving it a new URL when the user searches, but it doesn't seem to update correctly, and I'm having trouble figuring out where my problem(s) are.
(function(){
'use strict';
var red = red || {};
//model////////////////////////////////////////////////
red.RedditModel = Backbone.Model.extend({
defaults: {
urlTarget: $('#textBox').val(),
urlStart: 'https://www.reddit.com/r/',
urlEnd: '.json'
},
initialize: function() {
this.on('change:urlTarget', function() {
console.log('The Url Target has changed to ' + this.get("urlTarget"));
});
this.on('change:concatURL', function() {
console.log('The model Url has changed to ' + this.get("concatURL"));
});
this.on('change:url', function() {
console.log('The collection url has changed to: ' + this.get('url'));
});
}
});
var redditModel = new red.RedditModel();
var fullURL = new red.RedditModel({
concatURL: redditModel.attributes.urlStart + redditModel.attributes.urlTarget + redditModel.attributes.urlEnd
});
var listElmement,
$list = $('.list');
//collections//////////////////////////////////////////
red.redditCollection = Backbone.Collection.extend({
model: red.RedditModel,
url: fullURL.attributes.concatURL,
parse: function(response) {
var redditData = response.data.children;
return redditData;
}
});
//view////////////////////////////////////
red.RedditView = Backbone.View.extend({
model: fullURL,
collection: redditCollection,
el: '.searchBar',
events: {
'click .searchButton': function(e) {
this.updateModel(e);
this.updateCollection(e);
},
'change #textBox': 'initialize'
},
updateModel: function() {
this.$urlTarget = $('#textBox').val()
this.model.set('urlTarget', this.$urlTarget);
this.model.set('concatURL', redditModel.attributes.urlStart + this.$urlTarget + redditModel.attributes.urlEnd);
},
updateCollection: function() {
this.collection.reset();
this.$urlTarget = $('#textBox').val();
var newUrl = redditModel.attributes.urlStart + this.$urlTarget + redditModel.attributes.urlEnd;
this.collection.add({ urlTarget: this.$urlTarget });
this.collection.add({ url: newUrl });
console.log(newUrl);
},
tagName: 'li',
className: 'listItems',
initialize: function() {
$list.html('');
this.collection.fetch({
success: function(redditData) {
redditData.each(function(redditData) {
redditData = redditData.attributes.data.title
listElmement = $('<li></li>').text(redditData);
$list.append(listElmement);
})
}
});
},
render: function() {
}
});
var redditCollection = new red.redditCollection({
redditModel,
fullURL
});
var myRedditView = new red.RedditView({
model: redditModel,
collection: redditCollection
});
$('.page').html(myRedditView.render());;
})();
Parse within the model, and use it for its intended purpose. No need to store the reddit url and other search related info in a model.
red.RedditModel = Backbone.Model.extend({
parse: function(data) {
return data.data;
},
})
Since you already take care of the reddit url here. Don't be afraid to make yourself some utility functions and getters/setters in your Backbone extended objects (views, model, collection, etc).
red.RedditCollection = Backbone.Collection.extend({
url: function() {
return 'https://www.reddit.com/r/' + this.target + this.extension;
},
initialize: function(models, options) {
this.extension = '.json'; // default extension
},
setExtension: function(ext) {
this.extension = ext;
},
setTarget: function(target) {
this.target = target;
},
parse: function(response) {
return response.data.children;
}
});
Don't be afraid to have a lot of views, Backbone views should be used to wrap small component logic.
So here's the item:
red.RedditItem = Backbone.View.extend({
tagName: 'li',
className: 'listItems',
render: function() {
this.$el.text(this.model.get('title'));
return this;
}
});
Which is used by the list:
red.RedditList = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.listenTo(this.collection, 'sync', this.render);
},
render: function() {
this.$el.empty();
this.collection.each(this.renderItem, this);
return this;
},
renderItem: function(model) {
var view = new red.RedditItem({ model: model });
this.$el.append(view.render().el);
}
});
And the list is just a sub-component (sub-view) of our root view.
red.RedditView = Backbone.View.extend({
el: '.searchBar',
events: {
'click .searchButton': 'onSearchClick',
},
initialize: function() {
// cache the jQuery element for the textbox
this.$target = $('#textBox');
this.collection = new red.RedditCollection();
this.list = new red.RedditList({
collection: this.collection,
// assuming '.list' is within '.searchBar', and it should
el: this.$('.list'),
});
},
render: function() {
this.list.render();
return this;
},
onSearchClick: function(e) {
this.collection.setTarget(this.$target.val());
console.log(this.collection.url());
this.collection.fetch({ reset: true });
},
});
Then, you only need the following to use it:
var myRedditView = new red.RedditView();
myRedditView.render();
Notice the almost non-existent use of the global jQuery selector. If you're using Backbone and everywhere you're using $('#my-element'), you're defeating the purpose of Backbone which is, in part, to apply MVC concepts on top of jQuery.
Some notes on the code posted
Take time to understand what's going on. There are several lines of code in your question that doesn't do anything, or just don't work at all.
Though it's been removed in your answer, the following doesn't make sense because the collection constructor is Backbone.Collection([models], [options]) and what you have here translates to passing an options object (using ES6 shorthand property names { a, b, c}) to the models parameter.
var redditCollection = new red.redditCollection({
redditModel,
fullURL
});
This line does nothing, because .render() doesn't do anything and doesn't return anything.
$('.page').html(myRedditView.render());
Here, you're creating a new element manually using jQuery while you have Backbone which does this for you.
$('<li></li>').text(redditData);
Don't use the attributes directly, always use .get('attributeKey') unless you have a good reason not to.
redditModel.attributes.urlStart
Favor local variables whenever you can. The listElement var here is defined at the "app" level without a need for it.
listElmement = $('<li></li>').text(redditData);
$list.append(listElmement);
A Backbone collection is automatically filled with the new instances of models on success. You do not need to re-parse that in the success callback (in addition to the ambiguity with redditData).
this.collection.fetch({
success: function(redditData) {
redditData.each(function(redditData) {
redditData = redditData.attributes.data.title;
I don't mean to be rude and I took the time to write that long answer to try to help, you, and any future reader that comes by.

Model method error while trying to navigate

I have several Backbone Models rendered in a Collection View, and also I have a route that should render a view of that model. So, here come the views
resume.js
// this renders a single model for a collection view
var ResumeView = Backbone.View.extend({
model: new Resume(),
initialize: function () {
this.template = _.template($('#resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON));
return this;
}
});
#resume template
<section id="resume">
<h1><%= profession %></h1>
<!-- !!!!! The link for a router which should navigate to ShowResume view -->
View Details
</section>
Collection view:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection;
this.collection.on('add', this.render, this);
// Getting the data from JSON-server
this.collection.fetch({
success: function (res) {
_.each(res.toJSON(), function (item) {
console.log("GET a model with " + item.id);
});
},
error: function () {
console.log("Failed to GET");
}
});
},
render: function () {
var self = this;
this.$el.html('');
_.each(this.collection.toArray(), function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
}
});
The code above works perfectly and does exactly what I need -- an array of models is fetched from my local JSON-server and each model is displayed within a collection view. However, the trouble starts when I try to navigate through my link in the template above. Here comes the router:
var AppRouter = Backbone.Router.extend({
routes: {
'': home,
'resumes/:id': 'showResume'
},
initialize: function (options) {
// layout is set in main.js
this.layout = options.layout
},
home: function () {
this.layout.render(new ResumeList({collection: resumes}));
},
showResume: function (cv) {
this.layout.render(new ShowResume({model: cv}));
}
});
and finally the ShowResume view:
var ShowResume = Backbone.View.extend({
initialize: function (options) {
this.model = options.model;
this.template = _.template($('#full-resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
I didn't provide the template for this view because it is quite large, but the error is following: whenever I try to navigate to a link, a view tries to render, but returns me the following error: Uncaught TypeError: this.model.toJSON is not a function. I suspect that my showResume method in router is invalid, but I can't actually get how to make it work in right way.
You are passing the string id of the url 'resumes/:id' as the model of the view.
This should solve it.
showResume: function (id) {
this.layout.render(new ShowResume({
model: new Backbone.Model({
id: id,
profession: "teacher" // you can pass data like this
})
}));
}
But you should fetch the data in the controller and react accordingly in the view.
var AppRouter = Backbone.Router.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'resumes/:id': 'showResume'
},
initialize: function(options) {
// layout is set in main.js
this.layout = options.layout
},
home: function() {
this.layout.render(new ResumeList({ collection: resumes }));
},
showResume: function(id) {
// lazily create the view and keep it
if (!this.showResume) {
this.showResume = new ShowResume({ model: new Backbone.Model() });
}
// use the view's model and fetch
this.showResume.model.set('id', id).fetch({
context: this,
success: function(){
this.layout.render(this.showResume);
}
})
}
});
Also, this.model = options.model; is unnecessary as Backbone automatically picks up model, collection, el, id, className, tagName, attributes and events, extending the view with them.

How can I prevent Backbones save method from trying to update every model?

I am creating a crud web app with backbone. I am writing the functionality to update a resource (PUT). I am trying to achieve this by fetching a models properties from the server (see the SubscriberView) and on successfully fetching the resource to instantiate a SubscriberEditView whereby the newly fetched model is passed.
So far this works as expected; SubscriberEditView renders an html form which is populated with the model instance properties.
When I enter a new login value into the form I can trigger the update function which successfully makes a PUT request to the server resource and updates the model instance as expected.
However, the problem is that when I then repeat this process with another model instance the PUT request is made against the curent model AND the previously instantiated model.
Is the reason for this because I now have two instances of SubscriberEditView? Or is it something else that I have missed/misunderstood.
Please see below the described code.
// The view for a single subscriber
var SubscriberView = Backbone.View.extend({
tagName: 'tr',
template: _.template($('#subscribers-tmpl').html()),
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
return this;
},
events: {
'click .remove': 'onRemove',
'click .edit-subscriber': 'editSubscriber',
},
editSubscriber: function() {
var getSubscriberModel = this.model.set('id', this.model.attributes.id, {silent:true})
getSubscriberModel.fetch({
success: function (model, response) {
$('#addSubscriber').fadeOut();
new SubscriberEditView({model:model});
},
error: function (response) {
console.log('There was an error');
}
});
},
onRemove: function() {
this.model.destroy();
}
});
// The edit view
var SubscriberEditView = Backbone.View.extend({
tagName: 'div',
el: '#updateSubscriber',
template: _.template($('#subscriberEdit-tmpl').html()),
initialize: function() {
this.model.on('sync', this.render, this);
},
events: {
'click #close': 'cancel',
'click .save-subscriber': 'update'
},
update: function() {
var $login = this.$('#login');
this.model.save({
login: $login.val(),
},
{
dataType: 'text',
success: function (model, response, options) {
console.log('success');
},
error: function (model, response, options) {
console.log('error');
}
});
},
cancel: function() {
$('#addSubscriber').fadeIn();
$('#editInner').fadeOut();
},
render: function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
},
});
If anyone could help then that would be greatly appreciated.
Cheers
The issue is el: '#updateSubscriber',. All your view instances are pointing to same element to which events are delegated. So clicking on any of the .save-subscriber will trigger update for all the view instances. You should not specify el for a view that is going to have more than one instance.

Using views in backbone.js

I'm currently learning backbone.js and have a little problem. I dont' quite get how the view works.
I have created a model, a collection, and another model that again contains the collection:
Sensor = Backbone.Model.extend({
defaults: {
channel: '',
name: '',
temperature: 0,
tempMin: 0,
tempMax: 0
}
});
SensorList = Backbone.Collection.extend({
model: Sensor
});
Now I created a view, so I am able to render the sensor collection with handlebar.js template:
TemperatureView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function(eventName) {
var source = $('#sensor-list-template').html();
var template = Handlebars.compile(source);
var html = template(this.collection.toJSON());
this.$el.html(html);
} 
});
Now I want to load some data and render the information. But I don't know how to get the data into my view...I tried this:
$(document).ready(function() {
var temps = new TemperatureRequest();
temps.fetch({
success: function() {
console.log(temps);
var test = temps.get("sensors");
console.log(test);
var tempView = new TemperatureView({
collection: test
});
}
});
});
The data is fetched correctly. I have a collection of sensors. And now I want to pass them to the view so it is getting rendered....but I don't understand how this is done..pls help!
Since you are passing the collection to the view while creating it, you can access the same using this.collection inside your view anywhere.
var tempView = new TemperatureView({
collection: test
});
More over you have added the render function inside your initialize , it automatically calls the render function.Inside the render it fetches the collection and since your template needs only json object you are converting your collection it to json array objects.Templates takes care of appending the values to html.
If you want to add automatic view render to happen whenever the collection removes a model or adds a model into it you can add a listener and callback function to it
initialize : function(){
console.log("initializing view");
this.collection.on('add', this.render, this);
this.collection.on('reset', this.render, this);
this.render();
}
I just got it. Took me a while and I have definitly some reading to do.
There were several problems. First of all I have to overwrite the parse function, so the collection is stored correctly in my model:
TemperatureRequest = Backbone.Model.extend({
urlRoot: '/temperatures',
defaults: {
timestamp: '',
logfile: '',
sensorList: new SensorList()
},
parse: function(response) {
response.sensorList = new SensorList(response.sensors);
return response;
},
success: function(response) {
console.log('success');
}
});
In my view I know add the listen to events as suggested and also fetch the data within the initialize function to get rid of the success callback:
TemperatureView = Backbone.View.extend({
el: '#temperatures',
initialize: function() {
this.listenTo(this.model, 'reset', this.render);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'add', this.render);
this.model.fetch();
},
render: function(eventName) {
var list = this.model.get('sensorList');
console.log(list.toJSON());
var source = $('#sensor-list-template').html();
var template = Handlebars.compile(source);
var html = template(list.toJSON());
this.$el.html(html);
this.renderTimestamp();
},
renderTimestamp: function() {
var tsText = $("<p></p>").addClass("text-right");
var timestamp = $("<div></div>").addClass("col-sm-4 col-sm-offset-8").append(tsText);
tsText.text(this.model.get('timestamp'));
$('#timestamp').append(timestamp);
}
});
now I can do this to render the data:
$(document).ready(function() {
var temps = new TemperatureRequest();
var tempsView = new TemperatureView({
model: temps
});
});
Instead of passing the collection to the view I pass the model to it and fetch the data inside of the initialize function.
What I still don't understand is when I have to use "this" and when I have to use _bindAll...

Removing model from collection

This is a first attempt to making a backbone.js application.
I have a contact which is my model and a list/collection of contacts.
The initial rendering and fetching of the list of contacts works fine.
Now I'm trying to remove a contact from the collection after a click event.
It seems like I'm doing something wrong in the delete function of the ContactsView.
When I console.log contacts at the end of that method, the this.contacts collection is not changed.
Any help is appreciated!
var Contact = Backbone.Model.extend({
});
var Contacts = Backbone.Collection.extend({
model: Contact,
url: '/backbone/crm/contact'
});
var ContactsView = Backbone.View.extend({
initialize: function() {
this.contacts = new Contacts();
this.contacts.bind("reset", this.render, this);
this.contacts.bind("change", this.render, this);
this.contacts.bind("remove", this.render, this);
this.contacts.fetch();
},
events: {
"click .delete": "delete"
},
render: function() {
$("#contacts tbody").replaceWith(
$("#contacts_tmpl").render({ 'contacts': this.contacts.toJSON() }));
},
delete: function(e) {
var id = $(e.currentTarget).parents("tr").attr("id");
var model = this.contacts.get(id);
this.contacts.remove(model);
}
});
var contactsView = new ContactsView({ el: $("#contacts")});
The model is effectively removed from the collection. But now I need to remove it server side to.
I've implemented this by doing:
var Contacts = Backbone.Collection.extend({
model: Contact,
url: '/backbone/crm/contact',
initialize: function() {
this.bind("remove", this.delete, this);
},
delete: function(model) {
model.destroy();
}
This works and sends the correct delete request, but I find this kind of awkward, to call a destroy method from the delete method.

Categories