Backbone.js and Parse.com - FRONT-END PROTOTYPING - Rapid Prototyping with JS: Agile JavaScript Development (2014)

Rapid Prototyping with JS: Agile JavaScript Development (2014)

II. FRONT-END PROTOTYPING

5. Backbone.js and Parse.com

Summary: illustration the Backbone.js uses with Parse.com and its JavaScript SDK on the modified Chat app; suggested list of additional features for the app.

“Java is to JavaScript what Car is to Carpet.”Chris Heilmann

If you’ve written some complex client-side applications, you might have found that it’s challenging to maintain the spaghetti code of JavaScript callbacks and UI events. Backbone.js provides a lightweight yet powerful way to organize your logic into a Model-View-Controller (MVC) type of structure. It also has nice features like URL routing, REST API support, event listeners and triggers. For more information and step-by-step examples of building Backbone.js applications from scratch, please refer to the chapter Intro to Backbone.js.

You can download the Backbone.js library at backbonejs.org. Then, after you included it just like any other JavaScript file in the head (or body) of your main HTML file, you’ll be able to access the Backbone class. For example, to create the router:

1 var ApplicationRouter = Backbone.Router.extend({

2 routes: {

3 "": "home",

4 "signup": "signup",

5 "*actions": "home"

6 },

7 initialize: function() {

8 this.headerView = new HeaderView();

9 this.headerView.render();

10 this.footerView = new FooterView();

11 this.footerView.render();

12 },

13 home: function() {

14 this.homeView = new HomeView();

15 this.homeView.render();

16 },

17 signup: function() {

18 ...

19 }

20 });

View, Models and Collections are created the same way:

1 HeaderView = Backbone.View.extend({

2 el: "#header",

3 template: '<div>...</div>',

4 events: {

5 "click #save": "saveMessage"

6 },

7 initialize: function() {

8 this.collection = new Collection();

9 this.collection.bind("update", this.render, this);

10 },

11 saveMessage: functiton() {

12 ...

13 },

14 render: function() {

15 $(this.el).html(_.template(this.template));

16 }

17 });

18

19 Model = Backbone.Model.extend({

20 url: "/api/item"

21 ...

22 )};

23

24 Collection = Backbone.Collection.extend({

25 ...

26 });

For a more details on Backbone.js, please refer to the Intro to Backbone.js chapter.

5.1 Chat with Parse.com: JavaScript SDK and Backbone.js version

It’s easy to see that if we keep adding more and more buttons like “DELETE”, “UPDATE” and other functionalities, our system of asynchronous callback will grow more complicated. And we’ll have to know when to update the view, i.e., the list of messages, based on whether or not there were changes to the data. The Backbone.js Model-View-Controller (MVC) framework can be used to make complex applications more manageable and easier to maintain.

If you felt comfortable with the previous example, let’s build upon it with the use of the Backbone.js framework. Here we’ll go step by step, creating a Chat application using Backbone.js and Parse.com JavaScript SDK. If you feel familiar enough with it, you could download the Super Simple Backbone Starter Kit at github.com/azat-co/super-simple-backbone-starter-kit. Integration with Backbone.js will allow for a straightforward implementation of user actions by binding them to asynchronous updates of the collection.

The application is available under rpjs/sdk, but again you are encouraged to start from scratch and try to write your own code using the example only as a reference.

Here is the structure of Chat with Parse.com, JavaScript SDK and Backbone.js version:

1 /sdk

2 -index.html

3 -home.html

4 -footer.html

5 -header.html

6 -app.js

7 /css

8 -bootstrap-responsive.css

9 -bootstrap-responsive.min.css

10 -bootstrap.css

11 -bootstrap.min.css

12 /img

13 -glyphicons-halflings-white.png

14 -glyphicons-halflings.png

15 /js

16 -bootstrap.js

17 -bootstrap.min.js

18 /libs

19 -require.min.js

20 -text.js

Create a folder; in the folder create an index.html file with the following content skeleton:

1 <!DOCTYPE html>

2 <html lang="en">

3 <head>

4 ...

5 </head>

6 <body>

7 ...

8 </body>

9 </html>

Download the necessary libraries or hot-link them from Google API. Now include JavaScript libraries and Twitter Bootstrap stylesheets into the head element along with other important but not required meta elements.

1 <head>

2 <meta charset="utf-8" />

3 <title></title>

4 <meta name="description" content="" />

5 <meta name="author" content="" />

We need this for responsive 'margin-bottom:6.0pt;line-height:normal'>1 <meta name="viewport"

2 content="width=device-width, initial-scale=1.0" />

Hot-linked jQuery from Google API:

1 <script type="text/javascript"

2 src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js">

3 </script>

Nice to have Twitter Bootstrap plug-ins:

1 <script type="text/javascript" src="js/bootstrap.min.js"></script>

Parse JavaScript SDK is hot-linked from Parse.com CDN:

1 <script type="text/javascript"

2 src="http://www.parsecdn.com/js/parse-1.0.5.min.js">

3 </script>

Twitter Bootstrap CSS inclusion:

1 <link type="text/css"

2 rel="stylesheet"

3 href="css/bootstrap.min.css" />

4 <link type="text/css"

5 rel="stylesheet"

6 href="css/bootstrap-responsive.min.css" />

Our JS application inclusion:

1 <script type="text/javascript" src="app.js"></script>

2 </head>

Populate the body element with Twitter Bootstrap scaffolding (more about it in the Basics chapter):

1 <body>

2 <div class="container-fluid">

3 <div class="row-fluid">

4 <div class="span12">

5 <div id="header">

6 </div>

7 </div>

8 </div>

9 <div class="row-fluid">

10 <div class="span12">

11 <div id="content">

12 </div>

13 </div>

14 </div>

15 <div class="row-fluid">

16 <div class="span12">

17 <div id="footer">

18 </div>

19 </div>

20 </div>

21 </div>

22 </body>

Create an app.js file and put Backbone.js views inside:

· headerView: menu and app-common information

· footerView: copyrights and contact links

· homeView: home page content

We use Require.js syntax and shim plugin for HTML templates:

1 require([

2 'libs/text!header.html',

3 'libs/text!home.html',

4 'libs/text!footer.html'], function (

5 headerTpl,

6 homeTpl,

7 footerTpl) {

The application router with a single index route:

1 var ApplicationRouter = Backbone.Router.extend({

2 routes: {

3 "": "home",

4 "*actions": "home"

5 },

Before we do anything else, we can initialize views which are going to be used across the app:

1 initialize: function() {

2 this.headerView = new HeaderView();

3 this.headerView.render();

4 this.footerView = new FooterView();

5 this.footerView.render();

6 },

This code takes care of the home route:

1 home: function() {

2 this.homeView = new HomeView();

3 this.homeView.render();

4 }

5 });

The header Backbone View is attached to the #header element and uses the headerTpl template:

1 HeaderView = Backbone.View.extend({

2 el: "#header",

3 templateFileName: "header.html",

4 template: headerTpl,

5 initialize: function() {

6 },

7 render: function() {

8 console.log(this.template)

9 $(this.el).html(_.template(this.template));

10 }

11 });

To render the HTML, we use the jQuery.html() function:

1 FooterView = Backbone.View.extend({

2 el: "#footer",

3 template: footerTpl,

4 render: function() {

5 this.$el.html(_.template(this.template));

6 }

7 });

The home Backbone View definition uses the #content DOM element:

1 HomeView = Backbone.View.extend({

2 el: "#content",

3 // template: "home.html",

4 template: homeTpl,

5 initialize: function() {

6 },

7 render: function() {

8 $(this.el).html(_.template(this.template));

9 }

10 });

To start an app, we create a new instance and call Backbone.history.start():

1 app = new ApplicationRouter();

2 Backbone.history.start();

3 });

The full code of the app.js file:

1 require([

2 'libs/text!header.html',

3 //example of a shim plugin use

4 'libs/text!home.html',

5 'libs/text!footer.html'], function (

6 headerTpl,

7 homeTpl,

8 footerTpl) {

9 var ApplicationRouter = Backbone.Router.extend({

10 routes: {

11 "": "home",

12 "*actions": "home"

13 },

14 initialize: function() {

15 this.headerView = new HeaderView();

16 this.headerView.render();

17 this.footerView = new FooterView();

18 this.footerView.render();

19 },

20 home: function() {

21 this.homeView = new HomeView();

22 this.homeView.render();

23 }

24 });

25 HeaderView = Backbone.View.extend({

26 el: "#header",

27 templateFileName: "header.html",

28 template: headerTpl,

29 initialize: function() {

30 },

31 render: function() {

32 console.log(this.template)

33 $(this.el).html(_.template(this.template));

34 }

35 });

36

37 FooterView = Backbone.View.extend({

38 el: "#footer",

39 template: footerTpl,

40 render: function() {

41 this.$el.html(_.template(this.template));

42 }

43 })

44 HomeView = Backbone.View.extend({

45 el: "#content",

46 // template: "home.html",

47 template: homeTpl,

48 initialize: function() {

49 },

50 render: function() {

51 $(this.el).html(_.template(this.template));

52 }

53 });

54 app = new ApplicationRouter();

55 Backbone.history.start();

56 });

The code above displays templates. All views and routers are inside, requiring the module to make sure that the templates are loaded before we begin to process them.

Here is what home.html looks like:

· A table of messages

· Underscore.js logic to output rows of the table

· A new message form

Let’s use the Twitter Bootstrap library structure (with its responsive components) by assigning row-fluid and span12 classes:

1 <div class="row-fluid" id="message-board">

2 <div class="span12">

3 <table class="table table-bordered table-striped">

4 <caption>Chat</caption>

5 <thead>

6 <tr>

7 <th class="span2">Username</th>

8 <th>Message</th>

9 </tr>

10 </thead>

11 <tbody>

This part has Underscore.js template instructions, which are just some JS code wrapped in <% and %> marks. _.each() is an iteration function from the UnderscoreJS library (underscorejs.org/#each), which does exactly what it sounds like — iterates through elements of an object/array:

1 <% if (models.length>0) {

2 _.each(models, function (value,key, list) { %>

3 <tr>

4 <td><%= value.attributes.username %></td>

5 <td><%= value.attributes.message %></td>

6 </tr>

7 <% });

8 }

9 else { %>

10 <tr>

11 <td colspan="2">No messages yet</td>

12 </tr>

13 <%}%>

14 </tbody>

15 </table>

16 </div>

17 </div>

For the new message form, we also use the row-fluid class and then add input elements:

1 <div class="row-fluid" id="new-message">

2 <div class="span12">

3 <form class="well form-inline">

4 <input type="text"

5 name="username"

6 class="input-small"

7 placeholder="Username" />

8 <input type="text" name="message"

9 class="input-small"

10 placeholder="Message Text" />

11 <a id="send" class="btn btn-primary">SEND</a>

12 </form>

13 </div>

14 </div>

The full code of the home.html template file:

1 <div class="row-fluid" id="message-board">

2 <div class="span12">

3 <table class="table table-bordered table-striped">

4 <caption>Chat</caption>

5 <thead>

6 <tr>

7 <th class="span2">Username</th>

8 <th>Message</th>

9 </tr>

10 </thead>

11 <tbody>

12 <% if (models.length>0) {

13 _.each(models, function (value,key, list) { %>

14 <tr>

15 <td><%= value.attributes.username %></td>

16 <td><%= value.attributes.message %></td>

17 </tr>

18 <% });

19 }

20 else { %>

21 <tr>

22 <td colspan="2">No messages yet</td>

23 </tr>

24 <%}%>

25 </tbody>

26 </table>

27 </div>

28 </div>

29 <div class="row-fluid" id="new-message">

30 <div class="span12">

31 <form class="well form-inline">

32 <input type="text"

33 name="username"

34 class="input-small"

35 placeholder="Username" />

36 <input type="text" name="message"

37 class="input-small"

38 placeholder="Message Text" />

39 <a id="send" class="btn btn-primary">SEND</a>

40 </form>

41 </div>

42 </div>

Now we can add the following components to:

· Parse.com collection,

· Parse.com model,

· Send/add message event,

· Getting/displaying messages functions.

Backbone compatible model object/class from Parse.com JS SDK with a mandatory className attribute (this is the name of the collection that will appear in the Data Browser of the Parse.com web interface):

1 Message = Parse.Object.extend({

2 className: "MessageBoard"

3 });

Backbone compatible collection object from Parse.com JavaScript SDK that points to the model:

1 MessageBoard = Parse.Collection.extend ({

2 model: Message

3 });

The home view needs to have click event listener on the “SEND” button:

1 HomeView = Backbone.View.extend({

2 el: "#content",

3 template: homeTpl,

4 events: {

5 "click #send": "saveMessage"

6 },

When we create homeView, let’s also create a collection and attach event listeners to it:

1 initialize: function() {

2 this.collection = new MessageBoard();

3 this.collection.bind("all", this.render, this);

4 this.collection.fetch();

5 this.collection.on("add", function(message) {

6 message.save(null, {

7 success: function(message) {

8 console.log('saved ' + message);

9 },

10 error: function(message) {

11 console.log('error');

12 }

13 });

14 console.log('saved' + message);

15 })

16 },

The definition of saveMessage() calls for the “SEND” button click event:

1 saveMessage: function(){

2 var newMessageForm = $("#new-message");

3 var username = newMessageForm.

4 find('[name="username"]').

5 attr('value');

6 var message = newMessageForm.

7 find('[name="message"]').

8 attr('value');

9 this.collection.add({

10 "username": username,

11 "message": message

12 });

13 },

14 render: function() {

15 console.log(this.collection);

16 $(this.el).html(_.template(

17 this.template,

18 this.collection

19 ));

20 }

The end result of our manipulations in app.js might look something like this:

1 /*

2 Rapid Prototyping with JS is a JavaScript

3 and Node.js book that will teach you how to build mobile

4 and web apps fast. — Read more at

5 http://rapidprototypingwithjs.com.

6 */

7

8 require([

9 'libs/text!header.html',

10 'libs/text!home.html',

11 'libs/text!footer.html'],

12 function (

13 headerTpl,

14 homeTpl,

15 footerTpl) {

16 Parse.initialize(

17 "your-parse-app-id",

18 "your-parse-js-sdk-key");

19 var ApplicationRouter = Backbone.Router.extend({

20 routes: {

21 "": "home",

22 "*actions": "home"

23 },

24 initialize: function() {

25 this.headerView = new HeaderView();

26 this.headerView.render();

27 this.footerView = new FooterView();

28 this.footerView.render();

29 },

30 home: function() {

31 this.homeView = new HomeView();

32 this.homeView.render();

33 }

34 });

35

36 HeaderView = Backbone.View.extend({

37 el: "#header",

38 templateFileName: "header.html",

39 template: headerTpl,

40 initialize: function() {

41 },

42 render: function() {

43 $(this.el).html(_.template(this.template));

44 }

45 });

46

47 FooterView = Backbone.View.extend({

48 el: "#footer",

49 template: footerTpl,

50 render: function() {

51 this.$el.html(_.template(this.template));

52 }

53 });

54 Message = Parse.Object.extend({

55 className: "MessageBoard"

56 });

57 MessageBoard = Parse.Collection.extend ({

58 model: Message

59 });

60

61 HomeView = Backbone.View.extend({

62 el: "#content",

63 template: homeTpl,

64 events: {

65 "click #send": "saveMessage"

66 },

67

68 initialize: function() {

69 this.collection = new MessageBoard();

70 this.collection.bind("all", this.render, this);

71 this.collection.fetch();

72 this.collection.on("add", function(message) {

73 message.save(null, {

74 success: function(message) {

75 console.log('saved '+message);

76 },

77 error: function(message) {

78 console.log('error');

79 }

80 });

81 console.log('saved'+message);

82 })

83 },

84 saveMessage: function(){

85 var newMessageForm=$("#new-message");

86 var username =

87 newMessageForm

88 .find('[name="username"]')

89 .attr('value');

90 var message = newMessageForm

91 .find('[name="message"]')

92 .attr('value');

93 this.collection.add({

94 "username": username,

95 "message": message

96 });

97 },

98 render: function() {

99 console.log(this.collection)

100 $(this.el).html(_.template(

101 this.template,

102 this.collection

103 ));

104 }

105 });

106

107 app = new ApplicationRouter();

108 Backbone.history.start();

109 });

The full source code of the Backbone.js and Parse.com Chat application is available under rpjs/sdk.

5.2 Deploying Chat to PaaS

Once you are comfortable that your front-end application works well locally, with or without a local HTTP server like MAMP or XAMPP, deploy it to Windows Azure or Heroku. In-depth deployment instructions are described in the jQuery and Parse.com chapter.

5.3 Enhancing Chat

In the last two examples, Chat had very basic functionality. You could enhance the application by adding more features.

Additional features for intermediate level developers:

· Sort the list of messages through the updateAt attribute before displaying it

· Add a “Refresh” button to update the list of messages

· Save the username after the first message entry in a run-time memory or in a session

· Add an up-vote button next to each message, and store the votes

· Add a down-vote button next to each message, and store the votes

Additional features for advanced level developers:

· Add a User collection

· Prevent the same user from voting multiple times

· Add user sign-up and log-in actions by using Parse.com functions

· Add a Delete Message button next to each message created by a user

· Add an Edit Message button next to each message created by a user