I'm trying out backbonejs but got stuck on how to bind the model to the view.
yepnope({
load : ["/static/js/lib/jquery-1.6.2.min.js", "/static/js/lib/underscore-min.js", "/static/js/lib/backbone-min.js"],
complete: nameList
});
function nameList() {
var PageItem = Backbone.Model.extend({
defaults: {name: "default name" }
});
var Page = Backbone.Collection.extend({
model: PageItem
});
var page = new Page;
var AppView = Backbone.View.extend({
el: $("#names"),
$artistList: $('#names_list'),
$inputField: $('input#new_name'),
events: {
"keypress input": "processKeyPress"
},
processKeyPress: function(event){
if(event.charCode == 13) {
event.preventDefault();
this.addName();
}
},
addName: function(event) {
var newName = this.$inputField.val();
this.$artistList.prepend('<li>' + newName + '</li>');
page.push(new PageItem({name: newName}));
// I've also tried page.push({name: newName});
} });
var app = new AppView;}
When I press enter on the input field, it runs processKeyPress which calls addName, the new name is added the the html list but not pushed onto the model. I keep getting:
Uncaught TypeError: Object function (a){return new l(a)} has no method 'isObject'
ok, please test this out for yourself, but it appears to work with the Github version of underscore so maybe there was a bug which has been fixed.. http://jsfiddle.net/yjZVd/6/
Related
I have an 'email' style app that displays messages grouped by the date. When the app loads, a shallow collection of messages are fetched and loaded into a backbone collection. Each model in the collection represents a list of messages within a grouping. The MessageGroup represents a group of messages and the MessagesView displays the groups of messages.
This all works well until the collection is fetched again like after a filter is applied, only the group headers are displayed, not the messages inside. I've tried triggering an event that the MessagesView can listen for, then re-render itself but I get an error: listening.obj.off is not a function.
var MessageModel = Backbone.Model.extend({});
var MessageCollection = Backbone.Collection.extend({
model: MessageModel
});
var GroupModel = Backbone.Model.extend({});
var GroupCollection = Backbone.Collection.extend({
model: GroupModel,
url: '/messages/recipient',
parse: function (response) {
// Create a grouped JSON to render nested views with
var messageArray = [];
var groupedlist = _.groupBy(response.messages, function(model) {
return model.publishDate;
});
_.forEach(groupedlist, function(n, key) {
var grouping = {};
grouping.group = key;
grouping.list = n;
messageArray.push(grouping);
});
return messageArray;
},
fetchMessages: function() {
this.fetch({
data: filtermodel.toJSON(),
success: function() {
var messagecollection = new MessageCollection();
// Loop through each grouping and set sub-collections
groupcollection.each(function(group) {
var list = group.get('list');
messagecollection.reset(list);
group.set('list', messagecollection);
});
}
});
}
});
// Model to track applied filters
var FilterModel = Backbone.Model.extend({
defaults: {
folder: 0
}
});
// ------------------------ VIEWS ------------- //
// View for a single Message
var MessageView = Backbone.Marionette.ItemView.extend({
template: require('../../../templates/activities/message-item.ejs'),
events: { 'click li.item': 'getMessageDetail' },
getMessageDetail: function(e){
this.triggerMethod('showDetail', this.model);
//initMessageDetail(this.model);
}
});
// Grouped container view for a list of Messages within a group
var MessageGroup = Backbone.Marionette.CompositeView.extend({
template: require('../../../templates/activities/message-list.ejs'),
className: "list-view-group-container",
childView: MessageView,
childViewContainer: "ul.viewcontainer",
initialize: function() {
this.collection = this.model.get('list');
}
});
// Top level view for all grouped messages
var MessagesView = Backbone.Marionette.CollectionView.extend({
childView: MessageGroup,
initialize: function() {
this.collection.on('change', this.log, this);
},
log: function() {
console.log('triggered log');
}
});
// View for selected message detail
var MessageDetailView = Backbone.Marionette.ItemView.extend({
template: require('../../../templates/activities/message-detail.ejs'),
className: "message-content-wrapper"
});
// View for filter selection bar
var MessageFilterView = Backbone.Marionette.ItemView.extend({
template: require('../../../templates/activities/message-filter-bar.ejs'),
events: {
'click #search-btn': function() {
filtermodel.set('search', $('#search-input').val());
groupcollection.fetchMessages();
}
}
});
var filtermodel = new FilterModel();
var groupcollection = new GroupCollection();
// Fetch messages first run
groupcollection.fetchMessages();
// LayoutView to display in center panel of application
module.exports = ViewMessages = Marionette.LayoutView.extend({
template: require('../../../templates/activities/viewmessages.ejs'),
className: 'content full-height',
regions: {
'messagelistregion': '#messageList',
'messagedetailregion': '.message-detail',
'messagefilterregion': '.filter-bar'
},
childEvents: { 'showDetail': 'onMessageSelected' },
onMessageSelected: function (childView, childViewModel) {
var that = this;
var detailModel = childViewModel.clone();
var messageDetailView = new MessageDetailView({model:detailModel});
that.messagedetailregion.show(messageDetailView);
},
onShow: function(){
var that = this;
var messagesview = new MessagesView({
collection: groupcollection
});
var messageFilterView = new MessageFilterView();
that.messagelistregion.show(messagesview);
$("#messageList").ioslist();
that.messagefilterregion.show(messageFilterView);
this.messagedetailregion.on('show', function() {
console.log('message detail region shown:' + that.messagedetailregion.currentView);
})
}
});
I'm thinking its because the work that is done to build out the groupings of messages inside the success callback doesn't finish before the reset event is triggered and the view is refreshed. How can I get the MessagesView to update after subsequent fetches?
UPDATE:
I moved the post-success logic of grouping the collection into its hierarchical tree/leaf structure to a custom event (fetchSuccess) in the top level collectionview (MessagesView):
var MessagesView = Backbone.Marionette.CollectionView.extend({
childView: MessageGroup,
initialize: function() {
this.collection.on('fetch:success', this.fetchSuccess, this);
},
fetchSuccess: function() {
var messagecollection = new MessageCollection();
groupcollection.each(function(group) {
var list = group.get('list');
messagecollection.reset(list);
group.set('list', messagecollection);
});
}
});
It is being triggered in the success callback of the fetch. I'm pretty sure this is a good way of rendering the collection, but I cant seem to get around the error in Marionette:
**Uncaught TypeError: listening.obj.off is not a function**
Anyone have any ideas why this collectionview will not re-render??
I was able to determine that the creation of the models in the collection occurred after the reset event, but before the structure of the nested models was built out:
success: function() {
var messagecollection = new MessageCollection();
// Loop through each grouping and set sub-collections
groupcollection.each(function(group) {
var list = group.get('list');
messagecollection.reset(list);
group.set('list', messagecollection);
});
};
After any filter event, grouping, sorting etc, the collection structure needs to be modified into this nested hierarchy each time. The view was picking up the reset event before the structure was built out so the child views had no data to render. I fixed this by cloning the original collection after the changes and having the views render the cloned collection:
groupcollection.fetch({
reset: true,
data: filtermodel.toJSON(),
success: function() {
groupcollection.each(function(group) {
var list = group.get('list');
var messagecollection = new MessageCollection(list);
group.set('list', messagecollection);
});
filteredcollection.reset(groupcollection.toJSON());
}
});
I'm kind of new to Backbone and I'm having trouble understanding how to set the attributes of a View. I'm using a view without a model.
This is the View:
var OperationErrorView = Backbone.View.extend({
attributes: {},
render: function(){
var html = "<h3>" + this.attributes.get("error") +"</h3>";
$(this.el).html(html);
}
})
Then later on:
if (errors.length > 0){
errors.forEach(function(error){
// var errorView = new OperationErrorView({attributes: {"error": error} }); Doesn't work
var errorView = new OperationErrorView();
errorView.set({attributes: {"error": error}})
errorView.render()
$("#formAdd_errors").append(errorView.$el.html());
});
}
Which is the correct approach to do this? Right now it doesn't work: When I try the method that is not commented out, it gives me the error TypeError: errorView.set is not a function, if I try it the first way, it just doesn't call the render() function.
UPDATE:
var OperationErrorView = Backbone.View.extend({
attributes: {},
initialize: function(attributes){
this.attributes = attributes;
},
render: function(){
var html = "<h3>" + this.attributes.get("error") +"</h3>";
console.log("html");
$(this.el).html(html);
}
})
if (errors.length > 0){
errors.forEach(function(error){
console.log(error);
var errorView = new OperationErrorView({"error": error});
errorView.render()
$("#formAdd_errors").append(errorView.$el.html());
});
}
I tried including this.render() in the initialize function. Doesn't work. Doesn't even call the render function. Why?
A couple things:
set is not a function of a Backbone View. Check the API
In your commented code, calling new OperationErrorView(...) does not automatically evoke the render function. You have to do this manually.
The attributes property of the View does not have a get method. Again, Check the API
So, what should you do?
Research different ways to initialize a View with properties. Then figure out how to get those properties on the HTML that your View controls.
Here's a bit to get you started
var OperationErrorView = Backbone.View.extend({
tagName: 'h3',
initialize: function(attributes) {
this.attributes = attributes;
this.render();
},
render: function(){
// attach attributes to this.$el, or this.el, here
// insert the element into the DOM
$('#formAdd_errors').append(this.$el);
}
});
// later in your code
if ( errors.length > 0 ) {
errors.forEach(function(error) {
new OperationErrorView({ error: error });
});
}
Thanks to chazsolo for the answer, all the info is there. So, I'll write the code that worked just in case someone finds it useful someday:
var OperationErrorView = Backbone.View.extend({
initialize: function(attributes){
this.attributes = attributes;
},
render: function(){
var html = "<h3>" + this.attributes.error +"</h3>";
$(this.el).html(html);
}
});
if (errors.length > 0){
errors.forEach(function(error){
var errorView = new OperationErrorView({'error':error});
errorView.render()
$("#formAdd_errors").append(errorView.$el.html());
});
}
I could not find anything that mentioned my issue, I am using my router file in backbone to navigate to a different page based on the current page ID through the use of next and previous buttons. However, when I click next or previous once, it works fine, but the second time the button is clicked, the functin is called twice instead of once, and then if I click it another time it seems to be called even more than twice to a point where it seems to go berserk.
Here is my router file:
define([
'jquery',
'underscore',
'backbone',
'views/page',
'models/search',
'views/search',
'text!templates/search.html',
'models/song',
'text!templates/song.html'
], function($, _, Backbone, PageV, SearchM, SearchV, SearchT, SongM, SongT) {
var vent = _.extend({}, Backbone.Events);
var AppRouter = Backbone.Router.extend ({
routes: {
'page/:id': 'showPage',
'search': 'showView' ///:page
}
});
var initialize = function () {
var app_router
app_router = new AppRouter;
console.log('router file hit');
app_router.on('route:showPage', function (id) {
console.log('page rendered');
var songies, collected, songM;
songM = new SongM();
songM.localStorage = new Backbone.LocalStorage("music");
songM.localStorage.findAll().forEach(function (i) {
collected = i;
});
var songPages = Math.ceil(collected.music.length / 25); //10 pages
var start = id * 25;
var songies = collected.music.splice(start, 25);
var titles = {
week: collected.week,
year: collected.year,
channel: collected. channel
};
var page = new PageV({model: songM, collection: songies, vent: vent, titles: titles});
page.render(id);
vent.on('next', advance);
vent.on('previous', moveBack);
var currentId = parseInt(id);
//PROBLEM OCCURS TO THE BOTTOM TWO FUNCTIONS, and they are triggered by the vent.on above.
function moveBack () {
console.log('here is the current ID');
var newPage = 'page/' + (currentId - 1);
if(currentId <= songPages && currentId > 0 ) {
app_router.navigate(newPage, true);
} else {
app_router.navigate('search', true);
}
}
function advance () {
console.log('here is the current ID');
var newPage = 'page/' + (currentId + 1);
console.log(currentId);
console.log(songPages);
console.log(newPage);
if(currentId < songPages && currentId >=0 ) {
app_router.navigate(newPage, true);
} else {
app_router.navigate('search', true);
}
}
});
app_router.on('route:showView', function () {
console.log('search page loaded');
var searchM = new SearchM();
var search = new SearchV({model: searchM, vent: vent}); //
search.render();
vent.on('nextPage', printCons);
function printCons () {
console.log('changing pages');
app_router.navigate('page/0', true);
};
});
Backbone.history.start();
};
return {
initialize: initialize
};
});
Here is the page with the page view:
define([
'jquery',
'underscore',
'backbone',
'models/song',
'collections/songs',
'views/song',
'text!templates/page.html',
'text!templates/song.html'
], function($, _, Backbone, Song, Songs, SongV, PageT, SongT){
var Page = Backbone.View.extend({
el: $("#Sirius"),
events: {
"click .prev": "previous",
"click .next": "next"
},
previous: function () {
this.options.vent.trigger('previous');
},
next: function () {
this.options.vent.trigger('next');
},
render: function () {
var headings = this.options.titles;
var info = {
week: headings.week,
channel: headings.channel,
year: headings.year
}
var pagetemp = _.template( PageT, info);
this.$el.html( pagetemp );
var songColl = this.collection;
var songV = new SongV({collection: songColl});
songV.render();
}
});
return Page;
});
The only problems I can think of is that it somehow remembers the past instance and calls the function on both of them... or else I have no idea why it gets called twice... because if I refresh the page with that id and then click previous or next it does not increment it twice... so it must be in memory or something not sure how to go around it...
The problem is with the following event handler bindings within your app_router.on event handler:
vent.on('next', advance);
vent.on('previous', moveBack);
Each time you show a new route, you are binding those functions to the event aggregator again. You should move both of these bindings outside to the initialize function so you don't bind it multiple times.
Another quick fix, if for some reason moving these bindings outside breaks the functionality, would be to unbind the previous bindings and then bind the event handlers again:
vent.off('next');
vent.on('next', advance);
vent.off('previous');
vent.on('previous', moveBack);
See the Backbone docs for more details regarding this.
The problem is that you're creating a new view every time you change the route, but you're never deleting the old views. You're probably doubling the views every time you click on the next one!
Here's a post that might help:
Disposing of view and model objects in Backbone.js
I loop data in quoteTemplate.html(using text! plug-in) then I got one variable total=100, and I want to print total value in my view according to checkbox event :
var testView = Backbone.View.extend({
initialize: function() {
},
el: '#container',
events : {
'change [name=chkIncludeTotal]' : 'checkboxHandler'
},
checkboxHandler : function (e) {
if($(e.currentTarget).is(':checked')){
$('#total').html(total);
}else{
$('#total').html(0);
}
},
render: function(){
var cartPanel = _.template(CartPanel);
this.$el.html(cartPanel);
var aa = _.template(AATemplate);
var bb = _.template(BBTemplate);
var cc = _.template(CCTemplate);
var dd = _.template(eeTemplate, {data : test.showEEtemplate() , total:total});
$('#div1').html(bb);
$('#div2').html(cc);
$('#div3').html(dd);
}
});
I really don't know how can I take total from html template file to use in the view.
Any help would be much appreciated, thank you.
I have a backbone view that is initialized via route, but i when i navigate to another route and return to the previous one again via link, the events in the view get fired twice
Heres my router
define(['underscore','backbone','views/projects/view_project',
'views/projects/project_tasks','views/projects/project_milestones',
'views/projects/project_tasklists','views/projects/project_documents'
],
function( _,Backbone,ProjectTasks,ProjectMilestones,
ProjectTasklists,ProjectDocuments) {
var ProjectRouter = Backbone.Router.extend({
initialize: function(projects) {
if(projects) {
this.projects = projects;
}
},
//url routes mapped to methods
routes: {
"project/:id":"get_project",
"project/:id/milestones":"get_project_milestones",
"project/:id/tasks":"get_project_tasks",
"project/:id/tasklists":"get_project_tasklists",
"project/:id/documents":"get_project_documents"
},
get_project: function(id) {
UberERP.UI.loadpage("#project-view");
var project_view = new ProjectView(this.projects,id);
},
get_project_tasks: function(id) {
UberERP.UI.loadpage("#project-tasks-view");
var project_tasks_view = new ProjectTasks(id,this.projects);
},
get_project_tasklists: function(id) {
UberERP.UI.loadpage("#project-tasklist-view");
var project_tasks_view = new ProjectTasklists(id,this.projects);
},
get_project_milestones: function(id) {
UberERP.UI.loadpage("#project-milestones-view");
var project_milestones_view = new ProjectMilestones(id,this.projects);
},
get_project_documents: function(id) {
UberERP.UI.loadpage("#project-documents-view");
var project_documents_view = new ProjectDocuments(id,this.projects);
}
});
return ProjectRouter;
});
and a snipper from the view
events: {
"click input[name=task]":"select_task",
"click a.remove-icon":"remove_task",
"click td.view-task":"view_task",
"click #project-tasks-table .sort-by-status":"sort_by_status",
"click #project-tasks-table .group-filter-btn":"sort_by_task_list"
},
select_task: function( event ) {
var el = $(event.currentTarget);
row = el.parent('td').parent('tr');
console.log(el.val());
if(row.hasClass('active')) {
row.removeClass('active');
}
else {
row.addClass('active');
}
}
I have a line in the select_task method that logs the value of the clicked input element.
When the view is initially called it works properly and logs to the console. But after navigating to another route and returning back, the value of the input element is logged twice when clicked. What could be wrong?
I think you just find your self in the middle of a Backbone ghost View issue.
Try to remove and unbind all your Views when you are moving from one route to another.