I'm using backbone with the backbone-rails gem which does its own templating and project structure.
The problem is that it puts 4 different views on one div, so what i've done is made another div and now the model, show,edit views are assigned to that other view, basically so i can have a list on the left side of the page and everything else in the middle.
The problem is that i can't redirect now, so when i update or create a new 'note' the list view does not refresh.
List View:
Supernote.Views.Notes ||= {}
class Supernote.Views.Notes.IndexView extends Backbone.View
template: JST["backbone/templates/notes/index"]
initialize: () ->
#options.notes.bind('reset','change', #addAll)
addAll: () =>
#options.notes.each(#addOne)
addOne: (note) =>
view = new Supernote.Views.Notes.NoteView({model : note, collection: #options.notes})
#$("li").append(view.render().el)
render: =>
$(#el).html(#template(notes: #options.notes.toJSON() ))
#addAll()
return this
Edit View:
Supernote.Views.Notes ||= {}
class Supernote.Views.Notes.EditView extends Backbone.View
template : JST["backbone/templates/notes/edit"]
events :
"submit #edit-note" : "update"
update : (e) ->
e.preventDefault()
e.stopPropagation()
#model.save(null,
success : (note) =>
#model = note
window.location.hash = "/#{#model.id}"
)
render : ->
$(#el).html(#template(#model.toJSON() ))
this.$("form").backboneLink(#model)
return this
Events is what you need,
when a model is added to a collection (new note)
it raises the add event on the collection itself
so in your collection you can catch that and do something with it.
var myCollection = Backbone.Collection.extend({
//... whole lot of irrelevant stuff goes here :)
});
var myCollectionListView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, 'onAdd');
this.collection.bind('add', this.onAdd);
}
onAdd: function(m){
// either re-render the whole collection
this.render();
// or do something with the single model
var item = $('<li>' + m.get('property') + '</li>');
$('#listview').append(item);
}
});
var myItems = new myCollection({});
var listview = new myCollectionListView({ collection: myItems });
then you have the 'add note' covered, (the exact same you could do for the reset or remove event which handle resetting the collection with a new list of models, and deleting a model from the collection )
let's say you update a note, this should be done with the same event system, though the change event could be used for that.
the trick here is, your list view renders not the model elements itself, but the list view creates a modelview for every model. in that modelview (you called it NoteView) you could do the same process as above,
and bind to it's own model:
initialize: function() {
this.bind('change', this.modelChanged);
},
modelChanged: function(m){
// do something, re-render the view, or anything else...
}
Related
Given a panel
var panel = new Backbone.CollectionView({...})
How do I get the current model being sorted?
panel.on('sortStart', function(e) {
var index = something;
});
I suppose you use some kind of UI manipulation tool for example jQuery UI. As Lesha said in her comment it can be done through triggering of event on the model view.
//creting children view
var PanelItem = Backbone.View.extend({
events: {
"sortStart": "sortEventPropagation"
},
initialize : function (options) {
this.parentView = options.parentView;
},
sortEventPropagation: function(){
this.parentView.trigger('sort:start:propagated', this.model);
},
})
Everytime you are creating panelItem view you need to pass it panel in options as parentView.
var childView = new PanelItem({
parentView: panel
})
And on panel you could easily listenTo sort:start:propagated event
var Panel = Backbone.CollectionView.extend({
initialize: function(){
this.listenTo(this, 'sort:start:propagated', function(model){
//Do magic with model
})
},
})
I'm trying to add an event to a collection. I want to re-render the view every time the collection changes (new model, model attribute changes, etc). Here is my code:
var app = {}; // custom name space
// models
app.Group = Backbone.Model.extend({
url: '/group'
});
app.Category = Backbone.Model.extend({
url: '/category'
});
// collections
app.GroupList = Backbone.Collection.extend({
model: app.Group,
url: 'data/getGroups.php' // '/groups'
});
app.CategoryList = Backbone.Collection.extend({
model: app.Category,
url: 'data/getCategories.php' // '/categories'
});
app.groupList = new app.GroupList();
app.categoryList = new app.CategoryList();
// views
app.CategoriesView = Backbone.View.extend({
...
});
// bind events
app.GroupList.on('change reset add remove', app.CategoriesView.render);
app.CategoryList.on('change reset add remove', app.CategoriesView.render);
...but, I get the following error in the console "TypeError: app.GroupList.on is not a function". I've tried doing the same for the model instead of the collection but same error - ".on" is not a function. In the documentation it seems "on" belongs to models at least but as mentioned this didn't work either. How is the correct way to add a listener? should I be doing so on the model or the collection?
If anyone can offer any help it would be much appreciated. Thanks
You can only bind events to instances:
app.groupList.on(...);
In the same vein, render is only for a view instance:
// views
app.CategoriesView = Backbone.View.extend({
...
});
var myView = new app.CategoriesView({
// somewhere in here you'll pass app.grouplist...
});
app.groupList.on('change reset add remove', myView.render);
I just don't have idea what causes problem and need help. Before posting I've came up to alternative solution, but I want to learn why this is not working properly.
I have router that initialize view which initialize entity collection and views like so:
advertiser_manage_campaign: function () {
this.campaignListView = new window.CampaignListView;
this.mainSidebar = new window.MainSidebar;
},
CampaignListView:
window.CampaignListView = Backbone.View.extend({
el: ("#right_column"),
initialize: function () {
this.render();
this.campaignCollection = new Campaign.CampaignCollection;
this.campaignCollectionView = new Campaign.CampaignCollectionView({ model: this.campaignCollection });
this.campaignCollection.fetch();
},
events: {
"click .campaign_dialog": "openCampaignDialog"
},
openCampaignDialog: function (e) {
var that = this;
var itemID = $(e.target).attr("item-id");
var model = {}; //model to populate dialog inputs
if (!isNaN(itemID))
model = this.campaignCollection.get(itemID).toJSON(); //get existing model from collection <- after described procedure, error
Campaign.Dialog.open(model, function (data) {
if (isNaN(itemID)) {//model does not exist, create
that.campaignCollection.create(data, { wait: true,
error: function (model, error) {
dialoger.showErrors(JSON.parse(error.responseText).errors);
},
success: function (mdl, response) { window.Campaign.Dialog.close(); }
});
} else {//model exist, update
model = that.campaignCollection.get(itemID);
model.save(data, { wait: true,
error: function (mdl, error) {
dialoger.showErrors(JSON.parse(error.responseText).errors);
},
success: function (mdl, response) { window.Campaign.Dialog.close(); }
});
}
});
return false;
},
render: function () {
$(this.el).html(window.Templates.getHTML("campaign_list_view", {}));
$(".button", $(this.el)).button();
}
});
-
openCampaignDialog
is for both edit models and creating new. Every view(table row) of model
has button with class ".campaign_dialog" and there is button for adding new model with same class.
Campaign.Dialog.open
shows dialog populated with model and in callback returns JSON from dialog form.
If I create new model via dialog, I can edit it right away, but when I create new model, change view, back to this view, create again new model, change view and then again back, click edit on last added item, I get error on commented line as model with this ID is not in collection, although it is. Response from server is OK. Obviously, I'm doing something wrong and after one day, I don't see what it is.
Alternative solution I've came up to is to create and populate dialog from event of model view (this works), but I thought that CampaingCollectionView or CampaingView should not deal with adding or editing models so I've implemented this in 'higher' view.
Thanks everyone for helping me...
Edit:
var CampaignCollectionView = Backbone.View.extend({
el: (".content_table tbody"),
initialize: function () {
this.model.bind("reset", this.render, this);
this.model.bind("add", this.add, this);
},
render: function () {
$(this.el).empty();
_.each(this.model.models, function (campaign) {
$(this.el).append(new CampaignView({ model: campaign }).render().el);
}, this);
return this;
},
add: function (model) {
window.Appender.AppendAndScroll($(new CampaignView({ model: model }).render().el), this.el);
}
});
I've found solution.
Problems arise, though, when we bind objects together through these
events but we don’t bother unbinding them. As long as these objects
are bound together, and there is a reference in our app code to at
least one of them, they won’t be cleaned up or garbage collected. The
resulting memory leaks are like the zombies of the movies – hiding in
dark corners, waiting to jump out and eat us for lunch.
Source: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Author suggests unbinding mechanism, but I'm going to reuse same objects if exist.
Router:
advertiser_manage_campaign: function () {
if (!this.campaignListView)
this.campaignListView = new window.CampaignListView;
else
this.campaignListView.initialize();
this.mainSidebar = new window.MainSidebar;
},
If someone thinks this is not best solution, I would like to hear why.
Thank you all who tried to help!
I have a collection of flash cards that are tied to a Backbone collection. Once I get the collection, I create an instance of a player model.
Then the user can navigate through the rest of the flash cards using the "next" and "previous" buttons. My first stab in doing this which I thought was simple was to pass the flashCards to a player like this.
Unfortunately, this design is causing the next and previous button events to be bound every time they are clicked. So, after the first time clicking on the next button for example, the event starts firing more than once. I read somewhere about ghost views, but could not exactly figure out how I can break the code below into a chunk that will help me prevent the ghost view issue.
var flashCards = new Quiz.Collections.FlashCards({
id: this.model.get('card_set_id')
});
Quiz.player = new Quiz.Models.FlashCardPlayer({
collection: flashCards
})
Quiz.Models.FlashCardPlayer = Backbone.Model.extend({
defaults: {
'currentCardIndex': 0
},
initialize: function(){
this.collection = this.get('collection');
this.showCard();
},
showCard: function(){
var flashCard = this.collection.at(this.get('currentCardIndex'));
var cardView = new Quiz.Views.FlashCardPlayer({
model: flashCard
});
},
currentFlashCard: function(){
return this.get('currentCardIndex');
},
previousFlashCard: function(){
var currentFlashCardIndex = parseInt(this.get('currentCardIndex'), 10);
if(currentFlashCardIndex <= 0){
console.log("no less");
}
this.set({
'currentCardIndex': currentFlashCardIndex--
});
this.showCard();
},
nextFlashCard: function(){
var currentFlashCardIndex = parseInt(this.get('currentCardIndex'), 10);
if(currentFlashCardIndex >= this.collection.length){
console.log("no more");
}
currentFlashCardIndex = currentFlashCardIndex + 1;
this.set({
'currentCardIndex': currentFlashCardIndex
});
console.log(this.get('currentCardIndex'));
this.showCard();
}
});
Quiz.Views.FlashCardPlayer = Backbone.View.extend({
el: $('#cardSet'),
tagName: 'div',
_template: _.template($('#playerTemplate').html()),
initialize: function(){
console.log("in view flashcardplayer", this);
this.render();
},
events: {
'click #previous': 'getPreviousCard',
'click #next': 'getNextCard'
},
render: function(){
$(this.el).html(this._template(this.model.toJSON()));
return this;
},
getPreviousCard: function(){
this.close();
Quiz.player.previousFlashCard();
},
getNextCard: function(){
this.close();
Quiz.player.nextFlashCard();
}
});
script#playerTemplate(type="text/template")
<div id="state"></div>
<div id="previous">Previous</div>
<div id="card">
<h2><%= question %></h2>
<h3><%= answer %></h3>
</div>
<div id="next">Next</div>
You're creating a new instance of Quiz.Views.FlashCardPlayer each time you show a new card. Each of these instances does its own event handling, so each instance is binding to the same #next and #previous elements.
I think there are a couple of conceptual issues here:
You only need one FlashCardPlayer view, which should bind events on the next/previous elements. You probably ought to have a separate FlashCard view, which displays a single card, and the player can swap those views in and out as the next/previous buttons are pressed. As a general rule, if you have an element with an id, you should only be rendering and binding to it once, with a single view instance, otherwise you end up with the same issue you have now.
You're trying to stuff way too much into the FlashCardPlayer model. As a rule, models should only know about their data, not about the views used to display them (in part because one model might need to be displayed in a variety of views). I don't mind having the nextFlashCard() and previousFlashCard() methods on the model, as this is still in the realm of storing data about the collection, but the showCard() method is really moving squarely into view territory, as it deals with presentation logic. A much better idea would be to have your view bind to the change:currentCardIndex event on the model and handle the display of the new card, using this.model.get('currentCardIndex')) (or a new getCurrentCard() method) to get it.
I am stuck on the following issue:
I have a model with a property that defines if it is visibly selected or not, which I will call SelectModel for the purpose of this question.
SelectModel = Backbone.Model.extend({
defaults:{
isSelected: false
}
})
Now the first part that I do not really get is how I should handle the selection in general.
If I want to use the observer pattern, my View should listen to the change of the isSelected property. But my view also triggers this in the first place, so I would have.
SelectView = Backbone.View.extend({
initialize: function(){
this.model.bind("change:isSelected", this.toggleSelectionVisually)
},
events: {
"click" : toggleSelection
},
toggleSelection: function(){
this.model.set({"isSelected": !this.model.get("isSelected");
},
toggleSelectionVisually:(){
//some code that shows that the view is now selected
},
})
So this in itself already feels a bit absurd but I guess I just understand something wrong.
But the part which I really fail to implement without making my code horrible is handling the selection for multiple models that only one model is selected at a time.
SelectListView = Backbone.View.extend({
initialize: function(){
this.collection = new SelectList();
},
toggleSelection: function(){
????
}
})
So who should notify whom of the selection change? Which part should trigger it and which part should listen? I am really stuck on this one. For a single View it is doable, for a collection I am sadly lost.
I would have suggested the following simplification for your SelectView until I saw the second part of your question:
SelectView = Backbone.View.extend({
events: {
"click" : toggleSelection
},
toggleSelection: function(){
this.model.set({"isSelected": !this.model.get("isSelected");
//some code that shows whether the view is selected or not
}
});
However, since the isSelected attribute is apparently mutually exclusive, can be toggled off implicitly when another one is toggled on, I think the way you have it is best for your case.
So, using your existing SelectView and, you could have a SelectListView as follows. WARNING: it iterates over your entire collection of models each time one is selected. If you will have a large number of models this will not scale well, and you'll want to cache the previously-selected model rather than iterating over the entire collection.
SelectListView = Backbone.View.extend({
initialize: function(){
this.collection = new SelectList();
this.collection.bind('change:isSelected', this.toggleSelection, this);
},
toggleSelection: function(toggledModel){
//A model was toggled (on or off)
if(toggledModel.get('isSelected') {
//A model was toggled ON, so check if a different model is already selected
var otherSelectedModel = this.collection.find(function(model) {
return toggledModel !== model && model.get('isSelected');
});
if(otherSelectedModel != null) {
//Another model was selected, so toggle it to off
otherSelectedModel.set({'isSelected': false});
}
}
}
});
I would recommend that your model not keep track of this, but rather the view.
In my mind the model has nothing to do with its display, but rather the data that you're tracking. The view should encapsulate all the info about where and how the data is displayed to the user
So I would put isSelected as an attribute on the view. Then it's trivial to write a method to toggle visibility. If you then need to explain the other views that a specific view is selected you can attach a listener $(this.el).on('other_visible', toggle_show) which you can trigger on your toggle_visibility method with $(this.el).trigger('other_visible')
Very close to the solution suggested by #rrr but moving the logic from the View to the Collection where I think it bellows to:
SelectsCollection = Backbone.Collection.extend({
initialize: function() {
this.on( "change:selected", this.changeSelected );
},
changeSelected: function( model, val, opts ){
if( val ){
this.each( function( e ){
if( e != model && e.get( "selected" ) ) e.set( "selected", false );
});
};
},
});
There are different ways you could do it. You could trigger an event on the collection itself and have all the SelectModel instances listen for it and update themselves accordingly. That seems a bit wasteful if you have a lot of SelectModel instances in the collection because most of them won't end up doing any work. What I would probably do is keep track of the last SelectModel in your View:
SelectListView = Backbone.View.extend({
initialize: function(){
this.collection = new SelectList();
this.lastSelectedModel = null;
},
toggleSelection: function(){
// populate newSelectedModel with the SelectedModel that you're toggling
var newSelectedModel = getNewSelectedModel();
if (!newSelectedModel.get('isSelected')) {
// if the SelectModel isn't already selected, we're about to toggle it On
// so we need to notify the previously selected SelectModel
if (this.lastSelectedModel) {
this.lastSelectedModel.set({isSelected: false});
}
this.lastSelectedModel = newSelectedModel;
} else {
// if the newSelectedModel we're about to toggle WAS already selected that means
// nothing is selected now so clear out the lastSelectedModel
this.lastSelectedModel = null;
}
newSelectedModel.set({isSelected: !newSelectedModel.get('isSelected')});
}
})