I am trying to bind events to elements that are placed by appending a backbone template:
appendEditTemplateAndSetEvents: function() {
var associatedCollection = App.Helpers.findAssociatedCollection(this.allCollections, this.associatedCollectionId);
var template = this.setEditTemplateForElement(associatedCollection.type);
var modalBody = this.$el.find('.modal-body');
modalBody.empty();
var firstModel = associatedCollection.at(0);
if(template.mainTemplate !== null) {
modalBody.append($('#edit-form-element-frame').html());
//each mode in collection
associatedCollection.each(function(model){
if(model.get('positionInContainer') === 1) {
firstModel = model;
}
console.log(model.attributes);
modalBody.find('.elements-in-editmodal-wrapper').append(template.mainTemplate(model.toJSON()));
});
}
if( template.templateValidation.length !== 0 ) {
modalBody.append('<hr><h3>Validateregels</h3>');
_.each(template.templateValidation, function(val, index) {
modalBody.append(val(firstModel.toJSON()));
});
}
//set listeners and handlers that apply when a edit modal is open
this.validationEventsForEditModal(firstModel);
this.editErrorMessagesInModal(firstModel);
},
Now the problem is that when the last two functions are called the html of the templates isn't appended yet so the the events are binded to an object with a length of 0.
Does anyone have a decent solution for this async problem? I tried $.Defferred but that did not work, but maybe someone get's it working.
I solved this by using this.$el.find(...) in the functions:
this.validationEventsForEditModal(firstModel);
this.editErrorMessagesInModal(firstModel);
I don't know if it's still an async problem, but this solves it.
Related
I am using Marionette region to display templates based on user radio input:(text/file).
Here is my itemview
var fileTemplateView = Marionette.ItemView.extend({
template : "#file-upload-template"
});
and region defined as
regions : {
composeRegion : "#compose-region",
}
and event declared as
events : {
"click #msg-input-type input:radio" : "changedRadio"
}
and event trigger function is
changedRadio : function(evt) {
var self = this;
var checkedObject = evt.currentTarget;
console.log('Radio Change Event'+checkedObject.value);
if (checkedObject.value === "file") {
if (self.fileView === undefined) {
self.fileView = new fileTemplateView();
}
this.composeRegion.show(self.fileView, { preventDestroy: true });
} else if (checkedObject.value === "text") {
if (self.textView === undefined) {
self.textView = new textTemplateView();
}
this.composeRegion.show(self.textView, { preventDestroy: true });
}
But preventDestroy method may not be working as defined where template is resetting on everytime radio event happen.
Your help is appreciated.
The preventDestroy option prevents the swapped view from being destroyed. This doesn't mean that is won't be re-rendered the next time it is shown. Make sure you are saving the state of the view so it can be used to reconstruct the view properly the next time.
I been trying to do checkbox Checkall and UnCheckall using subscribe and i'm partially successful doing that but i am unable to find a fix in couple of scenarios when i am dealing with subscribe .
Using subscribe :
I am here able to checkAll uncheckAll but when i uncheck a child checkbox i.e test1 or test2 i need my parent checkbox name also to be unchecked and in next turn if i check test1 the parent checkbox should be checked i.e keeping condition both child checkboxes are checked .
For fiddle : Click Here
ViewModel :
self.selectedAllBox.subscribe(function (newValue) {
if (newValue == true) {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(true);
});
} else {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(false);
});
}
});
The same scenario can be done perfectly in easy way using computed but due some performance issues i need to use subscribe which is best way it wont fire like computed onload .
Reference : Using computed same thing is done perfectly check this Fiddle
I tried to use change event in individual checkbox binding but its a dead end till now.
Any help is appreciated .
Your subscription only applies to edits on the selectedAllBox. To do what you want, you'll need subscriptions on every Person checkbox as well, to check for the right conditions and uncheck the selectedAllBox in the right situations there.
It strikes me as odd that this would be acceptable but using computed() is not. Maybe you should reconsider that part of your answer. I would much rather compute a "isAllSelected" value based on my viewModel state, then bind the selectedAllBox to that.
I solved a similar problem in my own application a couple of years ago using manual subscriptions. Although the computed observable method is concise and easy to understand, it suffers from poor performance when there's a large number of items. Hopefully the code below speaks for itself:
function unsetCount(array, propName) {
// When an item is added to the array, set up a manual subscription
function addItem(item) {
var previousValue = !!item[propName]();
item[propName]._unsetSubscription = item[propName].subscribe(function (latestValue) {
latestValue = !!latestValue;
if (latestValue !== previousValue) {
previousValue = latestValue;
unsetCount(unsetCount() + (latestValue ? -1 : 1));
}
});
return previousValue;
}
// When an item is removed from the array, dispose the subscription
function removeItem(item) {
item[propName]._unsetSubscription.dispose();
return !!item[propName]();
}
// Initialize
var tempUnsetCount = 0;
ko.utils.arrayForEach(array(), function (item) {
if (!addItem(item)) {
tempUnsetCount++;
}
});
var unsetCount = ko.observable(tempUnsetCount);
// Subscribe to array changes
array.subscribe(function (changes) {
var tempUnsetCount = unsetCount();
ko.utils.arrayForEach(changes, function (change) {
if (change.moved === undefined) {
if (change.status === 'added') {
if (!addItem(change.value))
tempUnsetCount++;
} else {
if (!removeItem(change.value))
tempUnsetCount--;
}
}
});
unsetCount(tempUnsetCount);
}, null, 'arrayChange');
return unsetCount;
}
You'll still use a computed observable in your viewmodel for the the select-all value, but now it'll only need to check the unselected count:
self.unselectedPeopleCount = unsetCount(self.People, 'Selected');
self.SelectAll = ko.pureComputed({
read: function() {
return self.People().length && self.unselectedPeopleCount() === 0;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:0});
Example: http://jsfiddle.net/mbest/dwnv81j0/
The computed approach is the right way to do this. You can improve some performance issues by using pureComputed and by using rateLimit. Both require more recent versions of Knockout than the 2.2.1 used in your example (3.2 and 3.1, respectively).
self.SelectAll = ko.pureComputed({
read: function() {
var item = ko.utils.arrayFirst(self.People(), function(item) {
return !item.Selected();
});
return item == null;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:1});
http://jsfiddle.net/mbest/AneL9/98/
In my Backbone app, I have the following
playlistView = new PlaylistView({ model: Playlist });
Playlist.getNewSongs(function() {
playlistView.initialize();
}, genre, numSongs);
Playlist.getNewSongs() is called back when some ajax request is finished. I want to re-initialize the view then. However, I believe the way I'm doing it leads to this problem of a view listening to a same event twice. Is calling initialize() like this acceptable? If not, what should I do instead?
Update:
I wrote this chrome extension in Backbone to learn Backbone, and it's in a design hell at the moment. I am in the middle of refactoring the entire codebase. The snippet below is my PlaylistView initialize() code block.
var PlaylistView = Backbone.View.extend({
el: '#expanded-container',
initialize: function() {
var playlistModel = this.model;
var bg = chrome.extension.getBackgroundPage();
if (!bg.player) {
console.log("aborting playlistView initialize because player isn't ready");
return;
}
this.listenTo(playlistModel.get('songs'), 'add', function (song) {
var songView = new SongView({ model: song });
this.$('.playlist-songs').prepend(songView.render().el);
});
this.$('#song-search-form-group').empty();
// Empty the current playlist and populate with newly loaded songs
this.$('.playlist-songs').empty();
var songs = playlistModel.get('songs').models;
// Add a search form
var userLocale = chrome.i18n.getMessage("##ui_locale");
var inputEl = '<input class="form-control flat" id="song-search-form" type="search" placeholder="John Lennon Imagine">' +
'<span class="search-heart-icon fa fa-heart"></span>'+
'<span class="search-input-icon fui-search"></span>';
}
this.$('#song-search-form-group').append(inputEl);
var form = this.$('input');
$(form).keypress(function (e) {
if (e.charCode == 13) {
var query = form.val();
playlistModel.lookUpAndAddSingleSong(query);
}
});
// Fetch song models from bg.Songs's localStorage
// Pass in reset option to prevent fetch() from calling "add" event
// for every Song stored in localStorage
if (playlistModel.get('musicChart').source == "myself") {
playlistModel.get('songs').fetch({ reset: true });
songs = playlistModel.get('songs').models;
}
// Create and render a song view for each song model in the collection
_.each(songs, function (song) {
var songView = new SongView({ model: song });
this.$('.playlist-songs').append(songView.render().el);
}, this);
// Highlight the currently played song
var currentSong = playlistModel.get('currentSong');
if (currentSong)
var currentVideoId = currentSong.get('videoId');
else {
var firstSong = playlistModel.get('songs').at(0);
if (!firstSong) {
// FIXME: this should be done via triggering event and by Popup model
$('.music-info').text(chrome.i18n.getMessage("try_different_chart"));
$('.music-info').fadeOut(2000);
//console.log("something wrong with the chart");
return;
}
var currentVideoId = firstSong.get('videoId');
}
_.find($('.list-group-item'), function (item) {
if (item.id == currentVideoId)
return $(item).addClass('active');
});
},
It is not wrong but probably not a good practice. You did not post the code in your initialize but maybe you have too much logic here.
If you are simply initializing the view again so that the new data is rendered, you should use event listener as such:
myView = Backbone. View.extend ({
initialize : function() {
// We bind the render method to the change event of the model.
//When the data of the model of the view changes, the method will be called.
this.model.bind( "change" , this.render, this);
// Other init code that you only need once goes here ...
this.template = _.template (templateLoader. get( 'config'));
},
// In the render method we update the view to represent the current model
render : function(eventName) {
$ (this.el ).html(this .template ((this.model .toJSON())));
return this;
}
});
If the logic in your initiialize is something totally else, please include it. Maybe there is a beter place for it.
I am using same el for more than 1 view like below. I'm not facing any problem till now. Is this good approach or should i do any changes?
<div id="app">
<div id="app-header"></div>
<div id="app-container"></div>
<div id="app-footer">
</div>
App View:
{
el: "#app",
v1: new View1(),
v2: new View2(),
render: function () {
if (cond1) {
this.v1.render();
} else if (cond2) {
this.v2.render();
}}
}
View 1:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
View 2:
{
el: "#app-container",
render: function (){
this.$el.html(template);
}
}
By reading your question, I do not really see what advantages you could possibly have using this approach rather than having the different div elements being the root el for your views 1, 2, 3 and using
this.$el.html(template)
in the render method.
Your approach could work for a small application, but I think it will become really hard to maintain as the application grows.
EDIT
I still do not really get your point, you could only initialize everything only once in both cases.
Here is a working Fiddle.
By the way I am changing the content by listening to the click event but this is to simplify the example. It should be done by the router.
I do use a mixin to handle such situation, I call it stated view. For a view with all other options I will send a parameter called 'state', render will in-turn call renderState first time and there after every time I set a 'state' renderState will update the view state. Here is my mixin code looks like.
var setupStateEvents = function (context) {
var stateConfigs = context.getOption('states');
if (!stateConfigs) {
return;
}
var state;
var statedView;
var cleanUpState = function () {
if (statedView) {
statedView.remove();
}
};
var renderState = function (StateView) {
statedView = util.createView({
View: StateView,
model: context.model,
parentEl: context.$('.state-view'),
parentView:context
});
};
context.setState = function (toState) {
if (typeof toState === 'string') {
if (state === toState) {
return;
}
state = toState;
var StateView = stateConfigs[toState];
if (StateView) {
cleanUpState();
renderState(StateView);
} else {
throw new Error('Invalid State');
}
} else {
throw new Error('state should be a string');
}
};
context.getState = function () {
return state;
};
context.removeReferences(function(){
stateConfigs = null;
state=null;
statedView=null;
context=null;
})
};
full code can be seen here
https://github.com/ravihamsa/baseapp/blob/master/js/base/view.js
hope this helps
Backbone Rule:
When you create an instance of a view, it'll bind all events to el if
it was assigned, else view creates and assigns an empty div as el for that view and bind
all events to that view.
In my case, if i assign #app-container to view 1 and view 2 as el and when i initialize both views like below in App View, all events bind to the same container (i.e #app-container)
this.v1 = new App.View1();
this.v2 = new App.View2();
Will it lead to any memory leaks / Zombies?
No way. No way. Because ultimately you are having only one instance for each view. So this won't cause any memory leaks.
Where does it become problematic?
When your app grows, it is very common to use same id for a tag in both views. For example, you may have button with an id btn-save in both view's template. So when you bind btn-save in both views and when you click button in any one the view, it will trigger both views save method.
See this jsFiddle. This'll explain this case.
Can i use same el for both view?
It is up to you. If you avoid binding events based on same id or class name in both views, you won't have any problem. But you can avoid using same id but it's so complex to avoid same class names in both views.
So for me, it looks #Daniel Perez answer is more promising. So i'm going to use his approach.
Given a page that uses Backbone.js to have a Collection tied to a View (RowsView, creates a <ul>) which creates sub Views (RowView, creates <li>) for each Model in the collection, I've got an issue setting up inline editing for those models in the collection.
I created an edit() method on the RowView view that replaces the li contents with a text box, and if the user presses tab while in that text box, I'd like to trigger the edit() method of the next View in the list.
I can get the model of the next model in the collection:
// within a RowView 'keydown' event handler
var myIndex = this.model.collection.indexOf(this.model);
var nextModel = this.model.collection.at(myIndex+1);
But the question is, how to find the View that is attached to that Model. The parent RowsView View doesn't keep a reference to all the children Views; it's render() method is just:
this.$el.html(''); // Clear
this.model.each(function (model) {
this.$el.append(new RowView({ model:model} ).render().el);
}, this);
Do I need to rewrite it to keep a separate array of pointers to all the RowViews it has under it? Or is there a clever way to find the View that's got a known Model attached to it?
Here's a jsFiddle of the whole problem: http://jsfiddle.net/midnightlightning/G4NeJ/
It is not elegant to store a reference to the View in your model, however you could link a View with a Model with events, do this:
// within a RowView 'keydown' event handler
var myIndex = this.model.collection.indexOf(this.model);
var nextModel = this.model.collection.at(myIndex+1);
nextModel.trigger('prepareEdit');
In RowView listen to the event prepareEdit and in that listener call edit(), something like this:
this.model.on('prepareEdit', this.edit);
I'd say that your RowsView should keep track of its component RowViews. The individual RowViews really are parts of the RowsView and it makes sense that a view should keep track of its parts.
So, your RowsView would have a render method sort of like this:
render: function() {
this.child_views = this.collection.map(function(m) {
var v = new RowView({ model: m });
this.$el.append(v.render().el);
return v;
}, this);
return this;
}
Then you just need a way to convert a Tab to an index in this.child_views.
One way is to use events, Backbone views have Backbone.Events mixed in so views can trigger events on themselves and other things can listen to those events. In your RowView you could have this:
events: {
'keydown input': 'tab_next'
},
tab_next: function(e) {
if(e.keyCode != 9)
return true;
this.trigger('tab-next', this);
return false;
}
and your RowsView would v.on('tab-next', this.edit_next); in the this.collection.map and you could have an edit_next sort like this:
edit_next: function(v) {
var i = this.collection.indexOf(v.model) + 1;
if(i >= this.collection.length)
i = 0;
this.child_views[i].enter_edit_mode(); // This method enables the <input>
}
Demo: http://jsfiddle.net/ambiguous/WeCRW/
A variant on this would be to add a reference to the RowsView to the RowViews and then tab_next could directly call this.parent_view.edit_next().
Another option is to put the keydown handler inside RowsView. This adds a bit of coupling between the RowView and RowsView but that's probably not a big problem in this case but it is a bit uglier than the event solution:
var RowsView = Backbone.View.extend({
//...
events: {
'keydown input': 'tab_next'
},
render: function() {
this.child_views = this.collection.map(function(m, i) {
var v = new RowView({ model: m });
this.$el.append(v.render().el);
v.$el.data('model-index', i); // You could look at the siblings instead...
return v;
}, this);
return this;
},
tab_next: function(e) {
if(e.keyCode != 9)
return true;
var i = $(e.target).closest('li').data('model-index') + 1;
if(i >= this.collection.length)
i = 0;
this.child_views[i].enter_edit_mode();
return false;
}
});
Demo: http://jsfiddle.net/ambiguous/ZnxZv/