Is there a way to separate the two cases for rendering the emptyView? - marionette

Is there a way to separate the two cases for rendering the emptyView?
1. When the CollectionView is just created. The collection is still empty
2. After collection is fetched but the data is empty.
_CollectionView = Backbone.Marionette.CollectionView.extend({
emptyView: _EmptyView,
itemView: _ItemView,
initialize: function () {
this.collection = new Backbone.Collection ();
this.collection.fetch();
},//initialize
});

this is the way I have done this in the past. Set your 'emptyView' to be your loading view and then after the collection has synced, set the 'emptyView' to your actual EmptyView if required. I have also used this in 'onBeforeRender' as in the example below you may need to re-render your view if it has already been rendered with the 'EmptyView':
emptyView: LoadingView,
collectionEvents: {
'sync': 'onSync'
},
onSync: function () {
if(this.collection.length === 0) {
this.emptyView = EmptyView;
//may need to call 'this.render();' here if already rendered
}
}

Related

Update state of like button in Backbone View

In the following code, I have a view which extends from another view (but does not inherit any functionality, only renders the template) and a model which I want to implement now. My view is for a like button, which I need to retrieve the state of the like button from the server each time the page is loaded. I am not sure how to do this using the model. Do I need to have an Ajax call in the model retrieving the state from the server or does that call fall into the view?
This is my code:
var likeButton = Backbone.Model.extend ({
initialize: function () {
this.isLiked = /* need something here! Ajax call to get state of button from server? */
}
});
var LikeButtonView = BaseButtonView.extend({ // extends form a previews view which simply extends from backbone and render's the template
template: _.template($('#like-button').html()),
sPaper: null,
sPolyFill: null,
sPolyEmpty: null,
isLiked: false,
events: {
"click .icon": "like",
},
model: new likeButton (),
initialize: function (options) {
BaseButtonView.prototype.initialize.apply(this, [options]); // inherit from BaseButtonView
this.likeButn = $("button.icon", this.$el);
this.svgNode = this.likeButn.find("svg").get(0); // find the svg in the likeButn and get its first object
this.sPaper = Snap(this.svgNode); // pass in the svg object into Snap.js
this.sPolyFill = this.sPaper.select('.symbol-solid');
this.sPolyEmpty = this.sPaper.select('.symbol-empty');
if (this.model.isLiked) {
this.likeButn.addClass("liked");
} else if (!this.model.isLiked) {
this.likeButn.addClass("unliked");
}
},
like: function() {
this._update();
},
_update: function () {
if ( !this.isLiked ) { // if isLiked is false, remove class, add class and set isLiked to true, then animate svg to liked position
this._like();
} else if ( this.isLiked ) { // is isLiked is false, remove class, add class, set isLiked to false, then animate svg to unliked position
this._unlike();
}
},
_like: function() {
this.likeButn.removeClass("unliked");
this.likeButn.addClass("liked");
this.isLiked = true;
this.sPolyFill.animate({ transform: 't9,0' }, 300, mina.easeinout);
this.sPolyEmpty.animate({ transform: 't-9,0' }, 300, mina.easeinout);
},
_unlike: function() {
this.likeButn.removeClass("liked");
this.likeButn.addClass("unliked");
this.isLiked = false;
this.sPolyFill.animate({ transform: 't0,0'}, 300, mina.easeinout);
this.sPolyEmpty.animate({ transform: 't0,0' }, 300, mina.easeinout);
}
});
There are three ways to implement the 'like' button's knowledge of the current state of the page: A hidden field delivered from the HTML, an Ajax call to the server, or generating your javascript server-side with the state of the like model already active.
Let's start with the basics. Your code is a bit of a mess. A model contains the state of your application, and a view is nothing more than a way of showing that state, receiving a message when the state changes to update the show, and sending messages to the model to change the state. The model and the view communicate via Backbone.Events, and the view and the DOM communicate via jQuery.Events. You have to learn to keep those two separate in your mind.
Here, I've turned your "like" model into an actual model, so that the Backbone.Event hub can see the changes you make.
var likeButton = Backbone.Model.extend ({
defaults: {
'liked': false
}
});
Now in your view, the initial render will draw the state in gets from the model. When a DOM event (described in the 'events' object) happens, your job is to translate that into a state change on the model, so my "toggleLike" only changes the model, not the view. However, when the model changes (explicitly, when the "liked" field of the model changes), the view will then update itself automatically.
That's what makes Backbone so cool. It's the way views automatically reflect the reality of your models. You only have to get the model right, and the view works. You coordinate the way the view reflects the model in your initialization code, where it's small and easy to reason about what events from the model you care about.
var LikeButtonView = BaseButtonView.extend({
template: _.template($('#like-button').html()),
events: {
"click .icon": "toggleLike",
},
initialize: function (options) {
BaseButtonView.prototype.initialize.call(this, options); // inherit from BaseButtonView
// A shortcut that does the same thing.
this.likeButn = this.$("button.icon");
this.model.on('change:liked', this._updateView, this);
},
render: function() {
BaseButtonView.prototype.render.call(this);
// Don't mess with the HTML until after it's rendered.
this.likeButn.addClass(this.model.isLiked ? "liked", "unliked");
},
toggleLike: function() {
this.model.set('liked', !this.model.get('liked'));
},
_updateView: function () {
if (this.model.get('liked')) {
this._showLikedState();
} else {
this._showUnlikedState();
}
}
});
How the like model gets initialized is, as I said above, up to you. You can set a URL on the model's options and in your page's startup code tell it to "fetch", in which case it'll get the state from some REST endpoint on your server. Or you can set it to a default of 'false'. Or you can set it in hidden HTML (a hidden div or something) and then use your page startup code to find it:
new LikeButtonView({model: new LikeButton({}, {url: "/where/page/state/is"}));
or
new LikeButtonView({model: new LikeButton({liked: $('#hiddendiv').data('liked')}, {}));
If you're going to save the liked state, I'd recommend the URL. Then you have someplace to save your data.

SlickGrid CSS styles wrong on filtered view

I have a SlickGrid with dataview working pretty good, grid and dataview are synced up for modify and delete selections using syncGridSelection, however an interesting problem occurs on the changed CSS styles. The changed rows CSS stlye are being applied to the same "visible" row number in the grid when I choose a filter set that does not include the actual changed rows. The sort works fine, but I noticed that the filter is not working. Does anyone have a fix for this? Can you paste as much info and code for me as possible because I'm new to SlickGrid. I pasted code that loads up the grid.
function LoadGridData() {
$.getJSON('#Url.Action("GetConfigurations")', function (rows) {
if (rows.length > 0) {
if (rows[0].id = 'undefined') {
$(rows).each(function (index) {
rows[index].newAttribute = "id"
rows[index]["id"] = index;
});
}
};
data = rows;
dataView.beginUpdate();
dataView.setItems(data);
dataView.setFilter(filter);
dataView.endUpdate();
// Refresh the data render
grid.invalidate();
grid.render();
grid.setSelectedRows([]);
dataView.syncGridSelection(grid, true);
});
}
After debugging I found that I had used an older example of marking css changed in function getItemMetadata. The correct code is below. Previously I was referencing data[row]. When Syncing DataView to Grid, the getItem() method returns the correct row. In this case my DataState is my own changed indicator on the view model.
dataView.getItemMetadata = function (row) {
var item = this.getItem(row);
if (item && item.DataState == 2) {
return {
"cssClasses":
"changed"
};
}

backbone click event fires as many times as there is views

I searched info on this topic but found only info about getting event element.
Yes, I can get an element of clicked div, but why it's fired all 19 times? (it's the number of views total). Info of clicked event is same - of the clicked div.
Here is what divs look like: http://d.pr/i/AbJP
Here is console.log: http://d.pr/i/zncs
Here is the code of index.js
$(function () {
var Table = new Backbone.Collection;
var TrModel = Backbone.Model.extend({
defaults: {
id: '0',
name: 'defaultName'
},
initialize: function () {
this.view = new Tr({model: this, collection: Table});
this.on('destroy', function () {
this.view.remove();
})
}
});
var Tr = Backbone.View.extend({
el: $('.pop-tags').find('.container'),
template: _.template($('#td_template').html()),
events: {
'click .tag': 'clicked'
},
clicked: function (event) {
event.preventDefault();
event.stopPropagation();
console.log(event.currentTarget);
},
initialize: function () {
this.render();
},
render: function () {
this.$el.append(this.template(this.model.attributes));
return this;
}
});
for (var i = 0, size = 19; i < size; i++) {
var trModel = new TrModel({id: i, name: 'name_' + i});
Table.add(trModel);
}
});
How can I avoid all elements from firing an event and fire only one that clicked and only 1 time?
el: $('.pop-tags').find('.container'),
Don't do that. You are attaching every view instance to the same DOM node. Each backbone view needs a distinct DOM node or, as you see, delegate events become complete chaos. In your view, set tagName: 'tr', then when creating your views, create them, call .render() and then append them to the DOM with something like $('.table-where-views-go').append(trView.el);.
You also may want to brush up on the basic MVC concept because Tables and Rows are view-related notions, not model-related, so a class called TrModel is a code smell that you aren't clear on Model vs View.
I would use a slightly different approach to solve your problem.
Instead for one view for every tr I would create one view for the entire table.
When I create the view I would pass the collection containing the 19 models to the view and in view.initialize use the collection to render the rows.
I created a jsbin with a working example.

Backbone.js - event trigger not work after rendering other views

There's a addPost function in my router. I don't want to re-create the postAddView every time the function is invoked:
addPost: function () {
var that = this;
if (!this.postAddView) {
this.postAddView = new PostAddView({
model: new Post()
});
this.postAddView.on('back', function () {
that.navigate('#/post/list', { trigger: true });
});
}
this.elms['page-content'].html(this.postAddView.render().el);
}
Here's the PostAddView:
PostAddView = backbone.View.extend({
events: {
'click #post-add-back': 'back'
}
, back: function (e) {
e.preventDefault();
this.trigger('back');
}
});
The first time the postAddView is rendered, the event trigger works well. However, after rendering other views to page-content and render postAddView back, the event trigger won't be trigger anymore. The following version of addPost works well, though.
addPost: function () {
var that = this, view;
view = new PostAddView({
model: new Post()
});
this.elms['page-content'].html(view.render().el);
view.on('back', function () {
delete view;
that.navigate('#/post/list', { trigger: true });
});
}
Somewhere you are calling jQuery's remove and that
In addition to the elements themselves, all bound events and jQuery data associated with the elements are removed.
so the delegate call that Backbone uses to bind events to your postAddView.el will be lost. Then, when you re-add your postAddView.el, there are is no delegate attached anymore and no events are triggered. Note that Backbone.View's standard remove method calls jQuery's remove; a few other things in jQuery, just as empty will do similar things to event handlers. So the actual function call that is killing your delegate could be hidden deep inside something else.
You could try calling delegateEvents manually:
this.elms['page-content'].html(this.postAddView.render().el);
this.postAddView.delegateEvents();
or better, just throw the view away and create a new one every time you need it. Your view objects should be pretty light weight so creating new ones should be cheap and a lot less hassle than trying to keep track of the existing views by hand.
If you really want to reuse the current DOM and View you do not need to set again and again the element as you are doing, everything that you call .html() you are destroying the DOM of the View and generating again and losing events. Also I prefer always to add the "el" in the DOM before render the View. I will have your function in this way:
addPost: function () {
if (!this.postAddView) {
this.postAddView = new PostAddView({
model: new Post()
});
this.postAddView.on('back', this.onBack);
this.elms['page-content'].html(this.postAddView.el);
}
this.postAddView.render();
},
onBack : function () {
this.navigate('#/post/list', { trigger: true });
}
I'm not fan of the use of local variables to refer to "this". If all of your Views uses _.bindAll(this) in the initialize method you could bind your events to your view and could use this(check how I transformed onBack).
With my code there is not a need to manually call this.delegateEvents()

Backbone click event fires events for all collection rather than model

Can't figure out what's wrong. When I click on a model title, it fetches all models in collection at once rather than fetch one model. If I move this event from logView to logsView it works properly but doesn't have access to model, well I can find this model using index or ant other model's ID but don't think this is a nice way.
var Log = Backbone.Model.extend({});
window.LogsList = Backbone.Collection.extend({
model:Log,
url:function (tag) {
this.url = '/logs/' + tag;
return this;
}
});
window.colList = new LogsList();
window.logView = Backbone.View.extend({
el:$('.accordion'),
template:_.template($('#log').html()),
initialize:function () {
this.model.bind('add', this.render, this);
},
events:{
"click .accordion-toggle" :"getLogBody"
},
render:function () {
return this.template(this.model.toJSON());
},
getLogBody:function () {
this.model.fetch();
}
});
window.LogsView = Backbone.View.extend({
el:$("#content"),
initialize:function (options) {
colList.bind('reset', this.addAll, this);
colList.url(options.data).fetch();
},
addOne:function (model) {
var view = new logView({model:model});
$("#accordion").append(view.render());
},
addAll:function () {
colList.each(this.addOne);
}
});
window.listView = new LogsView({data:"Visa_Cl"});
The problem is caused by your el in the LogView: el:$('.accordion')
Backbone's view events are scope to the view's el. In this case, you've specified the view's el as ALL HTML elements with a class of "accordion". Therefore, when you click on any of your HTML elements with this class, the code runs for all of them, which is why you are seeing this behavior.
This article will show you a few options for doing what you want, correctly:
Backbone.js: Getting The Model For A Clicked Element
I would also recommend reading this one, to better understand the use of el in Backbone, and a few of the tricks and traps of it:
Backbone.js: Object Literals, Views Events, jQuery, and el

Resources