I am building a gallery-like picker view in Backbone.js. The picker has many thumbnail views bound to models within a collection and a large preview view for the currently selected thumbnail. Clicking a thumbnail will fire a Backbone.Event to change the preview view's model. The preview view, however, observes several Backbone events on the model to change state depending on the model's attributes.
I'm having trouble when it comes to unregistering Backbone events on the previous model and re-registering the same events on the new model. I don't always have reference to the original .on() registration, and I am tempted to simply call this.model.off() to unregister all the model's events (I don't want to destroy any other events the model may have, however). The Backbone.js documentation outlines that calling .off(null, null, context) will unregister the events from the object within the current context. I am uncertain, however, if this will unregister all events just for the current view instance.
Let's use this setup to register and unregister events:
var ThumbView=Backbone.View.extend({
initialize: function() {
this.model.on("change:title", this.log, this);
},
log:function(model) {
console.log("Thumb view : "+model.get("id")+" : "+model.get("title"));
}
});
var MainView=Backbone.View.extend({
initialize: function() {
this.model.on("change:title", this.log, this);
},
log:function(model) {
console.log("Main view : "+model.get("id")+" : "+model.get("title"));
}
});
var m1=new Backbone.Model({id:1,title:"m1"});
var t=new ThumbView({model:m1});
var v=new MainView({model:m1});
m1.set({title:"m1, 1"});
v=new MainView({model:m1});
m1.set({title:"m1, 2"});
As is, creating a new MainView won't destroy the previous bindings and will result in a zombie view. The last 3 lines give the following result:
Thumb view : 1 : m1, 1
Main view : 1 : m1, 1
Thumb view : 1 : m1, 2
Main view : 1 : m1, 2
Main view : 1 : m1, 2
The accompanying Fiddle : http://jsfiddle.net/9xufW/
Let's test the off method with a specific context:
var MainView=Backbone.View.extend({
initialize: function() {
this.model.on("change:title", this.log, this);
},
log:function(model) {
console.log("Main view : "+model.get("id")+" : "+model.get("title"));
},
teardown: function() {
this.model.off(null, null, this);
}
});
Calling
m1.set({title:"m1, 1"});
v.teardown(); // "destroys" the old view
v=new MainView({model:m1});
m1.set({title:"m1, 2"});
yields the expected result
Thumb view : 1 : m1, 1
Main view : 1 : m1, 1
Thumb view : 1 : m1, 2
Main view : 1 : m1, 2
http://jsfiddle.net/9xufW/1/
The callbacks set in the thumbviews are preserved while the callbacks set in the main view are removed. Note that the teardown method could and probably should be used to undelegate DOM events.
Another Fiddle where the model in the main view is replaced instead of destroying/recreating the view http://jsfiddle.net/9xufW/2/
Related
I have the following ItemView (there is no model associated with the view, it's a very basic "form" which has a submit or cancel and a single input field):
App.BasicForm = Backbone.Marionette.ItemView.extend({
template: "build/templates/basic-form.html",
tagName: "div",
attributes: {
id: "some-id",
style: "display: none;"
},
events: {
"click button#bf-submit": "bfSubmit",
"click button#bf-close": "bfClose"
},
bfSubmit: function() {
var bfInputField= document.getElementById('bfSomeData').value;
},
bfClose: function() {
this.$el.hide();
}
});
So by default, this view is hidden (but is instantiated when App starts).
I want to have a button which, when clicked, simply changes the attribute style display to block.
I can do this easily like this:
document.getElementById('bfBasicFormDiv').style.display = "block";
However, I'd rather call the view's $el.attr and edit it there, something along the lines of:
App.BasicForm.$el.attr({style: "display: block;"});
However, this returns an undefined, and I can see no way of retrieving the attribute of the View (it's easy with models using .get()) but that doesn't hold for a view.
Thank you for any advice.
Gary
App.BasicForm is not an instance, so it doesn't hold an element. You need to initialize it and you will be able to reference the element with $el:
var basicForm = new App.BasicForm({
el: document.getElementById('bfBasicFormDiv')
});
basicForm.$el.css({display: "block"});
When Backbone Routers is used for rendering subviews into a main view there is an issue I cannot overcome. Same issue when inner view rendering from inside the home view preferred. This is faced when the page is refreshed. I can describe them in detail as follows.
I have a homeview, whole page. A mainrouter directed me to that
homeview at the beginning. Homeview has tabs. Clicking tabs should
show a subview by a method in homeview navigating by mainrouter.
Subviews are whole width under tab bar. After navigated, url has
been updated. Now subview is shown in the place I wrote. If at this
stage one refreshes the page,this same stage is not reached. Because
of the url, directly the router will route to the method to render
the subview but where to put it in dom. Homeview is not there with
its element.
This also should be solved in case when not routers but inner views
are used to render the subviews from click events inside the
homeview, I mean without routes in the main router to create and
call render of the subviews in the main router. Because tab clicks
updates the url. And When one refreshes the page at this point app
knows no where to go. In my case it sometimes refreshes with no
problem and in some cases do not render anything.
Not updating the url in tab clicks can be a solution, but of course click should visit the hashtag it references. Updating the url is not necessary at all since this is even in desktop browser a single page application. So no place to be bookmark-able, back button-able, as Backbone documentation describes for Backbone.Router.
What are the ways to overcome this issue?
Update
http://www.geekdave.com/2012/04/05/module-specific-subroutes-in-backbone/
var MyApp = {};
MyApp.Router = Backbone.Router.extend({
routes: {
// general routes for cross-app functionality
"" : "showGeneralHomepage",
"cart" : "showShoppingCart",
"account" : "showMyAccount",
// module-specific subroutes:
// invoke the proper module and delegate to the module's
// own SubRoute for handling the rest of the URL
"books/*subroute" : "invokeBooksModule",
"movies/*subroute" : "invokeMoviesModule",
"games/*subroute" : "invokeGamesModule",
"music/*subroute" : "invokeMusicModule"
},
invokeBooksModule: function(subroute) {
if (!MyApp.Routers.Books) {
MyApp.Routers.Books = new MyApp.Books.Router("books/");
}
},
invokeMoviesModule: function(subroute) {
if (!MyApp.Routers.Movies) {
MyApp.Routers.Movies = new MyApp.Movies.Router("movies/");
}
},
invokeGamesModule: function(subroute) {
if (!MyApp.Routers.Games) {
MyApp.Routers.Games = new MyApp.Games.Router("games/");
}
}
});
// Actually initialize
new MyApp.Router();
});
MyApp.Books.Router = Backbone.SubRoute.extend({
routes: {
/* matches http://yourserver.org/books */
"" : "showBookstoreHomepage",
/* matches http://yourserver.org/books/search */
"search" : "searchBooks",
/* matches http://yourserver.org/books/view/:bookId */
"view/:bookId" : "viewBookDetail",
},
showBookstoreHomepage: function() {
// ...module-specific code
},
searchBooks: function() {
// ...module-specific code
},
viewBookDetail: function() {
// ...module-specific code
},
});
[OLD]
There are various ways you can do this, the way I prefer is :
var Router = Backbone.Router.extend({
initialize : function(){
app.homeView = new HomeView({el:"body"}); //I prefer calling it ShellView
},
routes : {
"subView/*":"renderS",
},
renderSubViewOne : function(params){
app.homeView.renderSubView('one',params);
}
});
var HomeView = Backbone.View.extend({
renderSubView:function(viewName, params){
switch(viewName){
case 'one':
var subViewOne = new SubViewOne({el:"tab-one"},params); //_.extend will be cleaner
break;
}
}
});
Above code is just an skeleton to give an idea. If the app is complex, I suggest having multiple routers.
Multiple routers vs single router in BackboneJs
I'm trying to render a different handlebars template based on the current value of a property in my model, and there could be quite a few options (hence I'd rather not use a lot of {{#if}}s). The best thing I can think of is this:
Ember.Handlebars.registerBoundHelper('selectorType', function(name, options) {
return Ember.Handlebars.compile("{{template _selectors_" + name + "}}")(options.contexts[0], options);
});
And I use that in my template like:
{{selectorType selector.name}}
(instead of like a hundred {{#if}}s)
The problem is that I get this error during render: "You can't use appendChild outside of the rendering process"
Clearly I'm doing something wrong. What's the right way to do this?
I don't think there's any need to create a helper to do this. You can do it from within the view by modifying the templateName and then calling the rerender method once you've changed its templateName:
init: function() {
this.set('templateName', 'firstOne');
this._super();
},
click: function() {
this.set('templateName', 'secondOne');
this.rerender();
}
We can use the init method for setting the empty templateName before the template has been rendered. We'll then call the _super method to complete the insertion of the view into the DOM. We can then trigger the change of the view on the click event. We update the templateName variable and then call rerender() to re-render this particular view.
I've set you up a JSFiddle as an example: http://jsfiddle.net/pFkaE/ try clicking on "First One." to change the view to the secondOne.
I ended up solving this using a ContainerView with dynamic childViews, see Ember.js dynamic child views for a discussion on how.
The relevant code is (coffeescript):
App.SelectorType = Ember.Object.extend
name: null
type: null
typeView: null
options: null
App.SelectorTypes = [
App.SelectorType.create(
name: 'foo'
type: 'bar'
) #, more etc
]
App.SelectorTypes.forEach (t) ->
t.set 'typeView', Ember.View.create
templateName: "selectors/_#{t.get('viewType')}_view"
name: t.get('name')
App.SelectorDetailView = Ember.ContainerView.extend
didInsertElement: ->
#updateForm()
updateForm: (->
type = #get('type')
typeObject = App.SelectorTypes.findProperty('type', type)
return if Ember.isNone(type)
view = typeObject.get('typeView')
#get('childViews').forEach (v) -> v.remove()
#get('childViews').clear()
#get('childViews').pushObject(view)
).observes('type')
And the template:
Selector Type:
{{view Ember.Select
viewName=select
contentBinding="App.SelectorTypes"
optionValuePath="content.type"
optionLabelPath="content.name"
prompt="Pick a Selector"
valueBinding="selector.type"
}}
<dl>
<dt><label>Details</label></dt>
<dd>
{{view App.SelectorDetailView typeBinding="selector.type"}}
</dd>
</dl>
Seems too hard, though, would be interested to see better solutions!
I have a backbone.js View that reads a template from the HTML file and inserts values from its model into the template. One of this value is in the variable title, which can be long enough to disrupt the flow of elements on the page. I want to use Javascript to limit the max. number of characters title can have, instead of doing it on the backend because eventually the full title has to be displayed.
I tried selecting the div that contains title after the template was been rendered, but cannot seem to select it. How can I do this otherwise?
Template
<script type="text/template" id="tpl_PhotoListItemView">
<div class="photo_stats_title"><%= title %></div>
</script>
View
PhotoListItemView = Backbone.View.extend({
tagNAme: 'div',
className: 'photo_box',
template: _.template( $('#tpl_PhotoListItemView').html() ),
render: function() {
$(this.el).html( this.template( this.model.toJSON() ) );
console.log($(this.el).children('.photo_stats_title')); <!-- returns nothing -->
this.limitChars();
return this;
},
limitChars: function() {
var shortTitle = $(this.el).children('.photo_stats_title').html().substring(0, 10);
$(this.el .photo_stats_title).html(shortTitle);
}
});
Rather than try to modify the title after rendering it, modify it as it's rendered.
Pass a maxlength variable to your template as well, then:
<script type="text/template" id="tpl_PhotoListItemView">
<div class="photo_stats_title"><%= title.substr(0,maxLength) %></div>
</script>
If title.length is less than maxlength, the entire string will display. If it's greater, only the first maxlength characters will display.
Alternatively, simply hardcode the maximum length of the title into the call to .substr()
If you need to perform more advanced truncating (e.g. adding '...' to truncated titles), you're better off modifying the title before rendering the template, passing the truncated version of the title into the template
Another option would be to override Model.parse(response), creating a shortTitle attribute on the model; this way it's always available whenever you're working with the model
Two things, the first one, to get any View's children I recommend you this way instead of what you are doing:
console.log( this.$('.photo_stats_title') );
"this.$" is a jQuery selector with the specific scope of your view.
The second thing is to wrap your model to handle this, I do not suggest to validate this in your Template or your View. In your Model define a new attribute for the shortTitle:
var titleMaxLength = 20;
var YourModel : Backbone.Model.extend({
defaults : {
id : null,
shortTitle : null,
title : null
},
initialize : function(){
_.bindAll(this);
this.on('change:name', this.changeHandler);
this.changeHandler();
},
changeHandler : function(){
var shortTitle = null;
if( this.title ){
shortTitle = this.title.substr(0, titleMaxLength);
}
this.set({ shortTitle : shortTitle }, {silent: true});
}
});
I have a view that has a tooltip attribute. I want to set that attribute dynamically on initialize or render. However, when I set it, it appears on the next instantiation of that view instead of the current one:
var WorkoutSectionSlide = Parse.View.extend( {
tag : 'div',
className : 'sectionPreview',
attributes : {},
template : _.template(workoutSectionPreviewElement),
initialize : function() {
// this.setDetailsTooltip(); // doesn't work if run here either
},
setDetailsTooltip : function() {
// build details
...
// set tooltip
this.attributes['tooltip'] = details.join(', ');
},
render: function() {
this.setDetailsTooltip(); // applies to next WorkoutViewSlide
// build firstExercises images
var firstExercisesHTML = '';
for(key in this.model.workoutExerciseList.models) {
// stop after 3
if(key == 3)
break;
else
firstExercisesHTML += '<img src="' +
(this.model.workoutExerciseList.models[key].get("finalThumbnail") ?
this.model.workoutExerciseList.models[key].get("finalThumbnail").url : Exercise.SRC_NOIMAGE) + '" />';
}
// render the section slide
$(this.el).html(this.template({
workoutSection : this.model,
firstExercisesHTML : firstExercisesHTML,
WorkoutSection : WorkoutSection,
Exercise : Exercise
}));
return this;
}
});
Here is how I initialize the view:
// section preview
$('#sectionPreviews').append(
(new WorkoutSectionPreview({
model: that.workoutSections[that._renderWorkoutSectionIndex]
})).render().el
);
How can I dynamically set my attribute (tooltip) on the current view, and why is it affecting the next view?
Thanks
You can define attribute property as a function that returns object as result. So you're able to set your attributes dynamically.
var MyView = Backbone.View.extend({
model: MyModel,
tagName: 'article',
className: 'someClass',
attributes: function(){
return {
id: 'model-'+this.model.id,
someAttr: Math.random()
}
}
})
I hope it hepls.
I think your problem is right here:
var WorkoutSectionSlide = Parse.View.extend( {
tag : 'div',
className : 'sectionPreview',
attributes : {} // <----------------- This doesn't do what you think it does
Everything that you put in the .extend({...}) ends up in WorkoutSectionSlide.prototype, they aren't copied to the instances, they're shared by all instances through the prototype. The result in your case is that you have one attributes object that is shared by all WorkoutSectionSlides.
Furthermore, the view's attributes are only used while the the object is being constructed:
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
The _ensureElement call is the thing that uses attributes and you'll notice that it comes before initialize is called. That order combined with the prototype behavior is why your attribute shows up on the next instance of the view. The attributes is really meant for static properties, your this.$el.attr('tooltip', ...) solution is a good way to handle a dynamic attribute.