My Chrome Extension has two main components scripts:
popup.js and popup.html - the extension logic and UI respectively.
scrape.js - JavaScript embedded into a webpage to scrape a key value.
scrape.js - is very simple. It does only two things:
Scrape webpage for key.
Run 6 distinct database queries using scraped key.
Because of the library involved (outside of my control) the database queries must be run from scrape.js (i.e. in the context of tab's web page).
When (or more importantly, if) each of 6 queries completes, scrape.js calls popup.js notifying it some data is ready for rendering.
Upon receiving the callback notification popup.js uses Backbone models and views to render the data provided by scrape.js:
var modelA = Backbone.Model.extend({...});
modelA.set({...});
var modelB = Backbone.Model.extend({...});
modelB.set({...});
These models are created from callbacks and only exist if the relevant callback is made by scrape.js to popup.js. In this sense the models and any views that render them are optional.
If a callback is made, a model is stored in a global variable modeInstances so I may render it later. If all 6 queries complete, 6 callbacks are made and 6 Backbone model instances will be stored in modeInstances.
When popup.js receives a final callback, it renders any models and views:
modelInstances.a = modelA; // Could be undefined.
modelInstances.b = modelB; // Could be undefined.
I have three Backbone.js views, one of these views renders both models (A and B):
var viewA = new Views.ViewA({
modelA: modelInstances.modelA,
});
viewA.render();
var viewB = new Views.ViewB({
modelB: modelInstances.modelB,
});
viewB.render();
var viewAandB = new Views.ViewAandB({
modelA: modelInstances.modelA,
modelB: modelInstances.modelB,
});
viewAandB.render();
If either modelA or modelB were not created, I receive an exception in the View's initialize method (read code comments below):
Views.ViewAandB = Backbone.View.extend({
initialize: function(options) {
var self = this;
// Even if models are undefined, this is OK.
this.modelA = options.modelA;
this.modelB = options.modelB;
_.bindAll(this, 'render');
// This fails if models are undefined.
this.bidderModel.bind('change', this.render);
this.buyerModel.bind('change', this.render);
},
});
I could surround the the code which creates the view with a condition, but this feels messy:
if (modelInstances.modelA && modelInstances.modelB) {
var viewAandB = new Views.ViewAandB({
modelA: modelInstances.modelA,
modelB: modelInstances.modelB,
});
viewAandB.render();
}
I could check inside the view's initialize and render methods, but this too feels messy since it must be duplicated in any method using the view's model:
initialize: function(options) {
var self = this;
var haveModels = (options.modelA && options.modelB);
if(!haveModels) {
// No data.
return;
}
// Even if models are undefined, this is OK.
this.modelA = options.modelA;
this.modelB = options.modelB;
_.bindAll(this, 'render');
// This fails if models are undefined.
this.bidderModel.bind('change', this.render);
this.buyerModel.bind('change', this.render);
},
Question: How should I check the necessary models are created when rendering 'optional' views?
Related
I have a backbone application and a RESTful api. I used the sample created by Coenraets to understand the architecture of a backbone app, but I decided to setup my own structure and just use the data for testing.
I want to know the best way to return data from the RESTful api. I currently have my app folder structure setup with model, collection, view and service folders. I have a node server running with express that handles the backend and is working fine.
What I want to know is what is the best practice for accessing the restful data api? Should I do that in my service class or in my view class? How do I make this work dynamically using the returned data from my restful api: http://localhost:3000/employees
It seems like there are many ways to do this and for now I just want something that works, but eventually I do want to know what is the best way to do it. Ultimately I want to have a CRUD setup. But I'm not sure where that should be setup. Similar to what is detailed here: http://www.codeproject.com/Articles/797899/BackBone-Tutorial-Part-CRUD-Operations-on-Backbone
My files are as follows:
employeecolletion.js
var Backbone = require('backbone');
var Employee = require('../models/employeemodel.js');
module.exports = Backbone.Collection.extend({
model: Employee,
url:"http://localhost:3000/employees"
});
employeemodel.js
var Backbone = require('backbone');
var EmployeeCollection = require('../collections/employeecollection.js');
module.exports = Backbone.Model.extend({
urlRoot:"http://localhost:3000/employees"
// initialize:function () {
// this.reports = new EmployeeCollection();
// //this.reports.url = this.urlRoot + "/" + 1 + "/reports";
// }
});
employee.js (employee view that binds to my template)
var fs = require('fs');
var base = require('./base.js');
var EmployeeList = require('../collections/employeecollection.js');
var employeeService = require('../services/employeeService.js');
var template = fs.readFileSync('app/templates/employee.mu', { encoding: 'utf8' });
module.exports = base.extend({
el: '.view',
template:template,
collection: employeeService.collection,
initialize: function () {
this.viewModel = {
employee_list: this.collection.toJSON()
//employee_list: this.collection.fetch() --HERE I EXPERIMENTED WITH FETCHING THE DATA
};
this.render();
}
});
employeeservice.js (file in service folder that would ideally return the collection which I would just bind to my template in they employees view file)
var EmployeeCollection = require('../collections/employeecollection.js');
//if wanting to pass in data manually
var employee_list = [
{
id:1,
firstName:"James",
lastName:"King",
fullName:"James King",
managerId:0,
managerName:"",
title:"President and CEO",
department:"Corporate",
cellPhone:"617-000-0001",
officePhone:"781-000-0001",
email:"jking#fakemail.com",
city:"Boston, MA",
pic:"james_king.jpg",
twitterId:"#fakejking",
blog:"http://coenraets.org"
}
];
//HERE I WAS EXPERIMENTING WITH A DIFFERENT SYNTAX TO DO THE FILTERING BY ID
//IN MY SERVICE AND SIMPLY RETURNING THE FINAL DATA I WANT TO MY VIEW CLASS
// var employees = new EmployeeCollection({id: id});
// employees.fetch({
// success: function (data) {
// console.log(data);
// }
// });
module.exports = {
collection: new EmployeeCollection(employee_list)
};
Backbone is meant for RESTful services.
I'll try to explain the basics using some easy to understand terms.
So backbone is based on models and views.
The model is responsible to the data.
That means, that the model is the one who fetches the data from the server and stores it.
In an interactive application, the model should have a url or urlRoot properties which indicate what is the url of the specific resource this model refers to.
For example, if we had a Person resource, and assuming we are consuming a standard RESTfult service, I would expect something similiar to this:
var Person = Backbone.Model.extend({
url : 'http://localhost:3000/api/Person'
});
That actually lets us create new instances of this model and manipulate it.
This url will be used by the model for all CRUD operations related to it.
For example, if we now create a new instance:
var person = new Person();
We now have the following basic CRUD operations:
fetch: this method is executing an async AJAX GET request behind the scenes, and injects the data into the model.
Now, after we fetched the data, we can use it by simply calling get:
person.get('name'); * assuming there's a name property.
save this method is exectuing an async AJAX POST or PUT request behind the scene.
If the model's idAttribute is undefined, it will executed POST, otherwise PUT. The idAttribute is a model property which indicates what is the model's unique id.
A sample usage:
person.set({name : 'Mor'});
person.save();
The abvoe will execute a post request with the name: 'Mor' in the request body.
If for example I fetched the model, and already have an idAttribute assigned, calling the same save method will use the PUT request.
destroy this method will execute a DELETE request behind the scene.
Sample usage: person.destroy();.
Obviously I have just shown you the basic usages, there's a lot more options out there.
A collection is simply a list of models so there's not much to explain, you can read more here: http://backbonejs.org/#Collection
A view is all you see. It is the visual part of the application.
What Backbone lets us do, is to bind views to models and collections.
By that, we can create some dynamic content and visuals.
A basic view would like something like that:
var PersonView = Backbone.View.extend({
el: '.person',
initialize: function(){
this.listenTo(this.model, "change", this.render);
},
render: function(){
this.$el.html("hello :"+this.model.get("name"));
}
});
As you can see, I used listenTo. It is an event listener that calls render each time the model changes.
When I refer to this.model I refer to a model I will pass to the view when I initiate it:
var view = new View({ model : person});
By that, and since I used listenTo, my view is now binded with the person model.
This is basically it.
Obviously, there's a lot more to learn and understand, but this pretty much covers the basics.
Please refer to http://backbonejs.org/ and read some more information.
How do you call multiple models in a single view
Usual case is that you create a backbone model
var myModel = Backbone.Model.extend({ url:"api/authentication"});
and then assign it to a view, making it the default model of a certain view
var LoginView = Backbone.View.extend({
model: new myModel(),
initialize: function(){
_.bindAll(this, 'render', 'render_thread_summary', 'on_submit', 'on_thread_created', 'on_error');
this.model.bind('reset', this.render);
this.model.bind('change', this.render);
this.model.bind('add', this.render_thread_summary);
},
...
});
But I have many methods in a single view that calls different models. Currently I just create an instance of a model inside an event method.
The other solution that I'm thinking would be like this
initialize: function(){
_.bindAll(this, 'render', 'render_thread_summary', 'on_submit', 'on_thread_created', 'on_error');
new.model1().bind('reset', this.render);
new.model2().bind('change', this.render);
new.model3().bind('add', this.render_thread_summary);
},
But are these good practices? What else are the other way to do this. Thanks.
UPDATE
In response to Peter, here's my actual code
var userRegModel = Backbone.Model.extend({url: fn_helper.restDomain()+'user/reg'});
var userResendActivationModel = Backbone.Model.extend({url: fn_helper.restDomain()+'user/token/activation/new'});
var userResetPassModel1 = Backbone.Model.extend({url: fn_helper.restDomain()+'user/pwdreset/token/new'});
var userResetPassModel2 = Backbone.Model.extend({url: fn_helper.restDomain()+'user/pwdreset/change'});
var userLoginModel = Backbone.Model.extend({url: fn_helper.restDomain()+'user/login'});
_view: Backbone.View.extend({
el: 'div#main-container',
initialize: function(){},
events: {
'click #user-signup': 'signupUserFn', //userRegModel
'click a#activation-resend': 'resendActivationFn',//userResendActivationModel
'click #reset-password-submit1': 'execResetPasswordFn', //userResetPassModel1
'click #reset-password-submit2': 'execResetPasswordFn', //userResetPassModel2
Each models declared above corresponds to different methods declared in events
Normally model instances should be passed to views in the options argument to the initialize method.
Generally models represent data that either is currently or will eventually be stored in database somewhere. Now, sometimes you want to create a model just for managing ephemeral browser state, which is fine, but the common case is that most read/edit/delete views don't create models unless that's their primary purpose like a "Post New Comment" form. So normally if your view has to show some models, allow them maybe to be updated and saved, they should come into the view in the options argument to the initialize function.
However, if your view really needs so many models and nothing else in your system cares about them, just create them during initialize and store them directly as properties on this (your view instance) and off you go.
If you post a more complete code sample, we can give specific recommendations, but as it is it looks like if you have a LoginView and you need more than 1 model for it, you might be off in the weeds design-wise.
Part Deux
OK, so it looks like you think every API endpoint needs a model. Just implement those as methods on a single model class that use Backbone.sync with the appropriate url option.
var Model = Backbone.Model.extend({
resendActivation: function () {
return Backbone.sync.call('update', this, {url: fn_helper.restDomain()+ 'user/token/activation/new'});
}
});
You may have to do some manual triggering of events and I'm not sure what your API returns, but fundamentally these are all dealing with a single user record and can be handled by a single model.
Misc unrelated tips:
_.bindAll(this); //don't bother listing methods out
this.model.on('reset change', this.render); //bind multiple events at once
JavaScript by vast majority convention use camelCase not lower_with_underscores
I have my views correctly displaying fake information and so I am now trying to apply asynchronus data loading to retrieve actual data. The problem is that I am uncertain about how I should go about this. Should I create AJAX calls myself? Should I use the Socket API? Should I use the built in REST api (and how to do so asynchronously)? The server side handler is still unimplemented so as far as how the server serves up the data, that is completely flexible.
i doubt your own ajax calls is what is needed here...
i can't tell about sockets however i know it is possible and a solid idea depending on your app.
i have been using the default REST functionality and it works well for me,
a small example as how I would do it,
to make it less complex i will just act as if it is from page load, instead of using routers and all.
var myView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
var v = this;
this.model.bind("change", function(e) {
this.render();
});
},
render: function() {
this.el.empty();
this.el.text(this.model.get('name'));
}
});
var myModel = Backbone.Model.extend({
url: "/api/myModel", // change to your server code...
defaults: {
name: "john"
}
});
$(function(){
var m = new myModel({}); // dummy model
var v = new myView({ model: m, el: $('#myDiv')});
v.render();
m.fetch(); // takes the url of the model or collection and fetches it from the server side ...
});
if you want to test what the fetch would do, you can for try this code from your console, (or add it to the jquery document load function:
m.set({ name: 'peter' });
this changes the model's property 'name' and you will immediately see the view update itself, because it listens to the change event of the model.
more info on these events can be found here: http://documentcloud.github.com/backbone/#Events
I am working on my first project using backbone.js. It should be a frontend to a Play! App with a JSON interface. Here's a part of my JS code
var api = 'http://localhost:9001/api'
// Models
var Node = Backbone.Model.extend();
// Collections
var Nodes = Backbone.Collection.extend({
model: Nodes,
url: api + '/nodes',
});
// Views NODE
var NodeView = Backbone.View.extend({
el: $("#son_node_elements"),
render: function(){
var source = $("#son_node_item_templ").html();
var template = Handlebars.compile(source);
$(this.el).append(template(this.model.toJSON()));
return this;
}
});
var NodeListView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "render");
this.collection = new Nodes();
this.collection.bind("change",this.render);
this.collection.fetch();
this.render();
},
render: function(){
_(this.collection.models).each(function(item){
var nodeView = new NodeView({model: item});
$(this.el).append(nodeView.render().el);
}, this);
}
});
Nodes = new NodeListView({el:$('#son_nodes')});
My Problem is that when this.render() is called, this.collection.fetch() is still not done and this.collection does not contain anithing to render. All works fine when I set a breakpoint at this.render() (for example using firebug), so this.render() is not called immediately. I get exactly the same result when I access a local JSON file instead of the api of my app. Any suggestions how to handle this issue?
Fetch can also be called from outside view, so let the view listen for that instead:
this.collection.bind("reset",this.render);
Reset event will be triggered on every call to this.collection.fetch();
And finally, skip this.render(); Don't call this yourself, since reset event handler do this for you.
You need to call "render" inside the "success" callback for your "fetch()":
this.collection.fetch({
success: function(col) {
col.render();
}
});
That defers the rendering until the collection fetching is complete. It's all asynchronous.
Caveat: I barely know anything about Backbone in particular, but something along these lines is definitely your problem. In other words, there may be other things to worry about that I haven't mentioned (because I don't know what they are :-).
Is there any sort of hooks in backbone where I can easily say "whenever any of the collections is fetching data, show the spinner, hide it when they're done"?
I have a feeling it will be more complicated than that and require overwriting specific functions. When should I show the spinner? On fetch() or refresh() or something else?
You can use jQuery ajaxStart and ajaxStop. Those will globally run when an ajax request is made, so fetch and save will cause those to run. Add your code to show the spinner in the start and hide it in the end.
in Backbone.js 1.0.0 you can use the request and sync events http://backbonejs.org/#Events-catalog
This goes in the view.
initialize: function(){
this.items = new APP.Collections.itemCollection();
this.items.bind('request', this.ajaxStart, this);
this.items.bind('sync', this.ajaxComplete, this);
}
ajaxStart: function(arg1,arg2,arg3){
//start spinner
$('#item-loading').fadeIn({duration:100});
},
ajaxComplete: function(){
$('#item-loading').fadeOut({duration:100});
}
This can be applied per collection or per model here's some CSS for the spinner http://abandon.ie/notebook/simple-loading-spinner-for-backbonejs
Backbone doesn't trigger any event when Collection::fetch() starts (see source code), so you will have to override the fetch method. Maybe something like this:
var oldCollectionFetch = Backbone.Collection.prototype.fetch;
Backbone.Collection.prototype.fetch = function(options) {
this.trigger("fetch:started");
oldCollectionFetch.call(this, options);
}
This will override the fetch method to give you an event when the fetch starts. However, this only triggers the event on the specific collection instance so if you have a bunch of different collections you'll have to listen for that event on each collection.
The way i have done this without overriding backbone is:
In view
var myView = Backbone.View.extend({
initialize; function(){
this.$el.addClass('loading');
collection.fetch(success:function(){
this.$el.removeClass('loading')
})
}
})
The other route would be to remove the loading class when the models are added, usually you have:
var myView = Backbone.View.extend({
initialize; function(){
_.bindAll(this, 'addAll')
collection.bind('reset', this.addAll)
this.$el.addClass('loading');
collection.fetch();
},
addAll: function(){
this.$el.removeClass('loading');
collection.each(this.addOne);
}
})
These would be almost identical in most cases, and as the loader is really for the users experience removing it just prior to displaying the content makes sense.
And a little update. Since Dec. 13, 2012 have been added a "request" event to Backbone.sync, which triggers whenever a request begins to be made to the server. As well since Jan. 30, 2012 have been added a "sync" event, which triggers whenever a model's state has been successfully synced with the server (create, save, destroy).
So, you don't need to override or extand the native Backbone's methodes. For listening 'start/finish fetching' event you can add listener to your model/collection like this for example:
var view = new Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, 'request', this.yourCallback); //start fetching
this.listenTo(this.model, 'sync', this.yourCallback); //finish fetching
}
});
You can create a method called sync on any of your models, and backbone.js will call that in order to sync. Or you can simply replace the method Backbone.sync. This will allow you to make the change in only one place in your source code.
I have used NProgress in my backbone and it is the best functioning loader/spinner out there.
var view = Backbone.View.extend({
initialize: function () {
this.items = new APP.Collections.itemCollection();
this.items.on('reset', this.myAddFunction, this);
NProgress.start();
collection.fetch({
reset:true,
success: function () {
NProgress.done(true);
}
});
}
});
Use Backbone sync method,
It will call every time backbone sync method, not only fetch, save, update and delete also
/* over riding of sync application every request come hear except direct ajax */
Backbone._sync = Backbone.sync;
Backbone.sync = function(method, model, options) {
// Clone the all options
var params = _.clone(options);
params.success = function(model) {
// Write code to hide the loading symbol
//$("#loading").hide();
if (options.success)
options.success(model);
};
params.failure = function(model) {
// Write code to hide the loading symbol
//$("#loading").hide();
if (options.failure)
options.failure(model);
};
params.error = function(xhr, errText) {
// Write code to hide the loading symbol
//$("#loading").hide();
if (options.error)
options.error(xhr, errText);
};
// Write code to show the loading symbol
//$("#loading").show();
Backbone._sync(method, model, params);
};