I work on a large enterprise application with BackboneJS. One page in the application is constructed using multiple sub-system calls via REsT. How do I make sure that, all the services required to load the page has been called and the templates are binding is completed?
For example, I have a MasterView, that takes care of the collection.fetch() of each child views, like this.
myApp.views.MasterView = Backbone.View.extend({
initialize: function(params) {
var self = this;
this.collection.fetch({
success: function(resp) {
self.collection.bind("reset", self.render(), self);
},
error: function(xhr, xhrStatus) {
// push error message, in case of fetch fails.
}
});
},
render: function() {
var self = this;
this.collection.each(function(model) {
if (model.get('authorize') && model.get('status') === "success" && model.get('data').length > 0) {
self.bindTemplate(model.get('data')[0]);
}
});
}
});
I have a view set for the page, that takes care of rendering two other views CustomerInfo and CustomerAccounts. The view goes like this.
myApp.views.CustomerView = Backbone.View.extend({
initialize: function() {
var customerInfo = new myApp.collection.CustomerInfo();
new myApp.views.CustomerInfo({el: $("#infoContainer"), collection: customerInfo});
var customerAccount = new myApp.collection.CustomerAccount();
new myApp.views.CustomerAccount({el: $("#accountContainer"), collection: customerAccount});
}
});
And CustomerInfo and CustomerAccount views, looks like this,
myApp.views.CustomerInfo = myApp.views.MasterView.extend({
initialize: function() {
var self = this;
myApp.views.MasterView.prototype.initialize.call(self, {
qParam1: "qparam1",
qParam2: "qparam2"
});
},
render: function() {
var self = this;
self.template = _.template(myApp.Templates.get("customer-page/customer-info"));
myApp.views.MasterView.prototype.render.apply(self);
},
bindTemplate: function(data) {
var self = this;
$(self.el).html(self.template({"info": data}));
}
});
myApp.views.CustomerAccounts = myApp.views.MasterView.extend({
initialize: function() {
var self = this;
myApp.views.MasterView.prototype.initialize.call(self, {
qParam1: "qparam1"
});
},
render: function() {
var self = this;
self.template = _.template(myApp.Templates.get("customer-page/customer-accounts"));
myApp.views.MasterView.prototype.render.apply(self);
},
bindTemplate: function(data) {
var self = this;
$(self.el).html(self.template({"accounts": data}));
}
});
I would like to know if there is any way to know from the myApp.views.CustomerView that the views CustomerInfo and CustomerAccounts has completed its rendering? The main problem I have here is the CustomerInfo view loads quickly but the CustomerAccount view takes some time to load. Hence I need to display the page on one shot, when both the views are ready on DOM.
When you instantiate your views add a listener to a view_ready event. When the view finishes fetching the data and rendering make it trigger it on itself
At the end of the render method of the parent view
self.trigger('view_ready');
And on the Main view add something like :
this.listenTo(CustomerInfoView, 'view_ready', this.customer_info_ready);
this.listenTo(CustomerAccountsView, 'view_ready', this.customer_account_ready);
Then in your main view or main model add 2 properties : info_ready and customer_ready and initialize them to 0
Each time one of the 2 previously mentioned event is fired do something like the following :
customer_info_ready : function(){
this.model.set('info_ready',true);
if (this.model.get('account_ready') === true) {
this.trigger('both_elements_ready');
}
}
customer_account_ready : function(){
this.model.set('account_ready',true);
if (this.model.get('info_ready') === true) {
this.trigger('both_elements_ready');
}
}
Then add a listener to 'both_elements_ready' on your main view :
initialize: function() {
//your code
this.on('both_elements_ready',this.page_ready); }
EDIT: Added info to make answer more relevant to question and more detailed.
Edit: This answer is inspired by what I learned from a sample "Brunch with Chaplin" example here. Chaplin is a framwork built on top of backbone.
Ok, so I'm going to suggest a coffeescript based solution (I just happen to have a solution in coffee :/ ! Try using js2coffee if you want to convert back to js)
The Master Class
The idea is to have a master class - not view - that will hold the app in it's hand, instead of the master view.
module.exports = class Application
initialize: (finished_init) =>
#collection = new Collection()
#main_view = new View()
#collection.fetch(
success: (collection) =>
#main_view.collection = #collection
finished_init()
)
render: (targetLocation, params) ->
switch targetLocation
when "main" then (
#main_view.render()
)
In the initialize method, we fetch the collection data. On success, the finshed_init() is called. You can replace it with #render (Btw, # == this :) )
Initialization
Here's how I'd initialize my app:
$ ->
app = new Application()
app.initialize( ->
#----------------------------------#
# Create the router and its routes
# This is called at the finished_init() of the Application class
#----------------------------------#
app_router = new Router
)
Running More than one fetche Asynchronously
You could have a function that monitors fetche's completion, or you could try using async. It has a nice `parallel function that does just that.
#collection1 = new Collection()
#collection = new Collection()
#main_view1 = new View()
#main_view2 = new View()
async.parallel [ (callback) ->
setTimeout (->
#collection1.fetch(
success: (collection) =>
#main_view1.collection = #collection
callback null, "one"
), 200
, (callback) ->
setTimeout (->
#collection2.fetch(
success: (collection) =>
#main_view2.collection = #collection
callback null, "two"
), 100
], (err, results) ->
if not err
#render() # Render what you want
After breaking my head for quite sometime, and searching on Google, I found this link.
So I made few changes to my MasterView and it was working and have solved the problem I had. The change I made in the MasterView is this
var activeConnections=0;
myApp.views.MasterView = Backbone.View.extend({
initialize: function(params) {
activeConnections++;
var self = this;
this.collection.fetch({
success: function(resp) {
activeConnections--;
self.collection.bind("reset", self.render(), self);
if(activeConnections===0){
// trigger the page has finished rendering
}
},
error: function(xhr, xhrStatus) {
// push error message, in case of fetch fails.
if(activeConnections===0){
// trigger the page has finished rendering
}
}
});
},
render: function() {
var self = this;
this.collection.each(function(model) {
if (model.get('authorize') && model.get('status') === "success" && model.get('data').length > 0) {
self.bindTemplate(model.get('data')[0]);
}
});
}
});
Thanks to all those who have helped me solve this.
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!
Seems like this should be obvious, but there seem to be so many different examples out there, most of which cause errors for me, making me think they are out of date. The basic situation is that I have a MessageModel linked to a MessageView which extends ItemView, MessageCollection linked to a MessageCollectionView (itemView: MessageView). I have a slightly unusual scenario in that the MessageCollection is populated asynchronously, so when the page first renders, it is empty and a "Loading" icon would be displayed. Maybe I have things structured incorrectly (see here for the history), but right now, I've encapsulated the code that makes the initial request to the server and receives the initial list of messages in the MessageCollection object such that it updates itself. However, I'm not clear, given this, how to trigger displaying the view. Obviously, the model shouldn't tell the view to render, but none of my attempts to instantiate a view and have it listen for modelChange events and call "render" have worked.
I have tried:
No loading element, just display the CollectionView with no elements on load, but then it doesn't refresh after the underlying Collection is refreshed.
Adding modelEvents { 'change': 'render' } to the view --> Uncaught TypeError: Object function () { return parent.apply(this, arguments); } has no method 'on'
I also tried this.bindTo(this.collection..) but "this" did not nave a bindTo method
Finally, I tried, in the view.initialize: _.bindAll(this); this.model.on('change': this.render); --> Uncaught TypeError: Object function () { [native code] } has no method 'on'
Here is the code
Entities.MessageCollection = Backbone.Collection.extend({
defaults: {
questionId: null
},
model: Entities.Message,
initialize: function (models, options) {
options || (options = {});
if (options.title) {
this.title = options.title;
}
if (options.id) {
this.questionId = options.id;
}
},
subscribe: function () {
var self = this; //needed for proper scope
QaApp.Lightstreamer.Do('subscribeUpdate', {
adapterName: 'QaAdapter',
parameterValue: this.questionId,
otherStuff: 'otherstuff',
onUpdate: function (data, options) {
console.log("calling sync");
var obj = JSON.parse(data.jsonString);
self.set(obj.Messages, options);
self.trigger('sync', self, obj.Messages, options);
}
});
},
});
Views.MessageCollectionView = Backbone.Marionette.CollectionView.extend({
itemView: Views.MessageView,
tagName: 'ul',
// modelEvents: {
// 'change': 'render'
// },
onAfterItemAdded: function (itemView) {
this.$el.append(itemView.el);
}
});
var Api = {
subscribe: function (id) {
var question = new QaApp.Entities.Question(null, { id: id });
question.subscribe();
var questionView = new QaApp.Views.QuestionView(question);
QaApp.page.show(questionView);
}
};
I am very grateful for all the help I've received already and thanks in advance for looking.
Try this:
var questionView = new QaApp.Views.QuestionView({
collection: question
});
Ok I have a layout like the one in this pic:
The table in the upper part of the screen is made by:
MessageListView
define(['backbone','collections/messages','views/message'], function(Backbone, MessageCollection, MessageView) {
var MessageListView = Backbone.View.extend({
el: '#messagesContainer',
initialize: function() {
this.collection = new MessageCollection();
this.collection.fetch({reset:true});
this.listenTo( this.collection, 'reset', this.render );
this.table = this.$el.find("table tbody");
this.render();
},
render: function() {
this.collection.each( function(message, index) {
this.renderMessage(message, index);
}, this);
},
renderMessage: function(message, index) {
var view = new MessageView({
model:message,
className: (index % 2 == 0) ? "even" : "odd"
});
this.table.append( view.render().el );
}
});
return MessageListView;
});
MessageView
define(['backbone','models/message'], function(Backbone, MessageCollection, MessageView) {
var MessageView = Backbone.View.extend({
template: _.template( $("#messageTemplate").html() ),
render: function() {
this.setElement( this.template(this.model.toJSON()) );
return this;
},
events:{
'click':'select'
},
select: function() {
// WHAT TO DO HERE?
}
});
return MessageView;
});
AppView
define(['backbone','views/messages'], function(Backbone, MessageList) {
var App = Backbone.View.extend({
initialize: function() {
new MessageList();
}
});
return App;
});
I will soon add a new view (maybe "PreviewView") in the lower part of the screen.
I want to make something happen inside the "PreviewView" when user clicks a row.
For example, it could be interesting to display other model's attributes (details, e.g.) inside the PreviewView.
What is the best practice?
holding a reference to PreviewView inside each MessageView ?
triggering events inside select method, and listening to them using on() inside the preview view.
using a transient "selected" attribute in my model, and make PreviewView listen to collection "change" events?
Thank you, if you need more details tell me please, I'll edit the question.
Not sure about the best practice but I found this solution trivial to implement. I created a global messaging object, bus, whatever:
window.App = {};
window.App.vent = _.extend({}, Backbone.Events);
You have to register the "triggerable" functions of PreviewView on the previously created event bus (according to your example, this should be in the PreviewView):
initialize: function () {
App.vent.on('PreviewView.show', this.show, this);
}
Now you should be able to trigger any of registered events from anywhere within your application by calling: App.vent.trigger. For example when the user click on a row you will have something similar:
App.vent.trigger('PreviewView.show');
in case if you have to send and object along with the triggered event use:
App.vent.trigger('PreviewView.show', data);
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());
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);
});