I'm having a problem re rendering a simple collection in backbone, the render event is never fired from the listeners... I'm not sure of where is the mistake, could please someone help me?
File with models:
window.MetricDevice = Backbone.Model.extend({
defaults: {
ip: null,
framesReceived: null,
framesOutOfOrder: null,
framesLost: null
}
});
window.MetricDevicesCollection = Backbone.Collection.extend({
model: MetricDevice,
value: null,
url: function(){
return hackBase + "/wm/iptv/metric/devices/json";
},
initialize:function () {
this.fetch({ reset: true });
console.log("data fetched");
},
});
Render page:
window.MetricItemView = Backbone.View.extend({
events: {
"click input[type=button]" : "removeDevice",
},
initialize:function(){
this.template = _.template(tpl.get('metric-devices-item'));
this.render();
},
removeDevice:function(){
$.ajax({
url:hackBase + '/wm/iptv/metric/disable/' + this.model.get("ip") + '/0/json',
dataType:"json",
success:function (data) {
if ( data.return == 1 ){
alert(data.error);
}else{
alert("Metric disabled in " + this.model.get("ip"));
}
},
});
},
render:function(){
var ip = this.model.get("ip");
console.log("rendering item in view " + ip);
},
});
window.MetricView = Backbone.View.extend({
events: {
"click input[type=button]" : "add",
"click input[type=img]" : "updateAll",
},
clicked:function(e){
},
updateAll:function(e){
this.render();
},
initialize:function () {
this.template = _.template(tpl.get('metric-devices-list'));
this.model.bind("change", function(){
console.log("metricView data change detected");
this.render();
});
this.model.bind("reset", this.render());
},
add:function(e){
if($(e.currentTarget).attr("name") == "add" ){
var ip = document.getElementById('vaddress').value;
var threshold = document.getElementById('vthreshold').value;
$.ajax({
url:hackBase + '/wm/iptv/metric/enable/' + ip + '/' + threshold + '/json',
dataType:"json",
success:function (data) {
if ( data.return == 1 ){
alert(data.error);
}else{
alert("Metric enabled in device");
}
},
});
}else if($(e.currentTarget).attr("name") == "cancel"){
document.getElementById('vaddress').value = "";
document.getElementById('vthreshold').value = "";
}
},
render:function (eventName) {
$(this.el).html(this.template());
var list = $(this.el).find('#tableData');
console.log("On render!");
var subviews = [];
console.log("looping on models");
_.each(this.model.models, function (sw) {
console.log("model loop " + sw.get("ip"));
var m = new MetricItemView({model:sw, tagName: 'tbody', el: $(this.el).find('#tableData')});
list.append(m.template(sw.toJSON()));
}, this);
return this;
},
});
The problem is that the render method in MetricView is called just when the page is loaded for the first time, and after this I've the impression that the JSON stay cached, and the content just change if I close the browser clean the cache and run again...
The console output is:
On render! metricView.js:86
looping on models metricView.js:88
model loop 10.0.0.1 metricView.js:92
rendering item in view 10.0.0.1 metricView.js:26
And I'm instantiating MetricView like this
var metricdevices = new MetricDevicesCollection();
$('#content').html(new MetricView({model:metricdevices}).render().el);
Am i forgetting something?
The problem you are facing is that you are not using Backbone models and collection at their full potential. When you are calling manual $.ajax(...), no Backbone event will fire. Here are a couple suggestions that would make your code integrate with Backbone.
First, you should instantiate your view with the proper reserved keyword: collection
var metricdevices = new MetricDevicesCollection();
$('#content').html(new MetricView({ collection : metricdevices }).render().el);
Backbone events in collection are intended to work with precise REST API practices. A Model that belongs to a Collection will inherit it's url parameter. It expects your REST API to map to the following scheme:
model.save() --> model.id is present ? PUT collection.url/model.id : POST collection.url
model.delete() --> DELETE collection.url/model.id
model.fetch() --> GET collection.url/model.id
The idea is that you can manipulate models individually and use collection to fetch all the relevant models when needed. Your API does not seem to be adapted for that kind of workflow.
A monky patch that would keep your data updated is to trigger a fetch of the collection when an operation succeeds.
var collection = this.collection;
$.ajax({
//...
success: {
collection.fetch();
}
}
Since you are already listening to the Backbone events, it will trigger the reset event and render the view. Note that this is not a good way of doing things. What you should do is refactor your server access points to conform to standard good REST practice if you have access to it. If you don't, use proper Object Oriented patterns and implement model behavior in the model. For example:
window.MetricDevice = Backbone.Model.extend({
defaults: {
ip: null,
framesReceived: null,
framesOutOfOrder: null,
framesLost: null
},
enable : function() {
var device = this,
ip = this.ip,
treshold = this.treshold;
$.ajax({
url:hackBase + '/wm/iptv/metric/enable/' + ip + '/' + threshold + '/json',
dataType:"json",
success:function (data) {
if ( data.return == 1 ){
alert(data.error);
} else {
device.trigger('change');
alert("Metric enabled in device");
}
}
});
return this;
}
});
And then you can properly call the object from a view:
var ip = document.getElementById('vaddress').value;
var threshold = document.getElementById('vthreshold').value;
var metricDevice = new MetricDevice({ ip : ip, treshold : treshold });
this.collection.add(metricDevice.enable());
Related
I am learning Backbone.js and as a trial project I am creating a little WordPress user management application. So far my code shows a listing of all WordPress users and it has a form which enables you to add new users to the application.
This all works fine however when you add a new user the listing of users doesn't update automatically, you need to refresh the page to see the new user added which isn't ideal and defeats one of the benefits of Backbone.js!
I have a model for a user and then a collection which compiles all the users. I have a view which outputs the users into a ul and I have a view which renders the form. How do I make my code work so when the .save method is called the view which contains the users updates with the new user? Or is there another way to approach this?
//define the model which sets the defaults for each user
var UserModel = Backbone.Model.extend({
defaults: {
"username": "",
"first_name": "",
"last_name": "",
"email": "",
"password": "",
},
initialize: function(){
},
urlRoot: 'http://localhost/development/wp-json/wp/v2/users'
});
//define the base URL for ajax calls
var baseURL = 'http://localhost/development/wp-json/wp/v2/';
//function to define username and password
function authenticationDetails(){
var user = "myUserName";
var pass = "myPassword";
var token = btoa(user+':'+pass);
return 'Basic ' + token;
}
//add basic authorisation header to all API requests
Backbone.$.ajaxSetup({
headers: {'Authorization':authenticationDetails()}
});
//create a collection which returns the data
var UsersCollection = Backbone.Collection.extend(
{
model: UserModel,
// Url to request when fetch() is called
url: baseURL + 'users?context=edit',
parse: function(response) {
return response;
},
initialize: function(){
}
});
// Define the View
UserView = Backbone.View.extend({
model: UserModel,
initialize: function() {
// create a collection
this.collection = new UsersCollection;
// Fetch the collection and call render() method
var that = this;
this.collection.fetch({
success: function () {
that.render();
}
});
},
// Use an external template
template: _.template($('#UserTemplate').html()),
render: function() {
// Fill the html with the template and the collection
$(this.el).html(this.template({ users: this.collection.toJSON() }));
return this;
},
});
var userListing = new UserView({
// define the el where the view will render
el: $('#user-listing')
});
NewUserFormView = Backbone.View.extend({
initialize: function() {
this.render();
},
// Use an external template
template: _.template($('#NewUserTemplate').html()),
render: function() {
// Fill the html with the template and the collection
$(this.el).html(this.template());
return this;
},
events: {
'click .create-user':'addNewUser'
},
addNewUser: function(){
var newFirstName = $('.first-name').val();
var newLastName = $('.last-name').val();
var newEmail = $('.email').val();
var newPassword = $('.password').val();
var newUserName = newFirstName.toLowerCase();
var myNewUser = new UserModel({username:newUserName,first_name:newFirstName,last_name:newLastName,email:newEmail,password:newPassword});
console.log(myNewUser);
myNewUser.save({}, {
success: function (model, respose, options) {
console.log("The model has been saved to the server");
},
error: function (model, xhr, options) {
console.log("Something went wrong while saving the model");
}
});
}
});
var userForm = new NewUserFormView({
// define the el where the view will render
el: $('#new-user-form')
});
All backbone objects (models, collections, views) throw events, some of which would be relevant to what you want. Models throw change events when their .set methods are used, and Collections throw add or update events... a complete list is here.
Once you know which events are already being thrown, you can listen to them and react. For example, use listenTo - in your view's initialize, you can add:
this.listenTo(this.collection, 'add', this.render);
That will cause your view to rerender whenever a model is added to your collection. You can also cause models, collections, whatever, to throw custom events using trigger from anywhere in the code.
EDIT: For the specific case of getting your user listing view to rerender when a new user is added using the form, here are the steps you can take... In the initialize method of your UserView, after the initialize the collection, add:
this.listenTo(this.collection, 'add', this.render);
Then in your form view... assuming you want to wait until the save is complete on your server, in the addNewUser method, in the success callback of your save, add:
userlisting.collection.add(model);
This will work, since the instance of your UserView is in the global scope. Hope this one works for you!
Is there any forum/Issue tracking website for backbone.js ?
I have an Issue that click event triggers multiple times. I had found a work around using Underscore.js , debounce method.
Is the problem addressed in latest backbone.js ?
Please suggest me on this .
Raja K
define([
'jquery', 'underscore','backbone',], function($, _, Backbone,Marionette) {
var Sample = Backbone.Marionette.ItemView.extend({
template: 'sample/sample',
model : new Model(),
render: function() {
id = utils.getStorage('some_id');
if(parseInt(id) > 0 ) {
this.model.set({some_id:id});
this.rendersome(id);
} else {
data = new Model().toJSON();
this.renderdata(data);
}
},
events: {
"click #some" : "someinfo"
},
someinfo : function() {
var self = this;
$.ajax({
url: API_URL + "sample/sampleinfo",
type: 'POST',
crossDomain: true,
cache: true,
data: JSON.stringify({ 'code1': this.model.get('code1'),
'code2' : this.model.get('code2'),
"auth" : init.auth, "user_id" : init.user_id }),
contentType: 'application/json',
success: function(data,response,jqXHR) {
if('SUCCESS' == data._meta.status && data.records.message.me == 'positive') {
self.model.set(data.records);
self.renderdata(data.records);
} else {
console.log(data.records.message);
return false;
}
},
error: function (request, status, error) {
console.log(request);
}
});
},
change: function (event) {
var target = event.target;
var change = {};
change[target.name] = target.value;
this.model.set(change);
},
});
return Sample;
});
Router Init code below,
routeaction : function() {
var Header = new HeaderView({'el': '#header'});
Header.render();
var test = new Tview({'el': '#content'});
test.render();
}
UPDATE : I am using backbone.subroute and the view got destroyed but not rendering after that. Becuase both old and current object referring the same element. But why it is not rendering again ? Can you please suggest me what I am missing here ?
render: function () {
// remove the existing header object
if(typeof gheader == "object") gheader.close();
// render the object
self.$el.html(tmpl(data));
},
UPDATE : I guess view.remove() is working fine. I have reused the container at $el and view.remove() removed the container element and stopped rendering the other views. Should I recreate the container ?
I can use "tagName" but suggest me how do I apply the stylesheet ?
undelegateEvents() Removes all of the view's delegated events. Useful if you want to disable or remove a view from
the DOM temporarily.
http://backbonejs.org/#View-undelegateEvents
I'm still quite new to backbone, so I'm sorry if there is any gross error in what I'm doing.
What I'm trying to do seems very simple: getting a collection of models from the db and do some filters on it. Let's say we are trying to filter hotels. Once I have my main collection, I would like to filter them for price, stars, and so on (pretty much what you can find in yelp or tripadvisor or so) - and of course, I want to "reset" the filters once the user uncheck the different checkboxes.
So far, I have 3 views:
- one view based on the panel where the results will be displayed
- one view based on a template that represents each item (each hotel)
- one view will all the filters I want to use.
The problem I am having is that I am bot able to keep my collection in such a state that I am able to revert my filters or to refresh the view with the new collection.
Can you please help me to understand where my problem is? And what should I do?
<script>
// model
var HotelModel = Backbone.Model.extend({});
// model view
var ItemView = Backbone.View.extend({
tagName : 'div',
className : 'col-sm-4 col-lg-4 col-md-4',
template : _.template($('#hotelItemTemplate').html()),
initialize : function() {
this.model.bind('change', this.render, this);
this.model.bind('remove', this.remove, this);
},
render : function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//view list
var HotelListView = Backbone.View.extend({
el : '#paginated-content',
events : {
"scroll" : "fetch"
},
initialize : function(options) {
var items = this.collection;
items.on('add', this.add, this);
items.on('all', this.render, this);
},
add : function(item) {
var view = new ItemView({
model : item
});
$('#paginated-content').append(view.render().el);
},
fetch : function() {
this.collection.getNextPage();
}
});
// filterign menu
var FilteringView = Backbone.View.extend({
el : '#filtering',
// just examples of one of the filters that user can pick
events : {
'click #price_less_100 ' : 'updateValue',
'click #five_stars ' : 'updateStars',
},
template : _.template($('#filteringTemplate').html()),
initialize : function() {
this.collection.on('reset', this.render, this);
this.collection.on('sync', this.render, this);
},
render : function() {
this.$el.html(this.template);
return this;
},
updateValue : function(e) {
//this is something I'm not using at the moment but that it contains a copy of the collection with the filters
var filtered = new FilteredCollection(coursesPaginated);
// if is checked
if (e.currentTarget.checked) {
var max = 100;
var filtered2 = _.filter(this.collection.models, function(item) {
return item.get("price") < max;
});
//not used at the moment
//filtered.filterBy('price', function(item) {
// return item.get('price') < 100;
//});
//this does not work
//this.collection.reset(filtered2);
//so I do this
this.collection.set(filtered2);
} else{
// here, i would like to have something to put the collection in a state before this filter was applied
//something that I do not use at the moment
//filtered.removeFilter('price');
}
},
updateStars : function(e) {
//do something later
}
});
// collection
var HotelCollection = Backbone.PageableCollection.extend({
model : HotelModel,
// Enable infinite paging
mode : "server",
url : '{{url("/api/hotels")}}',
// Initial pagination states
state : {
pageSize : 15,
sortKey : "updated",
order : 1
},
// You can remap the query parameters from `state` keys from
// the default to those your server supports
queryParams : {
totalPages : null,
totalRecords : null,
sortKey : "sort"
},
parse : function(response) {
$('#hotels-area').spin(false);
this.totalRecords = response.total;
this.totalPages = Math.ceil(response.total / this.perPage);
return response.data;
}
});
$(function() {
hotelsPaginated = new HotelCollection();
var c = new HotelListView({
collection : hotelsPaginated
});
$("#paginated-content").append(c.render().el);
hotelsPaginated.fetch();
});
It seems to me that it is not so easy to do filtering like this using backbone. If someone has other suggestion,please do.
Thank you!
My solution for this:
Main Collection which fetched from server periodically.
Filtered Collection which resets each time you use filtering.
Main view which used to render filtered collection (example new MainView({collection: filteredCollection};)) Also there must be
handler for collection 'reset' event.
Filter view which have a model containing filter values and which triggers Filtered Collection reset with new values.
Everithing easy.
Sorry for no code examples, not on work)
I have these routes in my webservice and I can hit either of them directly through the browser and I return the correct value.
app.get('/repairs', repair.findAll);
app.get('/repairs/:id', repair.findById);
When I ask Backbone to do this I am unexpectedly getting a call to
app.get('/repairs', repair.findAll);
when I expect it to reach
app.get('/repairs/:id', repair.findById);
The piece of code that appears to be calling "/repairs" rather than "/repairs/:id" is
var EditRepair = Backbone.View.extend({
el : '.page',
render : function(options) {
var scope = this;
var repair = new Repair({id: options.id});
//This has the correct id
console.log(options.id);
//I would expect this to call /repairs/12344312
//However it calls /repairs
repair.fetch({
success : function(repair){
var template = _.template($('#edit-repair-template').html(), {repair : repair});
scope.$el.html(template);
}
});
}
});
var Repair = Backbone.Model.extend({
urlRoot : 'repairs'
});
var Router = Backbone.Router.extend({
routes: {
'edit/:id' : 'editRepair'
}
});
var editRepair = new EditRepair();
var router = new Router();
router.on('route:editRepair', function(id) {
console.log('Edit Id : ' + id);
editRepair.render({id:id});
});
The options.id can be console.logged and shows the correct id of the item. I've had a few issues so far with the difference between _id in mongodb and id in backbone which I have worked around but for the life of me I cannot see why this is issuing a call to repairs and not repairs/id.
Any help appreciated.
My fault, I had an ajax prefilter that was encoding the uri components.
This was messing up the requests being issued.
$.ajaxPrefilter( function( options, originalOptions, jqXHR ) {
options.url = "http://localhost:3000/" + encodeURIComponent( options.url );
console.log(options.url);
});
Changed to
$.ajaxPrefilter( function( options, originalOptions, jqXHR ) {
options.url = "http://localhost:3000/" + options.url;
console.log(options.url);
});
I have a collection and I need to access a model in the collection when a route is fired:
App.Houses = Backbone.Collection.extend({
model: App.House,
url: API_URL,
})
App.houseCollection = new App.Houses()
App.houseCollection.fetch();
App.HouseDetailRouter = Backbone.Router.extend({
routes: {
'': 'main',
'details/:id': 'details',
},
initialize: function() {
},
main: function() {
App.Events.trigger('show_main_view');
},
details: function(id) {
model = App.houseCollection.get(id);
console.log(model);
App.Events.trigger('show_house', model);
},
});
The result of that console.log(model) is undefined. I think that this is the case because the collection has not finished the fetch() call?
I want to attach the model to the event that I am firing so that the views that respond to the event can utilize it. I might be taking a bad approach, I am not sure.
One of the views that responds to the event:
App.HouseDetailView = Backbone.View.extend({
el: '.house-details-area',
initialize: function() {
this.template = _.template($('#house-details-template').html());
App.Events.on('show_house', this.render, this);
App.Events.on('show_main_view', this.hide, this);
},
events: {
'click .btn-close': 'hide',
},
render: function(model) {
var html = this.template({model:model.toJSON()});
$(this.el).html(html);
$(this.el).show();
},
hide: function() {
$(this.el).hide();
App.detailsRouter.navigate('/', true);
}
});
EDIT: Somewhat hacky fix: See details()
App.HouseDetailRouter = Backbone.Router.extend({
routes: {
'': 'main',
'details/:id': 'details',
},
initialize: function() {
},
main: function() {
App.Events.trigger('show_main_view');
},
details: function(id) {
if (App.houseCollection.models.length === 0) {
// if we are browsing to website.com/#details/:id
// directly, and the collection has not finished fetch(),
// we fetch the model.
model = new App.House();
model.id = id;
model.fetch({
success: function(data) {
App.Events.trigger('show_house', data);
}
});
} else {
// if we are getting to website.com/#details after browsing
// to website.com, the collection is already populated.
model = App.houseCollection.get(id);
App.Events.trigger('show_house', model);
}
},
});
Since you are using neither the callbacks nor events to know when the collection's fetch call completes, perhaps fetching the collection is generating an error, or the model you want is not included in the server response, or you are routing to the view before fetch has completed.
As to your approach, here are some miscellaneous tips:
better to pass the model to the view in the view's constructor's options parameter. render() takes no arguments and I think it is unconventional to change that.
Always return this from render() in your views
You can move your this.template = _.template code to the object literal you pass to extend. This code only needs to be run once per app load, not for each individual view
For now the simplest thing may be to instantiate just a model and a view inside your details route function, call fetch on the specific model of interest, and use the success callback to know when to render the view.