This is more of a conceptual question, in terms of using the backbone router and rendering views in backbone.
for the sake of an example (what I'm building to learn this with) I've got a basic CRUD app for contacts, with create form, a listing of all contacts, a contact single view and an edit form.
for simplicities sake I'm going to say that I would only want to see one of these things at a time. Obviously showing and hiding them with jQuery would be trivial, but thats not what I'm after.
I have two ideas,
1) trigger custom events from my router that removes all views and sends events that could be listened for in all views (triggering a close method ) and a main App view that then instantiates a specific view - ie :
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'addnew' : 'addNew',
'contacts/:id' : 'singleContact',
'contacts/:id/edit' : 'editContact'
},
index: function(){
vent.trigger('contactR:closeAll');
vent.trigger('contactR:index');
},
addNew: function() {
vent.trigger('contactR:closeAll');
vent.trigger('contactR:addNew');
},
singleContact: function(id) {
vent.trigger('contactR:closeAll');
vent.trigger('contactR:singleContact', id);
},
editContact: function(id) {
vent.trigger('contactR:closeAll');
vent.trigger('contactR:editContact', id);
},
});
(nb : vent is extending the backbone events obj so I can pub / sub )
2) or would / could / should I send a close all event and create an instance of the view in the router ?
Note I'm looking to achieve this without delving into additional libraries or frameworks like marionette etc.
You can use an utility object like this :
var ViewManager = {
currentView : null,
showView : function(view) {
if (this.currentView !== null && this.currentView.cid != view.cid) {
this.currentView.remove();
}
this.currentView = view;
return view.render();
}
}
and whenever you want to show a view use ViewManager.showView(yourView)
App.Router = Backbone.Router.extend({
routes: {
'' : 'index',
'addnew' : 'addNew',
'contacts/:id' : 'singleContact',
'contacts/:id/edit' : 'editContact'
},
index: function(){
var indexView ...
ViewManager.showView(indexView);
},
addNew: function() {
var addNewView ...
ViewManager.showView(addNewView);
},
singleContact: function(id) {
var singleContactView ...
ViewManager.showView(singleContactView);
},
editContact: function(id) {
var editContactView ...
ViewManager.showView(editContactView);
},
});
So it's the ViewManager that's responsible of rendering your views
Related
Is it possible to add a subrouter to a specific view? Lets say I have a Backbone app for multiple Cars. Right now, my Router would look like this
carRouter = Backbone.Router.extend({
routes: {
'': 'index'
'contact' : 'contact'
':car': 'car',
':car/gallery' : 'cargallery',
':car/specs' : 'specs',
':car/infos' : 'infos',
'about: 'aboutPage'
}
car: function(){
// do this
},
cargallery: function(){
// do this
},
specs: function(){
// do this
},
infos: function(){
// do this
}
...etc
});
that approach, obviously makes the whole page render, which I basically want to avoid. When I click on "gallery" and "specs" back and forth for example, the whole page re-renders on each click.
So, is it possible to do something like:
routes: {
'contact' : 'contact'
':car': 'car',
'about: 'aboutPage'
},
car: function(){
// new router containing
// ':car/gallery' : 'cargallery',
// ':car/specs' : 'specs',
// ':car/infos' : 'infos',
},
}
And then, on the Car page, I would have 3 tabs in the menu (gallery, specs, info), which will load the Model/collection of the specific car, without the page re-rendering?
Any help/suggestion is appreciated!
You can accomplish this with events,Backbone trigger and and custom EventHandler Object.
The events hash in your CarView :
events : {
"click .spec" : "showSpecs",
"click .gallery" : "showGallery",
"click .info" : "showInfo",
},
//Invoked when you click the show specs tab.
showSpecs : function(event) {
//Say that EventBus is your custom event handler
event.preventDefault();
MyApp.EventBus.trigger("show:spec",payload);
},
showGallery : function(event) {
event.preventDefault();
MyApp.EventBus.trigger("show:gallery",payload);
}
....
And in MyApp.EventBus :
_.extend(MyApp.EventBus,Backbone.Events);
MyApp.EventBus.showGallery = function(payload) {
// payload is an optional data that you can get from the triggered view
// Fetch your models or collection
// Instantiate the corresponding view and pass your data(models/collections) to it.
// Update the url to reflect the new view so that if you hit refresh you
// come to the same tab.
MyApp.router.navigate("/your-url",{trigger:false});
}
MyApp.EventBus.on("show:gallery",showGallery);
Also add a method in the router which can handle the refresh of tabs.
I have a backboneJS app that has a router that looks
var StoreRouter = Backbone.Router.extend({
routes: {
'stores/add/' : 'add',
'stores/edit/:id': 'edit'
},
add: function(){
var addStoresView = new AddStoresView({
el: ".wrapper"
});
},
edit: function(id){
var editStoresView = new EditStoresView({
el: ".wrapper",
model: new Store({ id: id })
});
}
});
var storeRouter = new StoreRouter();
Backbone.history.start({ pushState: true, hashChange: false });
and a model that looks like:
var Store = Backbone.Model.extend({
urlRoot: "/stores/"
});
and then my view looks like:
var EditStoresView = Backbone.View.extend({
...
render: function() {
this.model.fetch({
success : function(model, response, options) {
this.$el.append ( JST['tmpl/' + "edit"] (model.toJSON()) );
}
});
}
I thought that urlRoot when fetched would call /stores/ID_HERE, but right now it doesn't call that, it just calls /stores/, but I'm not sure why and how to fix this?
In devTools, here is the url it's going for:
GET http://localhost/stores/
This might not be the answer since it depends on your real production code.
Normally the code you entered is supposed to work, and I even saw a comment saying that it works in a jsfiddle. A couple of reasons might affect the outcome:
In your code you changed the Backbone.Model.url() function. By default the url function is
url: function() {
var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base;
return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
},
This is the function to be used by Backbone to generate the URL for model.fetch();.
You added a custom idAttribute when you declared your Store Model to be like the one in your DB. For example your database has a different id than id itself, but in your code you still use new Model({ id: id }); when you really should use new Model({ customId: id });. What happens behind the scenes is that you see in the url() function it checks if the model isNew(). This function actually checks if the id is set, but if it is custom it checks for that:
isNew: function() {
return !this.has(this.idAttribute);
},
You messed up with Backbone.sync ... lots of things can be done with this I will not even start unless I want to make a paper on it. Maybe you followed a tutorial without knowing that it might affect some other code.
You called model.fetch() "a la" $.ajax style:
model.fetch({
data: objectHere,
url: yourUrlHere,
success: function () {},
error: function () {}
});
This overrides the awesomeness of the Backbone automation. (I think sync takes over from here, don't quote me on that).
Reference: Backbone annotated sourcecode
I've implemented "change page" in my one page application with Backbone.js. However, I'm not sure if my Router should contain so much business logic. Should I consider go with Marionette.js to implement such functionality and make my Router thin? Should I worry about destroying Backbone models and views attached to "previous" active page/view when I change it (in order to avoid memory leaks) or it's enough to empty html attached to those models/views.
Here is my Router:
App.Router = Backbone.Router.extend({
routes: {
'users(/:user_id)' : 'users',
'dashboard' : 'dashboard'
},
dashboard: function() {
App.ActiveView.destroy_view();
App.ActiveViewModel.destroy();
App.ActiveViewModel = new App.Models.Dashboard;
App.ActiveViewModel.fetch().then(function(){
App.ActiveView = new App.Views.Dash({model: App.ActiveViewModel });
App.ActiveView.render();
});
},
users: function(user_id) {
App.ActiveView.destroy_view();
App.ActiveViewModel.destroy();
App.ActiveViewModel = new App.Models.User;
App.ActiveViewModel.fetch().then(function() {
App.ActiveView = new App.Views.UsersView({model: App.ActiveViewModel});
App.ActiveView.render();
});
}
});
Another approach:
Create an AbstractView
Having an AbstractView declared and then extending your other application specific View's from AbstractView has many advantages. You always have a View where you can put all the common functionalities.
App.AbstractView = Backbone.View.extend({
render : function() {
App.ActiveView && App.ActiveView.destroy_view();
// Instead of destroying model this way you can destroy
// it in the way mentioned in below destroy_view method.
// Set current view as ActiveView
App.ActiveView = this;
this.renderView && this.renderView.apply(this, arguments);
},
// You can declare destroy_view() here
// so that you don't have to add it in every view.
destroy_view : function() {
// do destroying stuff here
this.model.destroy();
}
});
Your App.Views.UsersView should extend from AbstractView and have renderView in place of render because AbstractView's render will make a call to renderView. From the Router you can call render the same way App.ActiveView.render();
App.Views.UsersView = AbstractView.extend({
renderView : function() {
}
// rest of the view stuff
});
App.Views.Dash = AbstractView.extend({
renderView : function() {
}
// rest of the view stuff
});
Router code would then change to :
dashboard: function() {
App.ActiveViewModel = new App.Models.Dashboard;
App.ActiveViewModel.fetch().then(function(){
new App.Views.Dash({model: App.ActiveViewModel }).render();
});
}
I have the following situation:
app.js: Singleton Marionette.Application() where I define a nav, a footer, and a main region. In the initializer I construct Marionette.Contoller's and attach them to the app's this.controller object for later control. I might not construct all the Controller's here, just the ones I want to Eagerly Load. Some are Lazy Loaded later. I also instantiate a Backbone.Router here, and pass in a reference to my app object:
var theApp = new TP.Application();
theApp.addRegions(
{
navRegion: "#navigation",
mainRegion: "#main",
footerRegoin: "#footer"
});
theApp.addInitializer(function()
{
// Set up controllers container and eagerly load all the required Controllers.
this.controllers = {};
this.controllers.navigationController = new NavigationController({ app: this });
this.controllers.loginController = new LoginController({ app: this });
this.controllers.calendarController = new CalendarController({ app: this });
this.router = new Router({ app: this });
});
**Controller.js: this is a general use controller that handles view & model intsantiation and eventing. Each Controller owns its own Marionette.Layout, to be filled into the App.mainRegion. Each Controller binds to the layout's "show" event to fill in the layout's regions with custom views. Each Controller offers a getLayout() interface that returns the controller's associated layout.
Marionette.Controller.extend(
{
getLayout: function() { return this.layout; },
initialize: function()
{
this.views.myView = new MyView();
...
this.layout.on("show", this.show, this);
...
},
show: function()
{
this.layout.myViewRegion.show(myView);
}
});
router.js: the router uses the app singleton to load a Controller's layout into the App's main region:
...
routes:
{
"home": "home",
"login": "login",
"calendar": "calendar",
"": "calendar"
},
home: function ()
{
var lazyloadedController = new LazyLoadController();
this.theApp.mainRegion.show(lazyLoadController.getLayout());
},
login: function (origin)
{
this.theApp.mainRegion.show(this.theApp.controllers.loginController.layout);
}
As it is, everything works fine except for reloading the same layout / controller twice. What happens is that the DOM events defined in the LoginView do not re-bind on second show. Which is easily solved by moving the LoginView initialization code into the "show" event handler for that Controller:
LoginController = Marionette.Controller.extend(
{
...
show: function()
{
if (this.views.loginView)
delete this.views.loginView.close();
this.views.loginView = new LoginView({ model: this.theApp.session });
this.views.loginView.on("login:success", function()
{
});
this.layout.mainRegion.show(this.views.loginView);
}
Now everything works fine, but it undoes part of the reason I created Controller's to begin with: I want them to own a View and its Models, create them once, and not have to destroy & recreate them every time I switch layouts.
Am I missing something? Is this not how I should be using Layouts? Isn't the whole point of Layouts and Regions that I can switch in & out Views at will?
Obviously I wouldn't jump back to LoginController/Layout often, but what about between a HomeController/Layout, CalendarController/Layout, SummaryController/Layout, etc... in a single page application I might switch between those 'top-level' layouts rather often and I would want the view to stay cached in the background.
I think your problem is that you don't maintain a single instance of the controller. The recommended way to handle routing/controllers (based on Brian Mann's videos) is like this
App.module('Routes', function (Routes, App, Backbone, Marionette, $, _) {
// straight out of the book...
var Router = Marionette.AppRouter.extend({
appRoutes: {
"home": "home",
"login": "login",
"calendar": "calendar"
}
});
var API = {
home: function () {
App.Controller.go_home();
},
login: function () {
App.Controller.go_login();
},
calendar: function () {
App.Controller.go_calendar();
}
};
App.addInitializer(function (options) {
var router = new Router({controller: API});
});
});
... and the controller:
App.module("Controller", function (Controller, App, Backbone, Marionette, $, _) {
App.Controller = {
go_home: function () {
var layout = new App.Views.Main();
layout.on('show', function () {
// attach views to subregions here...
var news = new App.Views.News();
layout.newsRegion.show(news);
});
App.mainRegion.show(layout);
},
go_login: function () {
....
},
go_calendar: function () {
....
}
};
});
I suspect your problem is the lazy-loaded controller...
I'm trying to learn Backbone by diving right in and building out a simple "question" app, but I've been banging my head against the wall trying to figure out how to use models and/or collections correctly. I've added the code up to where I've gotten myself lost. I'm able to get the collection to pull in the JSON file (doing "var list = new QuestionList; list.getByCid('c0') seems to return the first question), but I can't figure out how to update the model with that, use the current model for the view's data, then how to update the model with the next question when a "next" button is clicked.
What I'm trying to get here is a simple app that pulls up the JSON on load, displays the first question, then shows the next question when the button is pressed.
Could anyone help me connect the dots?
/questions.json
[
{
questionName: 'location',
question: 'Where are you from?',
inputType: 'text'
},
{
questionName: 'age',
question: 'How old are you?',
inputType: 'text'
},
{
questionName: 'search',
question: 'Which search engine do you use?'
inputType: 'select',
options: {
google: 'Google',
bing: 'Bing',
yahoo: 'Yahoo'
}
}
]
/app.js
var Question = Backbone.Model.Extend({});
var QuestionList = Backbone.Collection.extend({
model: Question,
url: "/questions.json"
});
var QuestionView = Backbone.View.extend({
template: _.template($('#question').html()),
events: {
"click .next" : "showNextQuestion"
},
showNextQuestion: function() {
// Not sure what to put here?
},
render: function () {
var placeholders = {
question: this.model.question, //Guessing this would be it once the model updates
}
$(this.el).html(this.template, placeholders));
return this;
}
});
As is evident, in the current setup, the view needs access to a greater scope than just its single model. Two possible approaches here, that I can see.
1) Pass the collection (using new QuestionView({ collection: theCollection })) rather than the model to QuestionView. Maintain an index, which you increment and re-render on the click event. This should look something like:
var QuestionView = Backbone.View.extend({
initialize: function() {
// make "this" context the current view, when these methods are called
_.bindAll(this, "showNextQuestion", "render");
this.currentIndex = 0;
this.render();
}
showNextQuestion: function() {
this.currentIndex ++;
if (this.currentIndex < this.collection.length) {
this.render();
}
},
render: function () {
$(this.el).html(this.template(this.collection.at(this.currentIndex) ));
}
});
2) Set up a Router and call router.navigate("questions/" + index, {trigger: true}) on the click event. Something like this:
var questionView = new QuestionView( { collection: myCollection });
var router = Backbone.Router.extend({
routes: {
"question/:id": "question"
},
question: function(id) {
questionView.currentIndex = id;
questionView.render();
}
});