Thursday, May 1, 2014
Writing a small application using Parse and Backbone - Part 3
The Story So Far
In the last post, I set up a Backbone view and a model, and connected the two. I also connected the view to a route so that we could see it render with the data. This time I want to figure out:- How to query the Facebook Graph API
- How to persist data to the Parse backend
The Graph API
The first thing you will need to hit the Graph API is an access token. This token as an associated set of permissions which will determine which of your queries succeed. When you play with the Graph API Explorer, you will see a button saying "Get Access Token", which brings up a window asking you which permissions you are going to ask the Facebook user for:
When you do this through Parse, you will ask for permissions in the call to Parse.FacebookUtils.logIn. If the user grants you the permission you want, the access token will be in Parse.User.current()._serverData.authData.facebook.access_token.
To use this token to make a query, you call the FB.api function. Which works fine... except when the FB object doesn't exist yet. Or the Parse.User hasn't signed in. Or the access token has expired. All of which are possible, especially if the initialization flow is still in progress.
To handle this I wrote a little helper:
facebookQuery = function(queryString, callback) {
var doer = function () {
if ((!!window.FB) && (!!Parse.applicationId) && (!!Parse.User.current())) {
window.FB.api(queryString, { 'access_token': Parse.User.current()._serverData.authData.facebook.access_token }, function(response) {
// token expired! this logic needs to be a)understood, and b)put everywhere
if(response.hasOwnProperty('error') && response.error.code === 190) {
Parse.User.logOut();
Invite.loginUser(doer);
}
else {
callback(response);
}
});
}
else {
// wait 0.1s
setTimeout(doer, 100);
}
};
doer();
};
The 190 error code is what Facebook returns if the access token has expired. What I do is I log out and log back in, and this seems to work.
Persisting to Parse
They have actually made this pretty easy - the Parse.User object has a save method you can call. Here is some code which shows a Graph query for the logged-in user's profile information - you can see that I am asking for only the name, gender, and cover photo. Then I call the set method to assign to the fields, followed by save to send it to their servers. facebookQuery('/me?fields=name,cover,gender', function(response) {
// information to populate the model
var info = {
fbId : response.id,
realname : response.name,
gender : response.gender,
privacy_non_fb_see_only_name : Parse.User.current().get("privacy_non_fb_see_only_name"),
photoURL : response.cover && response.cover.source
};
// overwrite existing values, if any, and send to Parse
_.each(info, function(val, key) {
Parse.User.current().set(key, val);
});
Parse.User.current().save();
//
next(info);
});
Next Time
In the next post I'm going to build the core functionality of this application, which is the creation of an invite. The code is on GitHub if you want to follow along!Update (May 10 2014)
I'm not going to get to the next post for a while, some other tasks have come up at higher priority! I'll get to it when I get to it.Wednesday, April 30, 2014
Writing a small application using Parse and Backbone - Part 2
Last time
In the previous post I created a Backbone application and integrated it with Parse. I also managed to log the user in using their Facebook account. This time I'm going to create a couple of pages, which will require some understanding of how Backbone's views work.I managed to figure this out yesterday, so in this post I will just record what worked, along with a few of the problems I recall having encountered.
Routes
First I'm going to add a route called invites. Backbone's routing system is simple enough to get started with, you create a Router object and specify your URL patterns and their handlers. My first handler isn't going to do much; here is the code: Invite.Router = Backbone.Router.extend({
routes: {
"invites" : "allInvites",
},
allInvites : function() {
console.log("allInvites");
}
});
When the user navigates to /#invites, the handler allInvites is called. If you don't have the #, this won't work! The router only works on whatever comes after it, and it needs that starting point. This is not specific to Backbone, and if you Google "fragment identifier URI" you will learn more. I plan to do that one day myself.
The next route I added is to look at the details of a specific invite. This route takes a parameter, the invite's id.
Invite.Router = Backbone.Router.extend({
routes: {
"invites" : "allInvites",
"invite/:id" : "viewInvite"
},
allInvites : function() {
console.log("allInvites");
},
viewInvite : function(inviteId) {
console.log("viewInvite, id=" + inviteId);
}
});
You can see how the parameter is specified in the URL, and you will also notice that the parameter name in the URL doesn't need to match the parameter to the handling function.
Now let's make the handlers do something.
Views
To create a view in Backbone, you run Backbone.View.extend passing it a configuration object. This object could contain a function called initialize, which will be called without parameters when the view is created. In order for the view to do anything i.e. modify the HTML of a DOM element, you probably want to add a function which you can call when you're ready, and typically this is called render. But note that you have to call this function yourself, Backbone doesn't call it for you.After five minutes I realized that setting the HTML of a DOM element is quite easily the most painful experience in the world, and why don't they have a template mechanism, integrating with things like Handlebars? If I knew more about how Handlebars worked, I could have probably figured out how to plug it in, but as it happens, Yeoman came to my rescue again. I ran yo backbone:view AllInvites and it generated this nice file for me:
Invite.Views = Invite.Views || {};
(function () {
'use strict';
Invite.Views.AllInvites = Backbone.View.extend({
template: JST['app/scripts/templates/all_invites.ejs'],
tagName: 'div',
id: '',
className: '',
events: {},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
})();
Here's what I noticed:
- The initialize function sets up the render function to list to the change event on the model
- (There's something called a model)
- The render function takes the model and runs it through a templating mechanism, the output of which is HTML which is set to a DOM element called $el. According to this helpful site, the el is a div which is created automatically by Backbone if we don't specify it ourselves.
- The template seems to be in a file with an EJS extension.
So it looks like I need a model. This model object receives (generates?) a change event, and also has a JSON serialization method. So it can't be any old object, it's probably a Backbone.Model. No problem, I can look that up, and generate one using yo backbone:model once I'm ready.
But what is EJS...oh here it is. With example usage! I love the Internet. It doesn't look at pretty as Handlebars, but I don't care, it will get my work done.
<h2>AllInvitesView</h2>
<ul>
<%
_.each(this.model.models, function(invite) {
%>
<li>
<a href="#/invite/<%= invite.get('id') %>">Detailed View</a>
<%=
invite.get('what') + ' ' + invite.get('who') + " " + invite.get('when') + " " + invite.get('where').venueName
%>
</li>
<%
});
%>
</ul>
<a href="#">Home</a>
There's really only one thing to pay attention to: if you want to run JavaScript, enclose it in <% %>, but if you actually want the results to be inserted into the HTML, you have to use <%= %>.
Notice that the data is coming from this.model.models. I actually just stumbled upon it (with a little help from the Chrome developer console), and it worked . A little more exploring led me to understand that this.model refers to a collection (if the view is set up correctly), and the collection provides access to its objects via models. More on this below.
It's time to talk about the models now.
Models and Collections
Running the Yeoman generator yo backbone:model gave me a very simple file, which I haven't needed to touch yet. I ran it to create a model called Invite.Models.Invite, and left it there. But there is a separate generator for collections - yo backbone:collection - and that's what I want right now, since I'm working on a view which displays a collection.The backbone collection definition looks like this:
Invite.Collections = Invite.Collections || {};
(function () {
'use strict';
Invite.Collections.AllInvites = Backbone.Collection.extend({
model: Invite.Models.Invite
});
})();
You can see that the Invite model is referenced in the configuration object, this clearly tells the collection what it's going to hold (in addition, I inferred that that Backbone collections are homogeneous, this turned out to be incorrect).
Creating objects is easy, as is creating a collection and adding these objects to it. Here is some example code which creates some dummy data for my application:
var invDate = new Date(2014, 04, 19, 15, 0, 0, 0);
var venue = {
venueName:'Pizza Hut',
venueId: 1
};
appController.allInvites = new Invite.Collections.AllInvites();
appController.allInvites.add(new Invite.Models.Invite({ id: 1, what: 'fun', who: 'rohit', when: invDate, where: venue }));
appController.allInvites.add(new Invite.Models.Invite({ id: 2, what: 'fun', who: 'abrar', when: invDate, where: venue }));
appController.allInvites.add(new Invite.Models.Invite({ id: 3, what: 'fun', who: 'arun', when: invDate, where: venue }));
You can see that I have created the collection inside an object called appController (which I will get to soon), and then just called the add method on the new Invite objects. There is no schema for the Invite model, and it accepts numbers, strings, dates and sub-objects with no extra work on my part! These objects can now be found in the models property of the collection, i.e appController.allInvites.models.
Now that we know how to create models and collections, and to insert models into a collection, it's time to link them to our views. I'm going to start by linking the appController.allInvites collection to the AllInvites view created earlier.
The way to associate data to a view is just to set the view's model property to the model or collection you want. So I create my view like this:
var invitesView = new Invite.Views.AllInvites({model:Invite.appController.allInvites});
Now when I render the view, the template code will find the individual models and iterate over them in the loop
_.each(this.model.models, function(invite) {
....
});
The App Controller
I have been referring to an object called , which I haven't talked about too much yet. I'm using it as a global state holder and data store, and as I go forward I will figure out what else I want it to do. But I wanted to mention one piece of state it holds - the current view.I lifted this from this StackOverflow answer, it performs some cleanup on any current view before switching to the new view and calling render. Then it takes the current view's el HTML and assigns it to a div I created in the index.html.
function AppController() {
this.showView = function(view) {
if (this.currentView){
this.currentView.close();
}
this.currentView = view;
this.currentView.render();
$("#targetElement").html(this.currentView.el);
};
}
Invite.appController = new AppController();
The way I use this is I set up the view in each route handler, and ask the appController to show it.
Next time
That's it for this post. In my next post I'm going to figure out how to use Parse's models and collections.Tuesday, April 29, 2014
Writing a small application using Parse and Backbone - Part 1
Motivation
A few weeks ago I discovered the Parse backend-as-a-service, and immediately loved what I saw. So now I'm going to put together a small application, called Invite. I'm going to use Backbone as my front-end framework, because that's what Parse is based on.My usual strategy is to read one good post on the subject at hand, and then just Google everything else I need as I need it. Sometimes I find a tutorial which is so well written and suited to my immediate need that I can get by with only that one, but normally I just bounce around a bunch of different ones.
The code is on Github, you will find it here.
Yeoman
To get started, I figure I should install a Yeoman generator for Backbone. And... Yeoman craps out on me. Bunch of error messages about not being able to find files in the global node_modules etc. So instead of trying to make it work, I just removed my ENTIRE node.js installation and built it from source. Only this time, I specified a value for the --prefix flag while running configure.sh, essentially telling it to install to a folder in my home directory.No problems - it installs, and I run npm config ls to ensure that it shows the correct prefix. It doesn't, so I update the .npmrc in my home directory, which I had completely forgotten even existed.
The upside? Now I can install Yeoman globally without using sudo. Which I shall immediately do... and it works. Next I need the generator for Backbone, so I run Yeoman and asked it to find it for me. Yeoman is smart, it shows me a list of possible generators, and I choose generator-backbone since the others looked like Backbone + other stuff. And this time it works. Yeoman even confirms that it has installed the generator as a global node module. Thank you, Yeoman!
The scaffold
Now I'm running the Backbone generator, and Yeoman is doing its thing. It looks like it's installing a million node modules, mostly grunt stuff which I will not be looking at today or anytime soon. But more interestingly, it creates the scaffolding for my Backbone application! Let's take a look at what we've got:- An invite/app/index.html
- An app/scripts/main.js - this is where the application definition is. The application object has been named Invite (which Yeoman figured out from the name of the folder), and has been placed inside the global window object.
- Other files I'm not interested in right now
yo backbone:router places
yo backbone:collection places
yo backbone:model places
yo backbone:view places
which create a bunch of files which I will get to soon. Ok, great. Now I can get back to actually building this application.
The Application
This would be a good point for me to pin down exactly what I want to build. So here it is:- Users must sign in, and sign-in must be through their Facebook account
- Once signed in, they can see a list of People, initially just their Facebook friends
- They see a list of places they could hang out at in their city. Also from Facebook
- They can invite their friends to meet them at a place, after specifying when this should happen. I am going to find the absolute simplest date & time control to do this with.
- Their friends will see the invitation when they sign in, and will be able to accept or reject. A notification goes out.
Ok, that's enough to get started. So here's a list of pages we will need:
- Sign-Up / Sign-In
- List of People
- Person Details
- List of Places
- Place Details
- Invite
- See invitations, respond, see other responses and other updates
Facebook Login
Naturally, I expect Parse to be able to handle this simple workflow, so I just need to figure out how they do it. I'm going to read the documentation and just note down over here what I did.
1. Integrate the Facebook JavaScript SDK. Instructions are here. I put the fb-root div into index.html right after the opening <body> tag, and I include the Parse JavaScript SDK just before scripts/main.js is included.
2. Parse says that fbAsyncInit should contain a call to their own initialization routine rather than directly to Facebook's.
3. Hmm their init function wants a channel URL...
4. StackOverflow to the rescue. Created my local channel.html
5. I placed the Facebook code along with the Parse init in main.js
So far, so good. Now the Parse documentation wants me to call a function called Parse.FacebookUtils.logIn. But where should I call it from...hmm.
6. Ok, I'm going to put it into fbAsyncInit right after the call to Parse.FacebookUtils.init. So now fbAsyncInit contains calls to Parse.initialize, Parse.FacebookUtils.init and Parse.FacebookUtils.logIn.
7. Chrome blocks pop-ups from localhost by default. Changed the policy.
8. Now Facebook is complaining that the URL is wrong. Changed it in my Facebook app's settings page i.e. https://developers.facebook.com/apps/<my app id>/settings/
9. Refreshed my app. The browser's console logs tell me that the user is "signed up and logged in through Facebook!".
Next
In the next installment I will figure out routes, views and models, and build one of the pages for the app.
Subscribe to:
Posts (Atom)
