I have several Backbone Models rendered in a Collection View, and also I have a route that should render a view of that model. So, here come the views
resume.js
// this renders a single model for a collection view
var ResumeView = Backbone.View.extend({
model: new Resume(),
initialize: function () {
this.template = _.template($('#resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON));
return this;
}
});
#resume template
<section id="resume">
<h1><%= profession %></h1>
<!-- !!!!! The link for a router which should navigate to ShowResume view -->
View Details
</section>
Collection view:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection;
this.collection.on('add', this.render, this);
// Getting the data from JSON-server
this.collection.fetch({
success: function (res) {
_.each(res.toJSON(), function (item) {
console.log("GET a model with " + item.id);
});
},
error: function () {
console.log("Failed to GET");
}
});
},
render: function () {
var self = this;
this.$el.html('');
_.each(this.collection.toArray(), function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
}
});
The code above works perfectly and does exactly what I need -- an array of models is fetched from my local JSON-server and each model is displayed within a collection view. However, the trouble starts when I try to navigate through my link in the template above. Here comes the router:
var AppRouter = Backbone.Router.extend({
routes: {
'': home,
'resumes/:id': 'showResume'
},
initialize: function (options) {
// layout is set in main.js
this.layout = options.layout
},
home: function () {
this.layout.render(new ResumeList({collection: resumes}));
},
showResume: function (cv) {
this.layout.render(new ShowResume({model: cv}));
}
});
and finally the ShowResume view:
var ShowResume = Backbone.View.extend({
initialize: function (options) {
this.model = options.model;
this.template = _.template($('#full-resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
I didn't provide the template for this view because it is quite large, but the error is following: whenever I try to navigate to a link, a view tries to render, but returns me the following error: Uncaught TypeError: this.model.toJSON is not a function. I suspect that my showResume method in router is invalid, but I can't actually get how to make it work in right way.
You are passing the string id of the url 'resumes/:id' as the model of the view.
This should solve it.
showResume: function (id) {
this.layout.render(new ShowResume({
model: new Backbone.Model({
id: id,
profession: "teacher" // you can pass data like this
})
}));
}
But you should fetch the data in the controller and react accordingly in the view.
var AppRouter = Backbone.Router.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'resumes/:id': 'showResume'
},
initialize: function(options) {
// layout is set in main.js
this.layout = options.layout
},
home: function() {
this.layout.render(new ResumeList({ collection: resumes }));
},
showResume: function(id) {
// lazily create the view and keep it
if (!this.showResume) {
this.showResume = new ShowResume({ model: new Backbone.Model() });
}
// use the view's model and fetch
this.showResume.model.set('id', id).fetch({
context: this,
success: function(){
this.layout.render(this.showResume);
}
})
}
});
Also, this.model = options.model; is unnecessary as Backbone automatically picks up model, collection, el, id, className, tagName, attributes and events, extending the view with them.
Related
I am creating a view but it isn't rendering in my page.
I am building a SPA with backbone, and I need that my template can open inside of div in my body, but I don't know what is my problem here.
What can be?
Show this error:
Uncaught TypeError: Cannot read property '_listenId' of undefined
at child.Events.(anonymous function) [as listenTo] (http://localhost:9000/bower_components/backbone/backbone.js:222:19)
at child.initialize (http://localhost:9000/scripts/views/RepositoriesView.js:21:12)
at child.Backbone.View (http://localhost:9000/bower_components/backbone/backbone.js:1001:21)
at new child (http://localhost:9000/bower_components/backbone/backbone.js:1566:41)
at child.repositories (http://localhost:9000/scripts/routes/AppRouter.js:46:7)
at child.execute (http://localhost:9000/bower_components/backbone/backbone.js:1265:30)
at Object.callback (http://localhost:9000/bower_components/backbone/backbone.js:1254:16)
at http://localhost:9000/bower_components/backbone/backbone.js:1481:19
at Function.some (http://localhost:9000/bower_components/lodash/dist/lodash.compat.js:4304:25)
at Backbone.History.loadUrl (http://localhost:9000/bower_components/backbone/backbone.js:1479:16)
My AppRouter is:
/*global Sice, Backbone*/
Sice.Routers = Sice.Routers || {};
Sice.Views = Sice.Views || {};
(function() {
'use strict';
Sice.Routers.AppRouter = Backbone.Router.extend({
//map url routes to contained methods
routes: {
"": "repositories",
"repositories": "repositories",
"search": "search",
"starreds": "starreds"
},
deselectPills: function() {
//deselect all navigation pills
$('ul.pills li').removeClass('active');
},
selectPill: function(pill) {
//deselect all navigation pills
this.deselectPills();
//select passed navigation pill by selector
$(pill).addClass('active');
},
hidePages: function() {
//hide all pages with 'pages' class
$('div#content').hide();
},
showPage: function(page) {
//hide all pages
this.hidePages();
//show passed page by selector
$(page).show();
},
repositories: function() {
this.showPage('div#content');
this.selectPill('li.repositories-pill');
new Sice.Views.RepositoriesView();
},
search: function() {
this.showPage('div#content');
this.selectPill('li.search-pill');
},
starreds: function() {
this.showPage('div#content');
this.selectPill('li.starreds-pill');
}
});
Sice.Views.AppView = Backbone.View.extend({
//bind view to body element (all views should be bound to DOM elements)
el: $('body'),
//observe navigation click events and map to contained methods
events: {
'click ul.pills li.repositories-pill a': 'displayRepositories',
'click ul.pills li.search-pill a': 'displaySearch',
'click ul.pills li.starreds-pill a': 'displayStarreds'
},
//called on instantiation
initialize: function() {
//set dependency on Sice.Routers.AppRouter
this.router = new Sice.Routers.AppRouter();
//call to begin monitoring uri and route changes
Backbone.history.start();
},
displayRepositories: function() {
//update url and pass true to execute route method
this.router.navigate("repositories", true);
},
displaySearch: function() {
//update url and pass true to execute route method
this.router.navigate("search", true);
},
displayStarreds: function() {
//update url and pass true to execute route method
this.router.navigate("starreds", true);
}
});
//load application
new Sice.Views.AppView();
})();
My View is:
/*global Sice, Backbone, JST*/
Sice.Views = Sice.Views || {};
(function() {
'use strict';
Sice.Views.RepositoriesView = Backbone.View.extend({
template: JST['app/scripts/templates/RepositoriesView.ejs'],
tagName: 'div',
id: 'repositoriesView',
className: 'page-repositories',
events: {},
initialize: function() {
this.listenTo(this.model, 'change', this.render);
},
render: function() {
this.$el.html(this.template());
}
});
})();
What's happening?
In the following router function, you're instantiating a new View instance.
repositories: function() {
this.showPage('div#content');
this.selectPill('li.repositories-pill');
new Sice.Views.RepositoriesView(); // <-- here
},
But you're not passing a model object which the view listens to.
initialize: function() {
this.listenTo(this.model, 'change', this.render);
},
So it's calling this.listenTo with this.model as undefined.
What's the solution?
Pass a model instance
new Sice.Views.RepositoriesView({ model: new MyModel() });
Create a model instance in the view
initialize: function() {
this.model = new MyModel();
this.listenTo(this.model, 'change', this.render);
},
I can not understand how to show a single product. When I want to show product page (model view - app.ProductItemView in productPageShow) I get "Uncaught TypeError: Cannot read property 'get' of undefined at child.productPageShow (some.js:58)" >> this.prod = this.prodList.get(id);
Here is my code:
// Models
var app = app || {};
app.Product = Backbone.Model.extend({
defaults: {
coverImage: 'img/placeholder.png',
id: '1',
name: 'Unknown',
price: '100'
}
});
app.ProductList = Backbone.Collection.extend({
model: app.Product,
url: 'php/listProducts.php'
});
// Views
app.ProductListView = Backbone.View.extend({
el: '#product-list',
initialize: function() {
this.collection = new app.ProductList();
this.collection.fetch({ reset: true });
this.render();
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.collection.each(function(item) {
this.renderProduct(item);
}, this);
},
renderProduct: function(item) {
app.productView = new app.ProductView({
model: item
});
this.$el.append(app.productView.render().el);
}
});
app.ProductItemView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#productPage').html()),
render: function(eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
app.ProductView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#productTemplate').html()),
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
// Router
app.Router = Backbone.Router.extend({
routes: {
"list": 'list',
"product/:id": "productPageShow"
},
initialize: function() {
this.$content = $("#product-list");
},
list: function() {
this.prodList = new app.ProductList();
this.productListView = new app.ProductListView({ model: this.prodList });
this.prodList.fetch();
this.$content.html(app.productListView.el);
},
productPageShow: function(id) {
this.prod = this.prodList.get(id);
this.prodItView = new app.ProductItemView({ model: this.prod });
this.$content.html(this.prodItView.el);
}
});
$(function() {
new app.Router();
Backbone.history.start();
});
There are some conceptual problems with the code, without getting into too much details, there are a lot of things happening in the Router that don't belong there, but for a (currently) non-complex application that's manageable.
I'll focus on the app.Router file because that's the culprit of your problems most probably.
routes: {
"list": 'list',
"product/:id": "productPageShow"
}
Let's start with the basics, when you define a list of routes in Backbone Router( or any other Router in other frameworks ) you give a route a key that will correspond to something in the URL that the Router will recognize and call a callback method.
If you navigate your browser to:
http://your-url#list
Backbone will call the list callback
Similarly:
http://your-url#product/1
Backbone will call productPageShow callback
Thing to know: ONLY ONE ROUTE CALLBACK CAN EVER BE CALLED! The first time a Backbone Router finds a matching route it will call that callback and skip all others.
In your code you're relying on the fact that this.prodList will exist in productPageShow method but that will only happen if you first go to list route and then to product/{id} route.
Another thing .. in your listcallback in the Router you set a model on the ProductListView instance .. but that model is neither user, nor is it a model since this.productList is a Backbone.Collection
Additionally, you need to know that fetch is an asynchronous action, and you're not using any callbacks to guarantee that you'll have the data when you need it ( other than relying on the 'reset' event ).
So this would be my attempt into making this workable:
// Models
var app = app || {};
app.Product = Backbone.Model.extend({
defaults: {
coverImage: 'img/placeholder.png',
id: '1',
name: 'Unknown',
price: '100'
}
});
app.ProductList = Backbone.Collection.extend({
model: app.Product,
url: 'php/listProducts.php'
});
// Views
app.ProductListView = Backbone.View.extend({
el: '#product-list',
initialize: function() {
this.render();
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.collection.each(function(item) {
this.renderProduct(item);
}, this);
},
renderProduct: function(item) {
app.productView = new app.ProductView({
model: item
});
this.$el.append(app.productView.render().el);
}
});
app.ProductItemView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#productPage').html()),
render: function(eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
app.ProductView = Backbone.View.extend({
tagName: 'div',
template: _.template($('#productTemplate').html()),
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
// Router
app.Router = Backbone.Router.extend({
routes: {
"": "list",
"product/:id": "productPageShow"
},
initialize: function() {
this.$content = $("#product-list");
},
list: function() {
this.prodList = new app.ProductList();
this.productListView = new app.ProductListView({ collection: this.prodList });
this.prodList.fetch({reset:true});
this.$content.html(app.productListView.el);
},
productPageShow: function(id) {
try {
this.prod = this.prodList.get(id);
this.prodItView = new app.ProductItemView({ model: this.prod });
this.$content.html(this.prodItView.el);
} catch (e) {
// Navigate back to '' route that will show the list
app.Router.navigate("", {trigger:'true'})
}
}
});
$(function() {
app.Router = new app.Router();
Backbone.history.start();
});
So with a bit of shooting in the dark without the complete picture, this is what changed:
ProductListView is no longer instantiating ProductList collection that is done in the list callback in Router
Changed the route from 'list' to '', that will guarantee that the list is shown immediately
In case there are no product data available in productPageShow navigate back to list
The problem is that every single time I add a new object through the fetch function of a collection (MovieCollection) on each element add some sort of event triggers and it initialises a new MovieList object to which the collection was passed after it has fetched all the items.
Here is my app.js:
// Global App skeleton for backbone
var App = new Backbone.Marionette.Application();
_.extend(App, {
Controller: {},
View: {},
Model: {},
Page: {},
Scrapers: {},
Providers: {},
Localization: {}
});
// set database
App.addRegions({ Window: ".main-window-region" });
App.addInitializer(function(options){
var mainWindow = new App.View.MainWindow();
try{
App.Window.show(mainWindow);
} catch(e) {
console.error("Couldn't start app: ", e, e.stack);
}
});
So basically I create the MainWindow View which looks like:
(function(App) {
"use strict";
var MainWindow = Backbone.Marionette.LayoutView.extend({
template: "#main-window-tpl",
id: 'main-window',
regions: {
Content: '#content',
},
initialize: function() {
//Application events
//App.vent.on('movies:list', _.bind(this.showMovies, this));
},
onShow: function() {
this.showMovies();
//App.vent.trigger('main:ready');
},
showMovies: function(e) {
var browser = new App.View.MovieBrowser();
this.Content.show(browser);
}
});
App.View.MainWindow = MainWindow;
})(window.App);
Here is the MovieBrowser which should make the collection, fetch it and then on the show would pass it to the MovieList.
(function(App) {
'use strict';
var MovieBrowser = Backbone.Marionette.LayoutView.extend({
template: '#movie-browser-tpl',
className: 'movie-browser',
regions: {
MovieList: '.movie-list-region'
},
initialize: function() {
console.log('Init MovieBrowser');
this.movieCollection = new App.Model.MovieCollection([], {});
this.movieCollection.fetch();
},
onShow: function() {
var ml = new App.View.MovieList({
collection: this.movieCollection
});
this.MovieList.show(ml);
}
});
App.View.MovieBrowser = MovieBrowser;
})(window.App);
So in turns the MovieCollection is (with I guess a major problem being something with it):
(function(App) {
"use strict";
var MovieCollection = Backbone.Collection.extend({
model: App.Model.Movie,
initialize: function(models, options) {
console.log('Init MovieCollection');
},
fetch: function() {
this.add([{"imdb":"1598172","title":"Once Upon a Time in Brooklyn","year":2013,"MovieRating":"4.1","image":"http://zapp.trakt.us/images/posters_movies/217035-300.jpg","bigImage":"http://zapp.trakt.us/images/posters_movies/217035-300.jpg","torrents":{"1080p":{"url":"https://yts.re/download/start/739F9B2F114DB4B48D34DEE2787725FF0747F6F3.torrent","size":"1768399511","seed":"2712","peer":"1709"},"720p":{"url":"https://yts.re/download/start/E482DC66BA1117F3706FACD0292BD32F1CDFE5F5.torrent","size":"854590014","seed":"1553","peer":"828"}},"backdrop":"http://zapp.trakt.us/images/fanart_movies/217035-940.jpg","synopsis":"After being released from prison, Bobby goes back to the mob connected streets. When forced to make a life altering decision the truth is revealed that he was too blind to see.","genres":[],"certification":"R","runtime":116,"tagline":"","trailer":""}]);
},
});
App.Model.MovieCollection = MovieCollection;
})(window.App);
And MovieList is:
(function(App) {
"use strict";
var SCROLL_MORE = 200;
var ErrorView = Backbone.Marionette.ItemView.extend({
template: '#movie-error-tpl',
onBeforeRender: function() {
this.model.set('error', this.error);
}
});
var MovieList = Backbone.Marionette.CompositeView.extend({
template: '#movie-list-tpl',
tagName: 'ul',
className: 'movie-list',
itemView: App.View.MovieItem,
itemViewContainer: '.movies',
initialize: function() {
console.log('Init MovieList')
if (typeof this.collection !== 'undefined') {
//this.listenTo(this.collection, 'loading', this.onLoading);
//this.listenTo(this.collection, 'loaded', this.onLoaded);
//this.collection.fetch();
} else {
console.trace()
}
},
});
App.View.MovieList = MovieList;
})(window.App);
This is the MovieItem:
(function(App) {
"use strict";
var MovieItem = Backbone.Marionette.ItemView.extend({
template: '#movie-item-tpl',
tagName: 'li',
className: 'movie-item',
ui: {
coverImage: '.cover-image',
cover: '.cover'
}
});
App.View.MovieItem = MovieItem;
})(window.App);
With finally the Movie model being:
(function(App) {
"use strict";
var Movie = Backbone.Model.extend({
idAttribute: 'imdb',
initialize: function() {
},
});
App.Model.Movie = Movie;
})(window.App);
So in order to run it I basically have to check if collection is undefined. To see if it would continue and it does but the application itself is not working properly as it creates several MovieList items adding several ul elements with the same id.
here is the html templates:
http://jsfiddle.net/tarazo8e/
and this is the order of inclusion of the JS files:
<!-- App Initialization -->
{{ HTML::script('js/App/app.js') }}
<!-- Backbone Views and Controllers -->
{{ HTML::script('js/App/views/main_window.js') }}
{{ HTML::script('js/App/views/movie_browser/movie_browser.js') }}
{{ HTML::script('js/App/views/movie_browser/movie_item.js') }}
<!-- Backbone Models -->
{{ HTML::script('js/App/models/movie.js') }}
{{ HTML::script('js/App/models/movie_collection.js') }}
{{ HTML::script('js/App/views/movie_browser/movie_list.js') }}
I tried moving movie_list.js at almost any position with no luck.
Any help would be so so so much appreciated as this is driving me crazy for the past 5 days.
Credits: The code has been taken from an old version of Popcorn-Time for only learning purposes.
Edit:
Live versions that shows the exact error:
http://googledrive.com/host/0Bxf4SpRPErmGQmdKNmoyaEVSbmc
Try to swap moview_browser.js and moview_item.js. ItemView should be defined first.
The default rendering mode for a CompositeView assumes a hierarchical, recursive structure. If you configure a composite view without specifying an childView, you'll get the same composite view class rendered for each child in the collection.
For some reasons, I keep on getting this error, (See attached screenshot). I've tried adding a _.bindAll(this); and even tried upgrading my code to have the latest version of backbonejs. Still no luck.
Can someone help me on this?
var app = app || {};
(function ($) {
'use strict';
app.EmployeeView = Backbone.View.extend({
el: '#container',
model: app.Employee,
events: {
'click #save' : 'saveEntry'
},
initialize: function(){
console.log('Inside Initialization!');
this.$empName = this.$('#txtEmpName');
this.$department = this.$('#txtDepartment');
this.$designation = this.$('#txtDesignation');
this.listenTo(app.employees, 'add', this.addEmployee);
app.employees.fetch();
console.log('End of Initialization!');
//this.render();
},
render: function () {
console.log('Inside Render!!');
console.log(this.model);
this.$el.html(this.template(this.model.toJSON()));
console.log('Inside End of Render!!');
return this;
},
newAttributes: function(){
return{
empName: this.$empName.val(),
department: this.$department.val(),
designation: this.$designation.val()
};
},
saveEntry: function(){
console.log('Inside SaveEntry!');
//console.log(this.newAttributes());
console.log('this.model');
console.log(app.Employee);
//app.employees.create(this.newAttributes());
app.Employee.set(this.newAttributes());
app.employees.add(app.Employee);
console.log('After SaveEntry!');
},
addEmployee: function (todo) {
var view = new app.EmployeeItemView({ model: app.Employee });
$('#empInfo').append(view.render().el);
}
})
})(jQuery);
Code for "collections/employees.js"
var app = app || {};
(function (){
console.log('Inside collection');
var Employees = Backbone.Collection.extend({
model: app.Employee,
localStorage: new Backbone.LocalStorage('employee-db')
});
app.employees = new Employees();
})();
Code for "model/employee.js"
var app = app || {};
(function(){
'use strict';
app.Employee = Backbone.Model.extend({
defaults: {
empName: '',
department: '',
designation: ''
}
});
})();
You're saying this in your view:
model: app.Employee
app.Employee looks like a model "class" rather than a model instance. Your view wants a model instance in its model property. Normally you'd say something like this:
var employee = new app.Employee(...);
var view = new app.EmployeeView({ model: employee });
this.model.toJSON() won't work since this.model is the app.Employee constructor. Actually I don't see any meaning in your EmployeeView.render method. If it is aggregate view why you have model on it? Otherwise what is the second view class EmployeeItemView? If you're following ToDo MVC example you can see that there is no model in AppView, that is why I conclude you need not model in your EmployeeView. And render method you provided seems to belong to EmployeeItemView.
Secondly, you call app.Employee.set which is also a call on a constructor not on an object. I think you meant
saveEntry: function(){
console.log('Inside SaveEntry!');
app.employees.create(this.newAttributes());
console.log('After SaveEntry!');
},
If you want to pass a model to app.EmployeeItemView you should use callback argument.
addEmployee: function (employee) {
var view = new app.EmployeeItemView({ model: employee });
$('#empInfo').append(view.render().el);
}
I have a web application using BackboneJS. In this application, I have a LayoutView.js file in which there is a Backbone View (called LayoutView). LayoutView has other functions (methods) that call other views. I am fetching some data in the initialize function of LayoutView, and I need to get this same data (model) in another view and work (update/delete) on it. Below is how I am passing data from LayoutView to myView:
var LayoutView = Backbone.View.extend({
el: $("#mi-body"),
initialize: function () {
var that = this;
this.ConfigData = new Configurations(); //Configurations is a collection
this.ConfigData.fetch({
success: function () {
alert("success");
},
error: function () {
alert("error");
}
});
this.render();
Session.on('change:auth', function (session) {
var self = that;
that.render();
});
},
render: function () {
// other code
},
events: {
'click #logout': 'logout',
'click #divheadernav .nav li a': 'highlightSelected'
},
myView: function () {
if (Session.get('auth')) {
this.$el.find('#mi-content').html('');
this.options.navigate('Myview');
return new MyLayout(this.ConfigData);
}
}
});
Still, I do not know how to "get"/access this data as my current data/model/collection (I am not sure which term is correct) in myView and work on it using Backbone's "model.save(), model.destroy()" methods. Also, whenever an edit/delete happens, the data of ConfigData should be modified and the update should reflect in the html displayed to the user.
Below is the code from MyView:
var MyView = Backbone.View.extend({
tagName: 'div',
id: "divConfigurationLayout",
initialize: function (attrs) {
this.render();
},
render: function () {
var that = this;
},
events: {
"click #Update": "update",
"click #delete": "delete"
},
update: function(){
//code for updating the data like model.save...
},
delete: function(){
//code for deleting the data like model.destroy...
}
});
Now the data I passed is in attrs in the initialize function. How to get this done..?
The syntax for instantiating a Backbone view is new View(options) where options is an Object with key-value pairs.
To pass a collection to your view, you'd instantiate it like so:
new MyLayout({
collection : this.configData
});
Within your view, this.collection would refer to your configData collection.