Here is how my Backbone Router looks like
define([
"jquery",
"underscore",
"backbone"
], function ($, _, Backbone) {
return Backbone.Router.extend({
routes: {
"overview": "overview"
},
overview: function () {
require([
"views/overview",
"models/user-collection",
"grid",
"spreadsheet"
], function (OverviewView, TestCollection, GridView, SpreadSheetView) {
// Data
var collection = new TestCollection();
// Main view
var view = new OverviewView({
el: "#page",
collection: collection
});
// Sub view #1
var gridView = new GridView({
el: "#backgridWrapper"
});
// Sub View #2
var spreadsheetView = new SpreadSheetView({
el: "#handsontableWrapper"
});
// Flow
collection.fetch({
success: function () {
view.render();
gridView.render();
spreadsheetView.render();
}
});
});
}
});
});
As you can see there are several views:
Main view
Sub view #1
Sub view #2
I've did a lot of searching on how to organize the views and sub-views in Backbone, however all of them supposed to create a new sub-view instance directly within a view definition, so that router only knows about Main view...
So the question is - is it a good idea to handle sub-views at a router, instead of directly at view constructor?
The router should be just handling routes and initializing stuff.
Things like fetching data should go in the view that uses it - The view displays the data or error messages (in case of a failure), so I think it's wise to let the view fetch the data rather than some router who's only interested in the routes and have no interest in the data.
and I prefer initializing the sub views, inside their parent view, rather than somewhere else. The parent - child relationship itself justifies that, you better keep the children with their parents than a stranger so they will be under better control and you can easily find them later as well :)
Mostly it's a matter of opinion, but the thing is if you don't, all your code will soon get cluttered in the router rather than being well organized.
Below is how I'll structure the same thing.
Note that I'm initializing child views as part of parent views render method. It could be done when the parent view is initialized, but I see no point in doing so unless the parent view successfully fetches data and is proceeding to render itself.
define([
"jquery",
"underscore",
"backbone"
], function($, _, Backbone) {
return Backbone.Router.extend({
routes: {
"overview": "overview"
},
overview: function() {
require(["views/overview"], function(OverviewView) {
// initialize Main view
var view = new OverviewView({
el: "#page"
});
});
}
});
});
define([
"jquery",
"underscore",
"backbone",
"models/user-collection",
"grid",
"spreadsheet"
], function($, _, Backbone, TestCollection, GridView, SpreadSheetView) {
return Backbone.View.extend({
initialize: function(options) {
this.collection = new TestCollection();
this.fetchData();
},
events: {},
render: function() {
// rendering subviews is part of rendering their parent view.
//I prefer to do that here
// Sub view #1
this.gridView = new GridView({
el: "#backgridWrapper"
});
// Sub View #2
this.spreadsheetView = new SpreadSheetView({
el: "#handsontableWrapper"
});
//Below lines can be handled while initializing the respective view
// (In their initialize() method, or after fetching some data etc
// or can be chained with the above initialization if their render() method returns a reference to itself (`return this`)
this.gridView.render();
this.spreadsheetView.render();
},
fetchData: function() {
var view = this;
this.collection.fetch({
success: function() {
view.render();
}
});
}
});
});
side note : I strongly suggest not to put a collection under models folder.
Related
I've created a Rails app using Backbone, but I've run into a small but annoying problem. When a user goes to his profile and then back to the frontpage the frontpage is empty and I have to refresh it in order to show the content of the backbone view.
In my Movieseat.Routers.Movieseats I have:
routes:
'': 'index'
initialize: ->
#movieSeats = new Movieseat.Collections.Movieseats()
#movieSeats.fetch()
#searchResults = new Movieseat.Collections.Moviesearch()
index: ->
view = new Movieseat.Views.MovieseatsIndex(collection: #movieSeats)
$('#container').html(view.render().el)
Any idea on why users have to refresh to see the frontpage?
I don't know CoffeeScript but the fetch() method takes a success callback where you can ensure the collection has loaded. In your index you're creating a new view passing the #movieSeats collection but the collection may have not been loaded by that time. Try the following:
{
routes: {
'': 'index'
},
initialize: function() {
this.movieSeats = new Movieseat.Collections.Movieseats();
},
index: function() {
var self = this, view;
this.movieSeats.fetch({
success: function(collection) {
view = new Movieseat.Views.MovieseatsIndex({ collection: collection })
$('#container').html(view.render().el)
}
});
}
}
However, you might want to "bootstrap" your collection like is shown on the Backbone docs: http://documentcloud.github.io/backbone/#FAQ-bootstrap
I'm putting together a simple todo list app based on some examples to learn about Backbone. I'm running the code on Django for database and using Tastypie to do the API, and am trying to use Backbone (with RequireJS for AMD) but am having trouble with the following error in my console:
Uncaught TypeError: Object function (){ return parent.apply(this, arguments); } has no method 'on'
I think my app is having trouble communicating via the backbone collection,because it seems to get this error at about here in my app.js (main app view):
// Bind relavent events to the todos.
this.listenTo(Todos, "add", this.addOne);
this.listenTo(Todos, "reset", this.addAll);
I've seen some similar issues on here but I'm just breaking into this stuff so an explanation of what's happening would be much appreciated.
collections.js
define([
'underscore',
'backbone',
'app/app',
'app/models/model'
], function(_, Backbone, app, Todo){
// The collection of our todo models.
TodoList = Backbone.Collection.extend({
model: Todo,
// A catcher for the meta object TastyPie will return.
meta: {},
// Set the (relative) url to the API for the item resource.
url: "/api/item",
// Our API will return an object with meta, then objects list.
parse: function(response) {
this.meta = response.meta;
return response.objects;
}
});
var Todos = new TodoList();
return Todos;
});
app.js:
define([
'jquery',
'underscore',
'backbone',
'app/models/model',
'app/collections/collection',
'app/views/view'
], function($, _, Backbone, Todos, Todo, TodoList, TodoView){
// The view for the entire app.
var AppView = Backbone.View.extend({
el: "#todo-app",
// Bind our events.
events: {
"keypress #new-todo": "createOnEnter",
},
initialize: function() {
// TastyPie requires us to use a ?format=json param, so we'll
// set that as a default.
$.ajaxPrefilter(function(options) {
_.extend(options, {format: "json"});
});
// Bind relavent events to the todos.
this.listenTo(Todos, "add", this.addOne);
this.listenTo(Todos, "reset", this.addAll);
// Cache some of our elements.
this.$input = this.$("#new-todo");
// Get our todos from the API!
Todos.fetch();
},
// Crate a new todo when the input has focus and enter key is hit.
createOnEnter: function(event) {
var keyCode = event.keyCode || event.which,
title = this.$input.val().trim();
// If we haven't hit enter, then continue.
if (keyCode != 13 || !title) return;
// Create a new todo.
Todos.create({title: title, complete: false});
// Reset the input.
this.$input.val("");
},
// Add a single todo to the list.
addOne: function(todo) {
var view = new TodoView({model: todo});
$("#todo-list").append(view.render().el);
},
// Clear the list and add each todo one by one.
addAll: function() {
this.$("todo-list").html("");
Todos.each(this.addOne, this);
}
});
return AppView;
});
The order of definitions and parameters in your app.js file is incorrect, you can try like this:
define([
'jquery',
'underscore',
'backbone',
'app/models/model',
'app/collections/collection',
'app/views/view'
], function($, _, Backbone, Todo, Todos, TodoView){
I'm using Controller to Fetch URL. I need a way to put Parameter in this POST. These Parameters are selected by users on View & Not Stored yet(I dont know how to store)
Currently I managed to
1) Display & Route The View with search result coming from API
2) Display and refresh the page when someone selects a Filter Option
Problem
1) I got no idea how to record what the users clicked
2) How do i "re-post" so i can get the new set of results
3) I read and say people saying POST Fetch should be done in Model , Conllection is for Store Multiple Models which i don't know in this scenario?
Collections
Jobs.js
define([
'jquery',
'underscore',
'backbone',
'models/filter'
], function($, _, Backbone,JobListFilterModel){
var Jobs = Backbone.Collection.extend({
url: function () {
return 'http://punchgag.com/api/jobs?page='+this.page+''
},
page: 1,
model: JobListFilterModel
});
return Jobs;
});
Collections Filter.JS
define([
'jquery',
'underscore',
'backbone',
'models/filter'
], function($, _, Backbone,JobListFilterModel){
console.log("Loaded");
var Jobs = Backbone.Collection.extend({
url: function () {
return 'http://punchgag.com/api/jobs?page='+this.page+''
},
page: 1,
model: JobListFilterModel
});
// var donuts = new JobListFilterModel;
// console.log(donuts.get("E"));
return Jobs;
});
Models
Filter.js
define([
'underscore',
'backbone'
], function(_, Backbone){
var JobFilterModel = Backbone.Model.extend({
defaults: {
T: '1', //Task / Event-based
PT: '1', //Part-time
C: '1', //Contract
I: '1' //Internship
}
});
// Return the model for the module
return JobFilterModel;
});
Models
Job.js
define([
'underscore',
'backbone'
], function(_, Backbone){
var JobModel = Backbone.Model.extend({
defaults: {
name: "Harry Potter"
}
});
// Return the model for the module
return JobModel;
});
Router.js
define([
'jquery',
'underscore',
'backbone',
'views/jobs/list',
'views/jobs/filter'
], function($, _, Backbone, JobListView, JobListFilterView){
var AppRouter = Backbone.Router.extend({
routes: {
// Define some URL routes
'seeker/jobs': 'showJobs',
'*actions': 'defaultAction'
},
initialize: function(attr)
{
Backbone.history.start({pushState: true, root: "/"})
},
showJobs: function()
{
var view = new JobListView();
view.$el.appendTo('#bbJobList');
view.render();
console.log(view);
var jobListFilterView = new JobListFilterView();
jobListFilterView.render()
},
defaultAction: function(actions)
{
console.info('defaultAction Route');
console.log('No route:', actions);
}
});
var initialize = function(){
console.log('Router Initialized');// <- To e sure yout initialize method is called
var app_router = new AppRouter();
};
return {
initialize: initialize
};
})
;
Capturing what a user has done should be completed within a view. For example:
filterView.js
var View = Backbone.View.extend({
initialize: function() {
_.extend(this, Backbone.Events);
},
render: function() {
// Your normal render routine
return this;
},
events: {
"submit form": "submit"
},
submit: function() {
var query = $("input[type=search]", this.el).val();
this.trigger("submit", query);
}
});
return View;
...whatever implements filterView and jobCollection...
// Implement our collections, views, etc
var jobCollection = new JobCollection();
var jobsView = new JobsView();
var filterView = new FilterView().render();
// Add listeners on our view
filterView.on("submit", updateJobs);
// Implement the functions for our view (this can be done in a number of ways)
function updateJobs(query) {
jobCollection.fetch({
data: {
query: query
}
}).done(function(response) {
// Technically you could just provide "jobsView.render" to the done() method, but I wanted to be a little more verbose here regarding parameters
jobsView.render(response);
});
}
Your view is only aware of itself (input structure, etc). The thing that implements the view and collection is the broker between these two modules.
You specify parameters within the fetch statement by providing an object which implements a data parameter. In this case, we're going to pass the query using the query key. fetch returns a promise, so once that's done, we return that response to the jobsView and pass it to the render function, which should be set up to accept the collection.
updateJobs is unaware of the view implementation, all it knows is it expects a query to be included.
This code is not meant to be directly implemented or copy/pasted, but it's meant to provide an outline for how you might accomplish what you're looking to do.
I'm new to backbone.js and handlebars and I'm having a problem getting my template to render out the data.
Here is my collection and model data from tagfeed.js module:
// Create a new module.
var Tagfeed = app.module();
// Default model.
Tagfeed.Model = Backbone.Model.extend({
defaults : {
name : '',
image : ''
}
});
// Default collection.
Tagfeed.Collection = Backbone.Collection.extend({
model : Tagfeed.Model,
url : Api_get('api/call')
});
Tagfeed.TagView = Backbone.LayoutView.extend({
template: "tagfeed/feed",
initialize: function() {
this.model.bind("change", this.render, this);
},
render: function(template, context) {
return Handlebars.compile(template)(context);
}
});
Then in my router I have:
define([
// Application.
"app",
// Attach some modules
"modules/tagfeed"
],
function(app, Tagfeed) {
// Defining the application router, you can attach sub routers here.
var Router = Backbone.Router.extend({
routes: {
"index.html": "index"
},
index: function() {
var collection = new Tagfeed.Collection();
app.useLayout('main', {
views: {
".feed": new Tagfeed.TagView({
collection: collection,
model: Tagfeed.Model,
render: function(template, context) {
return Handlebars.compile(template)(context);
}
})
}
});
}
});
return Router;
});
THis successfully makes a call to the api, makes a call to get my main template, and makes the call to get the feed template HTML. If I don't include that render(template, context) function, then it renders on the page as the straight up HTML that I have in the feed template with the {{ name }} still included. however when its included, I get the error
TypeError: this._input.match is not a function
[Break On This Error]
match = this._input.match(this.rules[rules[i]]);
and if I examine the variables that get passed into the appLayout views render function for feed, I see that the template var is a function, and the context var is undefined, then it throws that error.
Any ideas what I'm doing wrong? I know I have at least one problem here, probably more.
Since you're using requirejs, you can use the text module to externalise your templates or better still pre-compile them and include them in your view. Check out http://berzniz.com/post/24743062344/handling-handlebars-js-like-a-pro
E.g. using pre-compiled templates
// router.js
define(['views/tag_feed', 'templates/feed'], function(TagFeedView) {
var AppRouter = Backbone.Router.extend({
// ...
});
})
// tag_feed.js
define(['collections/tag_feed'], function() {
return Backbone.View.extend({
// ...
render: function() {
this.$el.html(
Handlebars.templates.feed({
name: '...'
})
);
}
});
})
For reference I've created simple boilerplate for a backbone/require/handlebars setup https://github.com/nec286/backbone-requirejs-handlebars
So I've got a pretty simple backbone app with a model, a collection, and a couple of views. I'm fetching the actual data from the server by doing a collection.fetch() at page load.
My problem is that one of my views is a "detail" view, and I want to bind it to a particular model - but I don't have the model yet when the page loads. My code looks a lot like this:
window.App = {
Models: {},
Collections: {},
Views: {},
Routers: {}
}
App.Models.Person = Backbone.Model.extend({
urlRoot: '/api/people'
});
App.Collections.People = Backbone.Collection.extend({
model: App.Models.Person,
url: '/api/people'
});
people = new App.Collections.People()
App.Views.List = Backbone.View.extend({
initialize: function() {
this.collection.bind('reset', this.render());
},
render: function() {
$(this.el).html("We've got " + this.collection.length + " models." )
}
});
listView = new App.Views.List({collection: people})
App.Views.Detail = Backbone.View.extend({
initialize: function() {
this.model.bind('change', this.render());
},
render: function() {
$(this.el).html("Model goes here!")
}
});
App.Routers.Main = Backbone.Router.extend({
routes: {
'/people': 'list',
'/people/:id': 'detail'
},
list: function() {
listView.render();
},
detail: function(id) {
detailView = new App.Views.Detail({model: people.get(id)})
detailView.render()
}
})
main = new App.Routers.Main();
Backbone.history.start();
people.fetch();
But if I start with the detail route active, the people collection is empty, so people.get(id) doesn't return anything, so my new view has this.model undefined, and won't let me bind any events relating to it. The error is:
Uncaught TypeError: Cannot call method 'bind' of undefined
If I start with the list route active, then by the time I click on an item to bring up the detail view people is populated, so everything works.
What's the right way to bind model-related events for a "detail" view when you're fetching the data after page load?
You have a part of the answer here: Backbone.js Collections not applying Models (using Code Igniter)
Indeed, you need to wait that people.fetch finishes its ajax request before to call Backbone.history.start(); and trigger the actual route.
Your code should look like:
// [...]
main = new App.Routers.Main();
peoples.fetch({
success: function (collection, response) {
// The collection is filled, trigger the route
Backbone.history.start();
}
});
You can add a loader on the page and hide it when the collection is loaded.