I'm new to all the backend side of things, so hopefully this is one of the questions I'm gonna look back to and think "man, that was silly of me". Let me start. I want to make an application where you can draw rectangles on a screen.
Every rectangle is a model represented by an ItemView and every screen is a collection represented by a CollectionView. Rectangles are added to the collection once they are created by the user, dynamically.
When the user clicks on a "new screen" button, the following takes place
MyController = Backbone.Marionette.Controller.extend({
initialize:function() {
i=0;
},
newScreen: function() {
i=i+1;
this.collection = new Rectangles([],{id: i});
routestring = "screen/"+i;
router.navigate(routestring);
canvas.show(new ScreenView({collection: this.collection}));
},
...
I use an iterator for the different screens so that my backend looks like this
demo.firebaseio.com/screens/1
I am using Firebase's backbone bindings but I have absolutely no idea on how I can access a collection that's already stored on the server. The reason is that I want to be able to navigate through the different screens, fetch the collection from that url, and render it...
Here's my collection code
Rectangles = Backbone.Collection.extend({
initialize: function(models, options) {
this.id = options.id;
},
url: function() {
return 'https://demo.firebaseio.com/screen/'+this.id;
},
model: Rectangle,
firebase: function() {
return new Backbone.Firebase("https://demo.firebaseio.com/screen/"+this.id)
}
});
and here's my router code!
var Router = Backbone.Marionette.AppRouter.extend({
controller: myController,
appRoutes: {
'': 'newScreen',
'screen/:number': 'doSomething'
}
})
I'm open to any suggestions, and I do understand that I might have done something horribly wrong!
The following should work:
Rectangles = Backbone.Collection.extend({
firebase: new Backbone.Firebase("https://demo.firebaseio.com/screens/"+this.id),
});
However, you'll need to call Collection.sync() to initially fetch the data. The alternative is to use a truly realtime collection in the form of Backbone.Firebase.Collection:
Rectangles = Backbone.Firebase.Collection.extend({
firebase: new Firebase("https://demo.firebaseio.com/screens/"+this.id),
});
In this case you won't have to call fetch/sync/save etc. - the collection is always kept in sync with Firebase. There's more documentation on how this works here: http://firebase.github.io/backfire/
Related
I'm working on a webapp to teach myself Ember, and I've walked into one large issue:
The page halts while it is attempting to fetch json, and my IndexRoute and IndexController feel very bloated. Additionally, this.store.find('pokemon') uses the RESTAdapater, and can freeze the page from rendering anything (besides the loader) for up to 1.5 seconds.
App.IndexRoute = Ember.Route.extend({
model: function() {
var store = this.store;
return Ember.RSVP.hash({
pokeballs: App.Pokeball.all(),
pokemon: store.find('pokemon'),
status: App.Status.all(),
levels: App.Levels
});
}
});
Updated Question: As it is now, my IndexController is larger than I would like, and is acting as a mediator for the pokeballs and pokemon collections. I am thinking it would be a good idea to split up IndexController so that I have an IndexController, a PokemonListController, and a PokeballListController. The problems I have are:
How should I populate the content of the PokemonListController and PokeballListController if I am on '/', which maps to the IndexRoute?
Is this actually a good idea, am I treating controller's they way they are intended to be treated?
Webapp Demo: http://theirondeveloper.github.io/pokemon-catch-rate
Github: https://github.com/TheIronDeveloper/pokemon-catch-rate
On one hand you are not tied to a single controller in a route, there is generally only a single controller associated with a route, but you can always set more controllers if you need them to, remember they are decorators of your models.
App.IndexRoute = Ember.Route.extend({
model: function() {
return store.find('pokemon');
},
setupController: function(controller, model) {
var pokemonListController = this.controllerFor('pokemons');
var pokeballListController = this.controllerFor('pokeball');
controller.set('model', model); //this would be the index controller
pokemonListController.set('model', model.pokemon);
pokeballListController.set('model', model.pokeballs);
}
});
Also you can render your page if you need to, without waiting for the responses, Ember will handle updating your UI once the response is received. if your response is too slow, the user will see the page, and an empty list (in this case, empty list of pokemon), and then once the request is resolved, the list will fill up with it.
To do that, just return an empty array from your model hook, and update it async:
App.IndexRoute = Ember.Route.extend({
model: function() {
var pokemon = [];
var store = this.store;
store.find('pokemon').then(function(allPokemon) {
pokemon = allPokemon; //untested, you may need to push them instead
});
return Ember.RSVP.hash({
pokeballs: App.Pokeball.all(),
pokemon: pokemon,
status: App.Status.all(),
levels: App.Levels
});
}
});
Not seeing anything "bloated" about your IndexRoute or IndexController. It is true that a lot of Ember apps will have multiple routes and thus multiple controllers, but that happens when it makes sense to switch to other routes. If it doesn't make sense for your application - then what you have is great.
If you have multiple routes (and thus multiple controllers), the approach #Asgaroth suggested will work great for setting multiple controllers. Otherwise, if you only have a single route - there is really no need to have multiple controllers.
The fact that your data gets fetched and that takes some time is normal. Now, ideally this (data fetching) should only happen once and your data would then get cached and as you peruse around your other routes (which you currently do not have) your data would already be available to you without any extra penalty.
If you do need to have multiple controllers and are wondering how to communicate between them, you are probably looking for the needs API outlined here.
UPDATE
I took another look at the model hook and it is weird how 3 out of 4 things in there are not promises at all and don't look like they belong in there.
So, here is how you can clean that up.
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('pokemon');
}
});
That's the only thing that belongs in there. The other properties might as well be properties on your controller, as in:
App.IndexController = Ember.Controller.extend({
levels: function(){
return App.Levels;
}.property(),
pokeballs: function(){
return App.Pokeball.all()
}.property(),
status: function(){
return App.Status.all();
}.property(),
Of course, you would then need to change references to those properties in your template and other code. So, for example, you would change from model.pokeballs to just pokeballs. You would also change from model.pokemon to just model
I submitted a pull request to show you the way I did this - see here
Not a full answer, but to reveal the magic between the route and controller ... here is how the model gets drop'd into the controller instance for you
App.IndexRoute = Ember.Route.extend({
model: function() {
return store.fin('pokemon');
},
setupController: function(controller, model) {
//the model that gets returned from the above method is added to the controller instance for you in this generated method on the route
controller.set('model', model); //also alias'd as content in older versions of ember
}
});
This is code from an Angular introduction video series, which explains how to populate angular controllers with data from persisted memory, but stops just short of explaining how to add the new product reviews to the persisted memory.
There do seem to be some articles explaining how to do this, but since I am very new to angular, I'm afraid I couldn't understand any of them.
I have figured out the syntax for making post requests using $http, but I don't see how to fit that code into the existing structure, so that it will 1) be called when pushing a new element to the reviews array, and 2) update the view when completed.
I am interested to learn a basic way to add the new product review to persistent memory.
(function() {
var app = angular.module('gemStore', ['store-directives']);
app.controller('StoreController', ['$http', function($http){
var store = this;
store.products = [];
$http.get('/store-products.json').success(function(data){
store.products = data;
});
}]);
app.controller('ReviewController', function() {
this.review = {};
this.addReview = function(product) {
product.reviews.push(this.review);
this.review = {};
};
});
})();
The JSON looks like this:
[
{
"name": "Azurite",
"description": "...",
...
"reviews": []
},
...
]
If the store-products.json is just a file on the server, you'll need an actual backend implementation (in PHP, nodejs, etc.) to actually update the file (or more typically just return the content from the database).
Normally you would make a save method and not post on every modification, though. But, in either case, depending on your backend, usually the implementation is as simple as $http.put('/store-products', store.products) whenever you click a "save" button. Typically, the put can return the same data, so there's typically no need to update the view since you just set it exactly to your state. But, if you have possibility of concurrent editing, and the put returns the modified data, it would look like your get:
$http.put('/store-products', store.products).success(function(data){
store.products = data;
});
For adding an item, it might almost identical, depending on your data model:
$http.post('/store-products', newProduct).success(function(data){
store.products = data;
});
In this case the POST gives an item to add and returns all of the products. If there are a lot of products -- that is, products are more like a large database than a small set in a "document", then the post would more typically return the added item after any server processing:
$http.post('/store-products', newProduct).success(function(newProductFromServer){
store.products.push(newProductFromServer); //if newProduct wasn't already in the array
//or, store.products[newProductIdx] = newProductFromServer
});
If you really wanted to call this function on every modification instead of a save button, you can use a watch:
$scope.$watchCollection(
function() { return store.products; },
function() { /* call the $http.put or post here */ }
}
I am trying to wrap my head around Backbone, more specifically how the an application flows throughout it's life. Unfortunately at my job I do not have access (or say for that matter) on how our API is structured. We have many different calls from different time periods with crazy inconsistent structure.
Overriding fetch or sync is not a problem to standaraize the return but what I run into (at the very beginning of my dive in the a Backbone application) is a how to layout the actual code.
Here is my real world example. This page is non-critical and I am trying to re-write it with Backbone. Here is the flow:
Page loads a list of genre types from a call
Clicking on a genre type loads sub genres based off of the genre type (the sub genre type requres a genre code as the parameter)
Clicking on the sub genre type loads all products with that criteria.
I can get pretty far but at some point I feel the code is getting mangled - or doesn't feel natural. Like I am shoving things in.
So my official questions is: How do I manage a Backbone app?
Here is a summary of my though process:
I created a global namespace as one should
var App = App || {};
Okay, lets start with the main application view as all examples show:
App.MainView = Backbone.View.extend({
//this loads the outer stuff
//and creates an instance of the Genre View
});
Alright pretty straightforward, I am going to need a genre model, collection, and view (this applies to sub genre as well)
App.Genre = Backbone.Model.extend();
App.Genres = Backbone.Collection.extend({
url: 'returns a list of genres',
model: App.Genre,
initialize: function() {
this.fetch();
},
fetch: function() {
var self = this;
$.ajax({
url: this.url,
success: function(response) {
**format return**
self.add(formattedArrayOfModels);
}
});
}
});
Now on to the view, the confusing part
App.GenreView = Backbone.View.extend({
el: 'element',//easy enough
tmpl: 'my handlebars template',//implementing handlebars...no problem
initialize: function() {
//this produces a collection full of genres
this.genreList = new App.Genres();
this.genreList.on('add', _.bind(this.render, this));
},
render: function() {
//rendering not a problem, pretty straight forward
}
});
Up until here I have no problems. The genre list loads and we're good to go. So, now when the user clicks a genre I want it to load a sub genre
events: {
'click a': 'getSubGenres'
},
getSubGenres: function(e) {
}
Here is my problem. In getSubGenres do I keep it local?
var subGenre = new App.SubGenreView();
Or should I make it part of the Genre view?
this.subGenre = new App.SubGenreView();
Should I somehow put it in a parent object so it can be accessed by other views? How do I control things like that?
And if I already have a collection of sub genres how do I just use the loaded collection (instead of another ajax call).
Is this the approach you would use?
couple of things before I answer,
first: the fetch function doesn't need an $ajax call since it's its job, so, you can evaluate error:function(){} and success:function(){} immediately inside fetch, but that's assuming that the URL is set correctly.
second: one thing that helped me a lot in my backbone keyboard-head-fight is the addy osmani Backbone Fundamentals which contains a very rich tutorial in pdf format.
now back to the question: from my experience, you will mostly need 'this', so it's a good habbit to get used to it, plus there is something that solves a lot of these issues if implemented correctly: backbone layoutmanager
anyway, the decision of where to place the subview, is totally a design decision in your case and depends a lot on how you structure your page and files.
about how to use the "collection" that is preloaded: I really didn't get it, because the collection you're talking about contains all the subgenres, so usually it shouldn't change even if the view changes to a certain genre view, you are still able to use it.
but still everything I said, is relative to how you structure your files, I do an app.js and a router.js and lots of other files, but the main work is always on the main two, so basically I always get access to everything.
I hope this answered your question
I am cleaning up a multi-page app of 65+ html pages and a central javascript library. My html pages have a ton of redundancies and the central js library has become spaghetti. I face limitations on consolidating pages because I am working within a larger framework that enforces a certain structure. I want to reduce the redundancies and clean up the code.
I discovered backbone, MVC patterns, microtemplating and requirejs, but they seem best for single page applications. Somehow I need to let the main module know what page is being loaded so it will put the right elements on the page. I am thinking of passing in the title of the html which will turn grab the correct collection of page elements and pass them into App.initialize as an object.
1) Can anyone validate this approach? If not are there alternate approaches recommended? How about extensions to backbone like marionette?
2) Can anyone recommend a means to get page specifics into the backbone framework?
Following backbone tutorials I built a successful test page with a main.js that calls an App.initialize method that calls a view.render method. My first thought is to read the html page title and use it to select a model for the specific page being loaded. I'd have to pass in an object with the specifics for each pages layout. Here's the view's render method so you can see what I am trying to do:
render: function () { // pass parameter to render function here?
var data = new InputModel,
pageTitle = data.pageTitle || data.defaults.pageTitle,
compiled,
template;
var pageElements = [
{ container: '#page_title_container', template: '#input_title_template' },
{ container: '#behavior_controls_container', template: '#behavior_controls_template' },
{ container: '#occurred_date_time_container', template: '#date_time_template' }]
for (var i = 0; i < pageElements.length; i++) {
this.el = pageElements[i].container;
compiled = _.template($(InputPageTemplates).filter(pageElements[i].template).html());
template = compiled({ pageTitle: pageTitle });
//pass in object with values for the template and plug in here?
$(this.el).html(template);
}
}
Your help will be greatly appreciated. I am having a lot of fun updating my circa 1999 JavaScript skills. There's a ton of cool things happening with the language.
Using the document title to choose the loaded scripts sounds a tad kludge-y. If it works, though, go for it.
Another idea worth exploring might be to utilize Backbone.Router with pushState:true to setup the correct page. When you call Backbone.history.start() on startup, the router hits the route that matches your current url, i.e. the page you are on.
In the route callback you could do all the page-specific initialization.
You could move the template and container selection out of the view into the router, and set up view in the initialize() function (the view's constructor). Say, something like:
//view
var PageView = Backbone.View.extend({
initialize: function(options) {
this.model = options.model;
this.el = options.el;
this.title = options.title;
this.template = _.template($(options.containerSelector));
},
render: function() {
window.document.title = title;
var html = this.template(this.model.toJSON());
this.$el.html(html);
}
});
Handle the view selection at the router level:
//router
var PageRouter = Backbone.Router.extend({
routes: {
"some/url/:id": "somePage",
"other/url": "otherPage"
},
_createView: function(model, title, container, template) {
var view = new PageView({
model:model,
title:title
el:container,
templateSelector:template,
});
view.render();
},
somePage: function(id) {
var model = new SomeModel({id:id});
this._createView(model, "Some page", "#somecontainer", "#sometemplate");
},
otherPage: function() {
var model = new OtherModel();
this._createView(model, "Other page", "#othercontainer", "#othertemplate");
}
});
And kick off the application using Backbone.history.start()
//start app
$(function() {
var router = new PageRouter();
Backbone.history.start({pushState:true});
}
In this type of solution the view code doesn't need to know about other views' specific code, and if you need to create more specialized view classes for some pages, you don't need to modify original code.
At a glance this seems like a clean solution. There might of course be some issues when the router wants to start catching routes, and you want the browser to navigate off the page normally. If this causes serious issues, or leads to even bigger kludge than the title-based solution, the original solution might still be preferrable.
(Code examples untested)
I am working on a single-page app using Backbone.js. An issue that has occurred to me is that since one is not reloading the page, that when one creates a instance of a View, then I assume, that the View object will remain in memory for the life of the app. This does not seem very efficient to me, since a particular view may no longer be needed if another route is called. However, a particular View may later need to be 'displayed' if one returns to that original route. So the question is, how to best manage views in Backbone with regards to routes?
In my app, many of the views are responsible for displaying a particular 'page' and as such share the same DOM element. When one of these 'page' views is called, it will replace the content in the DOM element previously put in place by the previous view. Thus the previous view is no longer needed.
Do I need to somehow manually destroy the previous View (or is this somehow handled by the Router object)? Or is it better to leave the views once they have been initialized?
Following sample code shows how views instances are being creating in the Router in the app.
/**
* View - List of contacts
*/
var ListContactsView = Backbone.View.extend({
el: '#content',
template: _.template($('#list-contacts-tpl').html()),
initialize: function() {
_.bindAll(this, 'render');
this.collection = new Contacts();
this.collection.bind('reset', this.render);
this.collection.fetch();
},
render: function() {
this.$el.hide();
this.$el.html(this.template({ contacts: this.collection }));
this.$el.fadeIn(500);
}
});
/**
* View - Display single contact
*/
var DisplayContactView = Backbone.View.extend({
el: '#content',
events: {
'click #delete-contact-button': 'deleteContact'
},
template: _.template($('#display-contact-tpl').html()),
initialize: function() {
_.bindAll(this, 'deleteContact', 'render');
// Create reference to event aggregator object.
if (typeof this.options.id === 'undefined') {
throw new Error('View DisplayContactView initialized without _id parameter.');
}
this.model = new Contact({ _id: this.options.id });
// Add parse method since parsing is not done by collection in this
// instance, as this model is not called in the scope of collection
// Contacts.
this.model.parse = function(response) {
return response.data;
};
this.model.bind('change', this.render);
this.model.fetch();
},
deleteContact: function(id) {
// Trigger deleteContact event.
this.eventAggregator.trigger('deleteContact', id);
},
render: function() {
this.$el.html(this.template({ contact: this.model.attributes }));
}
});
/**
* Page routes
*/
var $content = $('#content');
var ClientSideRouter = Backbone.Router.extend({
routes: {
'browse': 'browse',
'browse/view/:id': 'browseViewContact',
'orgs': 'orgs',
'orgs/:orgName': 'orgs',
'orgs/:orgName/:id': 'orgs',
'contact/add': 'addContact',
'contact/view/:id': 'viewContact',
'contact/delete/:id': 'confirmDelete',
'*path': 'defaultPage'
},
addContact: function() {
// Display contact edit form.
var editContactFormView = new EditContactFormView();
// Display email field in edit form.
},
browse: function() {
var listContactsView = new ListContactsView();
},
browseViewContact: function(id) {
var displayContactView = new DisplayContactView({ id: id });
},
defaultPage: function(path) {
$content.html('Default');
},
home: function() {
$content.html('Home');
},
viewContact: function(id) {
$.ajax({
url: '/contact/view/' + id,
dataType: 'html',
success: function(data) {
$content.html(data);
}
});
}
});
var clientSideRouter = new ClientSideRouter();
Backbone.history.start();
Routes do not destroy views
Routes provide you convenient manner to interact with url changes. By convenience i mean url semantics and context of current page. For example url #/!/create/ will invoke a method that should display a form to create a model. Context here is the view to create model.
Views should be managed by the developer
there still does not exists a well known manner to manage views in Backbone.js, but i prefer the way of global variables. This would ensure your view instances are available throughout application and all the modules have access to them. For example doing this
window.App.Contacts.ContactView = new App.Contacts.View.ContactView({model:BenContact}); will make view used to display Ben's contact information available to application modules through window object. All you need to do for any views that use same el is to destroy the ContactView and render the new view.
You have methods on view to remove them
Undelegate Events and Remove methods help you remove them. Inside the callback method that handles routes hash change events. For example in the callback method that handles #/!/view/all ( url to view all the contacts list) you might come across situation where both the views now use the same el so you should destroy the ContactView and render ListView so in the callback do this
App.Contacts.ContactView.undelegateEvents();
App.Contacts.ContactView.remove();
Since Backbone.js has no built in support for view compositions, there are several patterns that you could follow when it comes to keeping track of child views.
Derick Bailey illustrates extending Backbone.View to allow views to
clean up after themselves -
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Another alternative is to add on child views to a property of the
parent view and manually clean them up when the parent view state is
removed.
var ParentView = Backbone.View.extend({
initialize : function(){
this.childViews = [];
},
render: function(){
this.childViews.push(new ChildView);
}
});
A third alternative is to make the child views subscribe to events
that the parent views trigger, so that they can clean up when the
parent view publishes a "close" event.
Also I noticed from your code that you are actually fetching a model within your child view class. Ideally, I would suggest passing the model as a parameter to the constructor as this decouples the view from the data. It's more MVC-ish