I am creating an app that will list the days of an event as buttons, then let you add dates and click each date to get a new "daily calendar".
This is my first real world app using backbone and underscore, so I keep running into road blocks. I would really appreciate anyone helping me out.
I am now at the point where my collection is full of dates, and I can add to those dates. Now what I am trying to figure out it routing the links to switch out the calendar, depending on the selected date.
Heres what I have relating to this part of the app so far:
Collections
var Days = Backbone.Collection.extend({
url: daysURL
});
var Calendar = Backbone.Collection.extend({
url: URL
});
Models
var Header = Backbone.Model.extend();
var header = new Header();
var ConferenceDay = Backbone.Model.extend();
var conferenceDay = new ConferenceDay();
View
var HeaderView = Backbone.View.extend({
el: $(".conf_days"),
template: _.template($('#days').html()),
events: {
'click a.day-link': 'changeDay',
'click #add_day' : 'addDay',
'click #previous_day' : 'prevDay',
'click #next_day' : 'nextDay',
'click #delete_day' : 'deleteDay'
},
initialize: function(){
_.bindAll(this, "render");
this.collection = new Days();
this.collection.fetch();
this.collection.bind("reset", this.render, this);
},
render: function(){
var JSONdata = this.collection.toJSON();
this.$el.html(this.template({days: JSONdata}));
console.log(JSON.stringify(JSONdata))
return this;
},
changeDay: function(e){
AppRouter.history.navigate($(this).attr('href'));
return false;
},
addDay: function() {
newDate = Date.parse($('.day-link:first-child').text()).add(1).day();
var newDay = new ConferenceDay();
newDay.set({date_formatted: newDate});
this.collection.add(newDay)
newDay.save({
success: function(){
alert('yes')
},
error: function(){
alert('no')
}
});
},
deleteDay: function(event){
var id = $('.day-link:last-child').data("id");
$('.day-link:last-child').remove();
},
prevDay: function() {
},
nextDay: function() {
},
loadTimes: function(){
var html = time.get('times');
$('.time_td').append(html);
}
});
var headerView = new HeaderView({ model: header });
ConferenceView = Backbone.View.extend({
el: $(".calendar"),
template: _.template($('#calendar').html()),
events: {
},
initialize: function(){
//this.listTracks();
this.collection = new Calendar();
this.collection.fetch();
this.collection.bind("reset", this.render, this);
},
render: function(){
var JSONdata = this.collection.toJSON();
this.$el.html(this.template({days: JSONdata}));
},
listTracks: function() {
}
});
var conferenceView = new ConferenceView({model:conferenceDay});
My current routing
var AppRouter = Backbone.Router.extend({
routes: {
'' : 'index',
'day/:id' : 'changeDay'
},
initialize: function() {
},
index: function() {
},
changeDay: function(id){
alert("changed");
this.calender.changeDay(id);
this.dayView = new ConferenceView({model:conferenceDay});
$('#calender').html(this.dayView.render().el).text('test');
},
});
var app = {
init: function() {
var routes = new AppRouter();
Backbone.history.start({pushState: true});
}
}
app.init();
Ideally, I would like the user to click the day-link button and have the url update via push state to the day/:id and then the #calender template would update with the correct model info received from the day update.
There's a lot of code in your post, so I'm not 100% sure the below will cover everything you need to do, but it's a start
This event handler might be causing some problems:
changeDay: function(e){
AppRouter.history.navigate($(this).attr('href'));
return false;
}
On a detail level, couple of things are off here:
You don't need to reference history. I'm not sure that the router even has such property. You should call AppRouter.navigate instead.
If you want the router to trigger your changeDay route method, you need to pass an option trigger:true, like so:
AppRouter.navigate($(this).attr('href'), {trigger:true}).
However, the actual solution is still simpler than that. You can remove the HeaderView.changeDay event handler, and the click a.day-link event binding from the events hash entirely. Backbone Router will detect the changed URL, and call the router method which matches the new URL automatically.
Related
In my App i have created a View. this View is composed of a Template like a little Form. The Form has an button and in my View i create an click event to handle this button to create a new instance of another View passing the Form data to this View and put the data on html element. The problem is: if i enter in home route or in product 3 times and send a Form data, will appears 3 same Form datas.
Form view
window.userFormView = Backbone.View.extend({
el:$("#principal"),
events : {
'click .userButton' : 'newUser'
},
initialize:function(){
this.template = _.template($("#userFormView").html());
},
newUser : function(ev) {
ev.preventDefault();
//criamos uma nova instancia do model
window.user_view = new userViewes({model: users});
var u = { nome : $("#iName").val() ,sobrenome : $("#iLName").val() };
var user = new userModel(u);
users.add(user);
console.log(users);
return false;
},
render: function() {
this.$el.html("");
this.$el.html(this.template);
}
});
Form Template View
<script type="text/template" id="userFormView">
<form action="" id="form-new-user" class="formulario">
<span class="label">Name?</span><input type="text" id="iName" class="input">
<span class="label">Last Name?</span><input type="text" id="iLName" class="input">
<button class="userButton">Send</button>
<hr>
</form>
</script>
and my route are like this:
window.AppRouter = Backbone.Router.extend({
//
// Definindo rotas
//
routes: {
'home': 'index',
'product': 'productsList',
'foo1': 'doNothing1',
'foo2': 'doNothing2'
},
index: function () {
window.users = new userCollections();
window.userForm = new userFormView();
},
productsList : function() {
window.pCollection = new productCollections();
window.produtoForm = new produtoFormView();
},
doNothing1: function () {
console.log('doNothing1()');
},
doNothing2: function () {
console.log('doNothing2()');
}
});
window.router = new AppRouter();
Backbone.history.start();
userViewes view
window.userViewes = Backbone.View.extend({
// model: users,
el: $("#userContainer"),
initialize: function(){
this.model.on("add", this.render, this);
this.model.on("remove", this.render, this);
},
render: function() {
var self = this;
self.$el.html("");
this.model.each(function(user, indice) {
self.$el.append((new userView({model: user })).render().$el);
});
return this;
}
});
and finally userView:
window.userView = Backbone.View.extend({
//model: new userModel(),
tagName : 'div',
class : "userName",
events :{
'click .editar' : 'editar',
'click .remover' : 'remover',
'blur .sobrenome': 'fechar',
'keypress .sobrenome' : 'onEnterUpdate',
},
editar : function(ev) {
ev.preventDefault();
this.$('.sobrenome').attr('contenteditable', true).focus();
},
fechar : function(ev) {
var sobrenome = $(".sobrenome").text();
this.model.set("sobrenome", sobrenome);
$(".sobrenome").val();
this.$(".sobrenome").removeAttr("contenteditable");
},
onEnterUpdate : function(ev) {
var self = this;
if(ev.keyCode == 13) {
self.fechar();
_.delay(function(){
self.$(".sobrenome").blur();
}, 100);
}
},
remover : function(ev) {
ev.preventDefault();
window.users.remove(this.model);
},
initialize: function(){
this.template = _.template($("#userTemplate").html());
},
render : function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
When your view is using el option, make sure you clean up the existing view before you make a new one.
As it is, every time you switch between routes (without a full page refresh) a new instance pointing to same element is created which causes more and more event handlers to be bound to the el element which is in DOM, and the views stay in memory because of the binding. Try something like:
index: function () {
window.users = window.users || new userCollections();
if(window.userForm){
// clean up is important
window.userForm.remove();
}
window.userForm = new userFormView();
},
And of course, instead of repeating similar code in all routes, have a variable like this.currentView that points to the active view, and a common function that does necessary clean up
P.S: Adding properties to window object is a bad practice. Create your own name space or use the Router instance instead of window
I have found the answer. i implemented singleton pattern to get only one instance of the object. follow the code:
var single = (function(){
function createInstance() {
window.userForm = new userFormView();
window.users = new userCollections();
}
function users() {
return window.users;
}
function userForm() {
return window.userForm;
}
return {
init : function() {
if(!window.users && !window.userForm) {
createInstance();
}else{
this.render();
}
},
render: function() {
window.userForm.render();
}
}
}());
single.init();
Sorry for my bad English. Tell me why the following happens:
I have some backbone collection:
var Background = window.Models.Background = Backbone.Model.extend({});
var Backgrounds = window.Models.Backgrounds = Backbone.Collection.extend({
model: window.Models.Background,
url: '/backgrounds/',
initialize: function() {
this.fetch({
success: this.fetchSuccess(this),
error: this.fetchError
});
},
fetchSuccess: function( collect_model ) {
new BackgroundsView ({ collection : collect_model });
},
fetchError: function() {
throw new Error("Error fetching backgrounds");
}
});
And some view:
var BackgroundsView = window.Views.BackgroundsView = Backbone.View.extend({
tagName: 'div',
className: 'hor_slider',
initialize: function() {
this.render();
},
render: function() {
console.log(this.collection);
this.collection.each( function (background) {
console.log(background);
//var backgroundView = new BackgroundView ({ model: background });
//this.$el.append(backgroundView.render().el);
});
}
});
now i creating collection
var backgrounds = new Models.Backgrounds();
but when I must render this view, in the process of sorting the collection its length is 0, but should be two. This log I see at console. How is this possible? What am I doing wrong??
You are creating the view before the collection fetch is successfull. Your code should be:
initialize: function() {
this.fetch({
success: this.fetchSuccess,
//------------------------^ do not invoke manually
error: this.fetchError
});
},
fetchSuccess: function(collection, response) {
new BackgroundsView ({ collection : collection});
},
You should let backbone call fetchSuccess when the fetch succeeds. Right now you're invoking the funcion immediately and passing the return value undefined as success callback.
This looks like a wrong pattern. Your data models shouldn't be aware of/controlling the presentation logic.
You have a view floating around without any reference to it. You should be creating a view instance with reference(for example from a router, or whatever is kick starting your application) and passing the collection to it. Then fetch the collection from it's initialize method and render after the fetch succeeds. Collection can be referenced via this.collection inside view.
Alternatively you can fetch the collection from router itself and then create view instance. Either way collection/model shouldn't be controlling views.
If the code is structured in the following way, the problem is solved. It was necessary to add a parameter reset to fetch.
var Background = window.Models.Background = Backbone.Model.extend({});
var Backgrounds = window.Models.Backgrounds = Backbone.Collection.extend({
model: window.Models.Background,
url: '/backgrounds/',
initialize: function() {
this.fetch({
reset : true,
});
}
});
var BackgroundsView = window.Views.BackgroundsView = Backbone.View.extend({
tagName: 'div',
className: 'hor_slider',
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.collection.each( function (background) {
var backgroundView = new BackgroundView ({ model: background });
this.$el.append(backgroundView.render().el);
}, this);
$('#view_list').empty();
$('#view_list').append(this.$el);
return this;
}
});
I am running the following view:
app.OrganisationTab = Backbone.View.extend({
el : "#organisations",
template : _.template( $("#tpl-groups-list").html() ),
events : {
"click .js-edit-group" : "editGroup"
},
initialize: function() {
this.listenTo(this.collection, 'change', this.change);
var that = this;
this.collection.fetch({
success: function() {
that.render();
}
})
},
change: function() {
//this.$el.empty();
console.log("collection has changed");
},
render:function() {
this.$el.empty();
this.addAll();
return this;
},
addAll: function() {
this.collection.each(this.addOne, this);
},
addOne: function(model) {
var view = new app.GroupEntry({
model: model
});
this.$el.append(view.render().el);
},
editGroup: function(e) {
e.preventDefault();
var elm = $(e.currentTarget),
that = this;
$('#myModal').on('hidden.bs.modal', function () {
$('.modal-body').remove();
});
var organisation = this.collection.findWhere({ id : String(elm.data('groupid')) });
var members = organisation.get('users');
organisation.set('members', new app.UserCollection(members));
var projects = organisation.get('projects');
organisation.set('projects', new ProjectCollection(projects));
var orgForm = new app.createOrganisationForm({
model : organisation,
});
$('#myModal').modal({
backdrop: 'static',
keyboard: false
});
}
});
This view triggers a new view, and in that I can change a model save it (sends a PUT) and I can get in my console, collection has changed. If I console.log this collection I can see that the collection has changed. If I try and re-render the page all I see are the models as they were without the edits.
Why would this be happening, when clearly the collection is getting changes as it fires the events and I can see it when I log the collection?
After reading your comment:
No sorry on collection change I try to run render() which should empty
the container, and add all the models...but it seems to render the old
collection again.
You're getting this problem because you are overriding the success handler for the fetch call. That success callback is triggered before the models are placed in the collection. You need to listen to the sync event if you want render after the collection has been synchronized with the server (models are updated after fetch).
Update initialize to:
initialize: function() {
this.listenTo(this.collection, 'change', this.change);
this.listenTo(this.collection, 'sync', this.render);
this.collection.fetch();
},
I have a few tabs on my page, whose contents (consisting of many SetView contained by SetListView) are loaded using Backbone.js whenever its tabs is being clicked on.
Problem:: When the user switches from a tab to a previously loaded/viewed tabbed, the contents load again and append to the previously loaded content in SetListView. I can get it to clear the previously loaded contents before loading it again, but it seems to be less than optimal to keep loading the same content.
Is it possible to make Backbone.js store existing content for a tab and not load it multiple times when switching back to the same tab?
Views
// Views
var SetListView = Backbone.View.extend({
el: '#set_list',
initialize: function() {
this.collection.bind('reset', this.render, this);
},
render: function() {
this.collection.each(function(set, index) {
$(this.el).append( new SetView({ model: set }).render().el );
}, this);
return this;
}
});
var SetView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
template: _.template( $('#tpl_SetView').html() ),
initialize: function() {
this.model.on('destroy', this.close, this);
},
render: function() {
$(this.el).html( this.template( this.model.toJSON() ) );
return this;
},
close: function() {
this.unbind();
this.remove();
}
});
Router
// Router
var AppRouter = Backbone.Router.extend({
routes: {
'': 'sets',
'sets': 'sets'
},
viewing_user_id: $('#viewing_user_id').val(),
sets: function() {
this.showTab('sets');
this.setList = new SetCollection();
this.setListView = new SetListView({ collection: this.setList });
var self = this;
this.setList.fetch({
data: {user_id: self.viewing_user_id},
processData: true
});
},
showTab: function(tab) {
// Show/hide tab contents
$('.tab-content').children().not('#tab_pane_' + tab).hide();
$('.tab-content').children('#tab_pane_' + tab).fadeIn('fast');
// Activate/deactivate tabs
$('#tab_' + tab).addClass('active');
$('#tab_' + tab).siblings().removeClass('active');
}
});
Backbone has not any in-house system to difference between when you want to re-fetch the content or re-using the already fetched one. You have to decide when do each of this actions.
A modification of your example code to achieve this can be:
var AppRouter = Backbone.Router.extend({
// ... more router code
sets: function() {
if( !this.setList ) this.initializeSets();
this.showTab('sets');
},
initializeSets: function(){
this.setList = new SetCollection();
this.setListView = new SetListView({ collection: this.setList });
var self = this;
this.setList.fetch({
data: {user_id: self.viewing_user_id},
processData: true
});
},
});
So you only call initializeSets() if they are not already initialized. Of course will be more elegant and clean ways to ask if the sets have been initialized but this is up to you.
I'm new to backbone.js and MVC so apologise if this is a silly question...
I have been experimenting with some of the backbone.js tutorials out there and am trying to work out how to load an initial set of data onto the page.
If anyone could point me in the right direction or show me the what I'm missing below, it would be greatly appreciated!
Thanks!
The code is below or at: http://jsfiddle.net/kiwi/kgVgY/1/
The HTML:
Add list item
The JS:
(function($) {
Backbone.sync = function(method, model, success, error) {
success();
}
var Item = Backbone.Model.extend({
defaults: {
createdOn: 'Date',
createdBy: 'Name'
}
});
var List = Backbone.Collection.extend({
model: Item
});
// ------------
// ItemView
// ------------
var ItemView = Backbone.View.extend({
tagName: 'li',
// name of tag to be created
events: {
'click span.delete': 'remove'
},
// `initialize()` now binds model change/removal to the corresponding handlers below.
initialize: function() {
_.bindAll(this, 'render', 'unrender', 'remove'); // every function that uses 'this' as the current object should be in here
this.model.bind('change', this.render);
this.model.bind('remove', this.unrender);
},
// `render()` now includes two extra `span`s corresponding to the actions swap and delete.
render: function() {
$(this.el).html('<span">' + this.model.get('planStartDate') + ' ' + this.model.get('planActivity') + '</span> <span class="delete">[delete]</span>');
return this; // for chainable calls, like .render().el
},
// `unrender()`: Makes Model remove itself from the DOM.
unrender: function() {
$(this.el).remove();
},
// `remove()`: We use the method `destroy()` to remove a model from its collection.
remove: function() {
this.model.destroy();
}
});
// ------------
// ListView
// ------------
var ListView = Backbone.View.extend({
el: $('body'),
// el attaches to existing element
events: {
'click button#add': 'addItem'
},
initialize: function() {
_.bindAll(this, 'render', 'addItem', 'appendItem'); // every function that uses 'this' as the current object should be in here
this.collection = new List();
this.collection.bind('add', this.appendItem); // collection event binder
this.render();
},
render: function() {
_(this.collection.models).each(function(item) { // in case collection is not empty
appendItem(item);
}, this);
},
addItem: function() {
var item = new Item();
var planStartDate = $('#planStartDate').val();
var planActivity = $('#planActivity').val();
item.set({
planStartDate: planStartDate,
planActivity: planActivity
});
this.collection.add(item);
},
appendItem: function(item) {
var itemView = new ItemView({
model: item
});
$('ul', this.el).append(itemView.render().el);
}
});
var listView = new ListView();
})(jQuery);
Thanks.
Here's the modified example: http://jsfiddle.net/kgVgY/2/
You create the collection first with the data you want
var list = new List([{
createdOn: 'Jan',
createdBy: 'John',
planStartDate: "dfd",
planActivity: "dfdfd"
}]);
and then pass the collection to the view you want
var listView = new ListView({collection: list});
That's about all you had wrong in this code. Few minor unrelated notes:
You were using _(this.collection.models).each. Backbone collections use underscore to expose all those functions on themselves, so that is equivalent to this.collection.each
You don't really need the "unrender" method on the ItemView but since you aren't using that I'm guessing you're using it for debugging.