Update state of like button in Backbone View - ajax

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.

Related

update reactjs context after ajax request finished with flux architecture

I need to update the context after an ajax request has finished. I'm using the flux architecture and everything works to the point that when my component is notified about the updated I need to set the new context.
A simple demostration:
I have a parent component which generates the context by calling a store. The store gets the data after an ajax request is initialized somewhere else. Like this:
RowAPI.ajaxGetAllRows();
Then I have my component which holds the context:
let ParentComponent = React.createClass({
childContextTypes: {
rows: React.PropTypes.object
},
getChildContext: function() {
return {
rows: RowStore.getAllRows(),
};
},
componentDidMount: function() {
RowStore.addChangeListener(this._onRowsChanged);
},
componentWillUnmount: function() {
RowStore.removeChangeListener(this._onRowsChanged);
},
render() {
return (
<ChildComponent />
);
},
_onRowsChanged: function() {
//Now we need to update context
}
});
Now since we are listening for row changes, we will get an update when our ajax request has finished and put the data into our store. Now we need to get that data and set it as context. That is the problem.
This is my child component that uses the context. I know that I just can pass the rows as a props to my child but this is just an example and in my real scenario I have many children which would need to pass the props.
let ChildComponent = React.createClass({
contextTypes: {
rows: React.PropTypes.object
},
render() {
return (
<div style={styles.wrapper}>
{this.context.rows}
</div>
);
},
});
Thanks in advance!
I would change the getChildContext in ParentComponent to refer to the state instead of a function call to the RowStore.
getChildContext: function() {
return {
rows: this.state.rows,
};
}
Then, whenever a row changes, and the _onRowsChanged callback it called, it can set this.state.rows accordingly.
I believe that the issue with the original method of calling RowStore.getAllRows() inside getChildContext is that it is only called once. Nothing is forcing it to call RowStore.getAllRows() on every change.
However, by using a state, you can use Flux concepts to "force" a change in state on every update, and that will be reflected in the context.

How to use SuperScrolloRama with Backbone.js Views

I am trying to understand how to use a plugin like http://johnpolacek.github.io/superscrollorama/, with Backbone.js by integrating it into my Views. I know that I need to hook into the backbone View-Events, but I want to do a horizontal scroll with the plugin, and I don't know of a horizontal scroll-event. How can I still utilize the plugin? Thanks in advance for any ideas.
Views:
var ArtistsView = Backbone.View.extend({
tagName: 'ul',
initialize: function () {
this.cleanUp();
$("body").attr('id','artists');
this.render();
},
events: {
"click div.open" : "largeArtViewOpen",
"click div.close" : "largeArtViewClose",
},
render: function () {
this.collection.each(function(model) {
var artistView = new ArtistView({ model: model });
this.$el.append(artistView.render().el);
}, this);
console.log('and a new view was rendered!')
return this;
},
cleanUp: function(){
if (this != null) {
this.remove();
this.unbind();
console.log('View was removed!');
}
},
largeArtViewOpen: function(e){
var thisArt = $(e.currentTarget).parent().attr("class");
console.log(thisArt);
$("#open-view, li."+thisArt).show();
},
largeArtViewClose: function(e){
//var thisArt = $(e.currentTarget).parent().attr("class");
console.log('clicked!');
$("#open-view, ul#large li").hide();
},
scrollFx: function(){
var controller = $.superscrollorama({
isVertical:false
});
controller.addTween('h2#fade-it', TweenMax.from( $('h2#fade-it'), .5, {css:{opacity: 0}}), 800);
//$('h2#fade-it').css({'color':'#dbdbdb'});
console.log('scroll message!');
},
});
var ArtistView = Backbone.View.extend({
tagName:'li',
className:'artistLink not-active',
render: function(){
this.id = this.model.get('idWord')+"-menu-item";
this.$el.attr('id', this.id).html(this.template(this.model.toJSON()));
return this;
},
});
So, in the past 3 days since I've asked this question, I've spent some time trying different scrollable 'targets' for Superscrollorama...Document vs. Window vs. Body vs. other DOM elements within the HTML, and the questions that I've had to consider are, should the scroll event be bound to the View's top element? Should it be bound to the body, but initialized in the view? In both cases I tried, I couldn't get the scroll events to continuously fire...this may just be due to bad code, but I couldn't make it happen.
So, what I arrived at, was, avoiding the view entirely: I instantiating and called Superscrollorama in a function called scrollFx() within a separate 'helper.js' document, and then called scrollFx() from my view's router.
I'm thinking I will just empty the target's styles and unbind any existing scroll events in the beginning of scrollFx(), before I call the Superscrollorama function so that the resulting scroll styles/animations are cleaned up, and events aren't exponentially bound.
I'm still very much working through these issues, though now the scroll events are working, so if anyone happens to read through this train of thought, please feel free to add your two sense, especially, if you have better ideas about re-implementing the Superscrollorama function within the View itself.
Thanks.

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.

Using durandal with jaydata and kendoui

I created a new VS2012 project using the hottowel template, which in return uses durandal, knockout and breeze.
I want to use jaydata instead of breeze and for the ui layer I want to use the excellent asKendoDataSource() functionality to power a kendoui grid.
I followed all the instructions to make kendoui work well with durandal. This is fine.
I have a model in which I get the jaydata entities and run asKendoDataSource() on it. My view is an MVVM declared kendoui grid with its source property set to the viewmodel's property that contains a reference to the asKendoDataSource().
In the knockout world the viewModel property would be an empty entities = ko.observableArray() which would then would be initialized by using entities(values) when the data service returns.
I need to mimic this such that I have a viewModel property that is an empty kendo datasource which then is initialized by the asKendoDataSource() conversion when the data comes back from jaydata. This way the mvvm kendo grid is bound initially to the empty datasource and then receives its items when the asKendoDataSource() is called.
This is all because the model - viewModel binding in durandal is asynchronous and there needs to be a placeholder property in the viewModel from the very beginning, which then, after the viewModel's activate() method promise is resolved, gets updated with the bound data and in return powers the DOM that the viewModel is bound to.
I can't figure out how to mimic the knockoutjs practice of an empty observable array which is bound to the dom and then (the same exact reference) gets initialized and populates the dom. There seems to be no way to create an empty kendo datasource which then is re-initialized by the asKendoDataSource() method. Reassigning the viewModel property to the new data source doesn't work because the kendo grid is bound to the original reference.
This is my airport view:
<section>
<h2 class="page-title" data-bind="text: title"></h2>
<div id="airportGrid" data-kendo-role="grid" data-kendo-sortable="true" data-kendo-pageable="true" data-kendo-page-size="25" data-kendo-editable="true" data-kendo-columns='["Id", "Abbrev", "Name"]' data-kendo-bind="source: airports"></div>
</section>
This is my datacontext:
define([
'durandal/system',
'services/logger'],
function (system, logger) {
var getAirports = function (airportsObservable) {
$data.Entity.extend("Airport", {
Id: { type: "int", key: true, computed: true },
Abbrev: { type: String, required: true, maxLength: 200 },
Name: { type: String, required: true, maxLength: 512 }
});
$data.EntityContext.extend("JumpSeatDatabase", {
Airports: { type: $data.EntitySet, elementType: Airport }
});
var airportDB = new AirportDatabase('http://localhost:2663/odata');
var deferred = Q.defer();
airportDB.onReady(function () {
deferred.resolve(airportDB.Airports);
});
return deferred.promise;
}
var datacontext = {
getAirports: getAirports
};
return datacontext;
function log(msg, data, showToast) {
logger.log(msg, data, system.getModuleId(datacontext), showToast);
}
function logError(msg, error) {
logger.logError(msg, error, system.getModuleId(datacontext), true);
}
//#endregion
});
This is my airport viewmodel:
define(['services/datacontext', 'durandal/plugins/router'],
function (datacontext, router) {
var airports = null;// = kendo.data.ObservableArray([]);
var activate = function () {
var airportRes = datacontext.getAirports();
return airportRes.then(function (airp) {
vm.airports = airp.asKendoDataSource();
});
};
var deactivate = function() {
//airports([]);
};
var viewAttached = function (view) {
//using this type of reach in to the viewModel is considered bad practice in durandal
//$('#airportGrid').kendoGrid({
// dataSource: airports.asKendoDataSource({ pageSize: 10 }),
// filterable: true,
// sortable: true,
// pageable: true,
// height: 500,
// columns: ['Id', 'Abbrev', 'Name']
//});
//kendo.init($("#container"));
kendo.bind(view, vm);//this should eventually go away the recommended ViewModelBinder code in main.js is not firing for me
};
var vm = {
activate: activate,
deactivate: deactivate,
airports: airports,
title: 'Airports',
viewAttached: viewAttached
};
return vm;
});
One last issue that I am seeing:
It seems to me that an MVVM declared kendoui grid, bound to the view model through data-kendo-bind={source: airports)" where airports is a property of the viewmodel that was created through entities.asKendoDataSource() does not work. Somehow the grid does not show the data. Is there something extra that needs to be done?
Thanks
My best guess is that this is a timing issue, where the Grid is binding to vm.airports while it is still null, then vm.airports = airp.asKendoDataSource(); is called after it is already bound? Perhaps try something like:
return airportRes.then(function (airp) {
vm.airports.data(airp.asKendoDataSource().data());
});

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