Animating between views - backbone.js

On each of my views I have this on each of my render methods:
render: function(){
template = _.template(ViewTemplate, {foo:get});
wrapper = this.$el;
wrapper.is(':hidden') ?
wrapper.html(template).show(200) :
wrapper.hide(200, function(){ wrapper.html(template).show(200) });
}
But this is so repetitive, I was wondering how could I be implement animation between my views but not repeating the same lines of code?

Maybe just add the fade-in as a utility method to the View prototype:
Backbone.View.prototype.fadeIn = function(template, wrapper) {
wrapper.is(':hidden') ?
wrapper.html(template).show(200) :
wrapper.hide(200, function(){ wrapper.html(template).show(200) });
};
That reduces the repetition in the render implementations:
render: function() {
template = _.template(ViewTemplate, {foo:get});
this.fadeIn(template, this.$el);
}

Related

Issue displaying child view which contains search box - Backbone.js

Previous Title :- Search functionality in Backbone.js.
I am working on Backbone.js to achieve search functionality( when user types something in search textbox, the list should get filetered as per search criteria).
For this I have my controller as :-
var departments = backbone.Collection.extend({
model: departmentModel,
url: '/MyController/GetDepartments',
comparator: function (department) {
return department.get('name');
},
initialize: function () {
this.selected = [];
},
search: function (letters) {
if (letters == "") return this;
var pattern = new RegExp(letters, "gi");
return _(this.filter(function (data) { //without wrapping the filter with the underscore function, the filter does not return a collection
return pattern.test(data.get("Name"));
}));
}
});
return departments;
In my Backbone View I have my keyup event defined on search textbox
"keyup #searchDepartments": "searchDepartments",
Where my searchDepartments is -
searchDepartments: function (e) {
var letters = $(e.currentTarget).val();
var searchResult = this.collection.search(letters);
var collection = new departmentCollection(searchResult.toArray());
// debugger;
this.renderFileteredData(collection);
//$(e.currentTarget).val(letters);
}
Lastly, renderFilteredData is simple
renderFileteredData: function (departments) {
$(this.$el).html(this.template(departments.toJSON()));
return this;
},
Now the issue is- when ever I am typing any text the list gets filtered out but the search text goes off. Whats wrong?
EDIT:- As per suggestions, now I have created a different view as below for search text box:-
function ($, _, backbone) {
'use strict';
var searchDepartmentView = backbone.View.extend({
el: "#search-container",
tagName: 'div',
template: Handlebars.templates.DepartmentSearchView,
initialize: function () {
//this.render();
return this;
},
render: function () {
console.log('in search departmnet view render method');
// $(this.$el).html(this.template());// I am trying to render this template.. But its not working
this.$el.html('in search departmnet view render method');//Passing dummy value to it
return this;
}
});
return searchDepartmentView;
});
and in my main view :-
render: function () {
// _.bindAll(this, "search");
this.innerView = new departmentSearchView();
$(this.$el).html(this.template(this.collection.toJSON()));
this.$el.find("#search-container").html(this.innerView.render().$el); //Tried approach 1 as suggested
// $(this.$el).html(this.innerView.render().el);//Tried approach 2 as suggested
return this;
},
Now, I am not getting my search text in search departmnet view render method displayed in main view
Finally, I found a way to do it. Thank to the great answer by Kelvin Peel in how-to-handle-initializing-and-rendering-subviews-in-backbone-js
Thanks to YuruiRayZhang for his suggestions!
Code snippet from there, which describes a detailed process of nested views :-
var ParentView = Backbone.View.extend({
el: "#parent",
initialize: function() {
// Step 1, (init) I want to know anytime the name changes
this.model.bind("change:first_name", this.subRender, this);
this.model.bind("change:last_name", this.subRender, this);
// Step 2, render my own view
this.render();
// Step 3/4, create the children and assign elements
this.infoView = new InfoView({el: "#info", model: this.model});
this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
},
render: function() {
// Render my template
this.$el.html(this.template());
// Render the name
this.subRender();
},
subRender: function() {
// Set our name block and only our name block
$("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
}
});
Also, an extended tutorial for the above mentioned process backbone-js-subview-rendering-trick/

how to delegate a "load" DOM event?

I would like to call a function when a "load" event is triggered:
events: {
"load #eventPicture" : "_resizeHeaderPic"
}
I don't want to do something like this.$("#eventPicture").on("load", _resizeHeaderPic); because I have a lot of views (it's a Single Page App) and I could go back to show another view before the image was loaded. So, if I then come back to this view I would have two listener for that "load" event. Right? By putting everything in my events hash, I can undelegate properly.
But it seems that "load #eventPicture" does not work. Any suggestion?
You cannot track load event from Backbone events because this event fires only on image instance and doesn't bubble. So Backbone.View's $el cannot track it.
jQuery callback on image load (even when the image is cached)
UPDATE
I would suggest to use another concept (JSFiddle). This is best practice:
var LayoutView = Backbone.View.extend({
el : '[data-container]',
show : function (view) {
// remove current view
this.$view && this.$view.remove();
// save link to the new view
this.$view = view;
// render new view and append to our element
this.$el.html(this.$view.render().el);
}
});
var ImageView = Backbone.View.extend({
template : _.template('<img src="https://fbcdn-sphotos-g-a.akamaihd.net/hphotos-ak-prn2/1375054_4823566966612_1010607077_n.jpg"/>'),
render : function () {
this.$el.html(this.template());
this.$('img').on('load', _.bind(this.onLoad, this));
return this;
},
onLoad : function () {
console.log('onLoad');
}
});
var OtherView = Backbone.View.extend({
template : _.template('lalala'),
render : function () {
this.$el.html(this.template());
return this;
}
});
var Router = Backbone.Router.extend({
routes : {
'other' : 'other',
'*any' : 'image'
},
initialize : function (options) {
this.layout = new LayoutView();
},
other : function () {
this.layout.show(new OtherView());
},
image : function () {
this.layout.show(new ImageView());
}
});
new Router();
Backbone.history.start();

Rendering each collection item asynchronously in Backbone.js

I am trying to render a collection of items. Normally what I would do is something like this:
StuffView = Backbone.View.extend({
...
render: function(){
...
this.$el.html( ... );
return this;
}
...
});
StuffCollectionView = Backbone.View.extend({
...
render: function(){
this.collection.each(addOne, this);
},
addOne: function(stuff){
var view = new StuffView({model: stuff});
this.$el.append(view.render().el);
}
...
});
However, this time I'm building a bit different type of view. Each StuffView's rendering takes some time, so I can't do this synchronously. The code for the new StuffView looks something like this:
StuffView = Backbone.View.extend({
...
render: function(){
...
// Asynchronous rendering
SlowRenderingFunction(function(renderedResult){
this.$el.html(renderedResult);
});
}
});
In this case, I can't just return this from render and append its result to the StuffCollectionView's el. One hack I thought of was to pass a callback function to StuffView's render, and let it callback when it has finished rendering. Here's an example:
StuffView = Backbone.View.extend({
...
render: function(callback){
...
// Asynchronous rendering
SlowRenderingFunction(function(renderedResult){
this.$el.html(renderedResult);
callback(this);
});
}
});
StuffCollectionView = Backbone.View.extend({
...
initialize: function(){
_.bindAll(this, "onStuffFinishedRendering");
},
render: function(){
this.collection.each(addOne, this);
},
addOne: function(stuff){
var view = new StuffView({model: stuff});
view.render(onStuffFinishedRendering);
},
onStuffFinishedRendering: function(renderedResult){
this.$el.append(renderedResult.el);
}
...
});
But it's not working for some reason. Furthermore, this feels too hacky and doesn't feel right. Is there a conventional way to render children views asynchronously?
Can't you pass StuffCollectionView's el into the SlowRenderingFunction? It's a bit nasty but I don't see why it wouldn't work.
Edit: I should say, and make SlowRenderingFunction an actual property of StuffView, so that StuffViewCollection can call it instead of calling render.
You can try using _.defer to prevent the collection items rendering blocking the UI.
Refer http://underscorejs.org/#defer for more details.
StuffCollectionView = Backbone.View.extend({
...
render: function(){
var self = this;
_(function() {
self.collection.each(addOne, self);
}).defer();
}
...
});

Backbone.js event after view.render() is finished

Does anyone know which event is fired after a view is rendered in backbone.js?
I ran into this post which seems interesting
var myView = Backbone.View.extend({
initialize: function(options) {
_.bindAll(this, 'beforeRender', 'render', 'afterRender');
var _this = this;
this.render = _.wrap(this.render, function(render) {
_this.beforeRender();
render();
_this.afterRender();
return _this;
});
},
beforeRender: function() {
console.log('beforeRender');
},
render: function() {
return this;
},
afterRender: function() {
console.log('afterRender');
}
});
Or you can do the following, which is what Backbone code is supposed to look like (Observer pattern, aka pub/sub). This is the way to go:
var myView = Backbone.View.extend({
initialize: function() {
this.on('render', this.afterRender);
this.render();
},
render: function () {
this.trigger('render');
},
afterRender: function () {
}
});
Edit: this.on('render', 'afterRender'); will not work - because Backbone.Events.on accepts only functions. The .on('event', 'methodName'); magic is made possible by Backbone.View.delegateEvents and as such is only available with DOM events.
As far as I know - none is fired. Render function is empty in source code.
The default implementation of render is a no-op
I would recommend just triggering it manually when necessary.
If you happen to be using Marionette, Marionette adds show and render events on views. See this StackOverflow question for an example.
On a side note, Marionette adds a lot of other useful features that you might be interested in.
I realise this question is fairly old but I wanted a solution that allowed the same custom function to be called after every call to render, so came up with the following...
First, override the default Backbone render function:
var render = Backbone.View.prototype.render;
Backbone.View.prototype.render = function() {
this.customRender();
afterPageRender();
render();
};
The above code calls customRender on the view, then a generic custom function (afterPageRender), then the original Backbone render function.
Then in my views, I replaced all instances of render functions with customRender:
initialize: function() {
this.listenTo(this.model, 'sync', this.render);
this.model.fetch();
},
customRender: function() {
// ... do what you usually do in render()
}
Instead of adding the eventhandler manually to render on intialization you can also add the event to the 'events' section of your view. See http://backbonejs.org/#View-delegateEvents
e.g.
events: {
'render': 'afterRender'
}
afterRender: function(e){
alert("render complete")
},
constructor: function(){
Backbone.View.call(this, arguments);
var oldRender = this.render
this.render = function(){
oldRender.call(this)
// this.model.trigger('xxxxxxxxx')
}
}
like this http://jsfiddle.net/8hQyB/

backbone.js - accessing a model from a click event

I have a BoardView containing a CellCollection of CellModels. I fetch the collection from the db and then create the CellViews.
This all works swimmingly until I try to access a CellModel via a click event on the BoardView. I can't get to the underlying models at all... only the views. Is there a way to do this?
I've attempted to include the relevant code below:
CellModel = Backbone.Model.extend({});
CellCollection = Backbone.Collection.extend({
model : CellModel
});
CellView = Backbone.View.extend({
className : 'cell',
});
BoardView = Backbone.View.extend({
this.model.cells = new CellCollection();
render : function() {
this.cellList = this.$('.cells');
return this;
},
allCells : function(cells) {
this.cellList.html('');
this.model.cells.each(this.addCell);
return this;
},
addCell : function(cell) {
var view = new Views.CellView({
model : cell
}).render();
this.cellList.append(view.el);
},
events : {
'click .cell' : 'analyzeCellClick',
},
analyzeCellClick : function(e) {
// ?????????
}
});
I need the click to "happen" on the BoardView, not the CellView, because it involves board-specific logic.
Good question! I think the best solution would be to implement an
EventBus aka EventDispatcher
to coordinate all events among the different areas of your application.
Going that route seems clean, loosely coupled, easy to implement, extendable and it is actually suggested by the backbone documentation, see Backbone Docs
Please also read more on the topic here and here because (even though I tried hard) my own explanation seems kind of mediocre to me.
Five step explanation:
Create an EventBus in your main or somewhere else as a util and include/require it
var dispatcher = _.clone(Backbone.Events); // or _.extends
Add one or more callback hanlder(s) to it
dispatcher.CELL_CLICK = 'cellClicked'
Add a trigger to the Eventlistener of your childView (here: the CellView)
dispatcher.trigger(dispatcher.CELL_CLICK , this.model);
Add a Listener to the Initialize function of your parentView (here: the BoardView)
eventBus.on(eventBus.CARD_CLICK, this.cardClick);
Define the corresponding Callback within of your parentView (and add it to your _.bindAll)
cellClicked: function(model) {
// do what you want with your data here
console.log(model.get('someFnOrAttribute')
}
I can think of at least two approaches you might use here:
Pass the BoardView to the CellView at initialization, and then handle the event in the CellView:
var CellView = Backbone.View.extend({
className : 'cell',
initialize: function(opts) {
this.parent = opts.parent
},
events : {
'click' : 'analyzeCellClick',
},
analyzeCellClick : function() {
// pass the relevant CellModel to the BoardView
this.parent.analyzeCellClick(this.model);
}
});
var BoardView = Backbone.View.extend({
// ...
addCell : function(cell) {
var view = new Views.CellView({
model : cell,
parent : this
}).render();
this.cellList.append(view.el);
},
analyzeCellClick : function(cell) {
// do something with cell
}
});
This would work, but I prefer to not have views call each other's methods, as it makes them more tightly coupled.
Attach the CellModel id to the DOM when you render it:
var CellView = Backbone.View.extend({
className : 'cell',
render: function() {
$(this.el).data('cellId', this.model.id)
// I assume you're doing other render stuff here as well
}
});
var BoardView = Backbone.View.extend({
// ...
analyzeCellClick : function(evt) {
var cellId = $(evt.target).data('cellId'),
cell = this.model.cells.get(cellId);
// do something with cell
}
});
This is probably a little cleaner, in that it avoids the tight coupling mentioned above, but I think either way would work.
I would let the CellView handle the click event, but it will just trigger a Backbone event:
var CellView = Backbone.View.extend({
className : 'cell',
initialize: function() {
_.bindAll(this, 'analyzeCellClick');
}
events : {
'click' : 'analyzeCellClick',
},
analyzeCellClick : function() {
this.trigger('cellClicked', this.model);
}
});
var BoardView = Backbone.View.extend({
// ...
addCell : function(cell) {
var view = new Views.CellView({
model : cell
}).render();
this.cellList.append(view.el);
view.bind('cellClicked', function(cell) {
this.analyzeCellClick(cell);
};
},
analyzeCellClick : function(cell) {
// do something with cell
}
});

Resources