Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/sunesimonsen/7w17732

A Twitter application with the purpose of showing the capabilities of jQuery
https://github.com/sunesimonsen/7w17732

Last synced: about 1 month ago
JSON representation

A Twitter application with the purpose of showing the capabilities of jQuery

Awesome Lists containing this project

README

        

## Prerequisite ##

This lab requires some knowledge about JavaScript. Skill in CSS and HTML would also help.

You also need a Twitter account. If you don't want to mess with you existing account just create a new one, but make sure to follow a few people so you timeline will not be empty.

It is a good idea to have the developer tools installed in you favorite browser.

Before arriving to the KHelg, please execute the following steps.

git clone https://github.com/sunesimonsen/7W17732.git
cd 7W17732
mvn clean install

When you arrive you should do a the following to synchronize with the latest changes. (If you already have initiated the lab, you'll need to run `mvn lab:reset` first):

cd 7W17732
git pull
mvn clean install

## Introduction ##

In this lab we will explore how to build a larger single page JavaScript application. We will use a couple of different libraries that I think is appropriate for the job (That does not mean competitive alternatives does not exists).

JavaScript seems to be growing in popularity, but almost all the projects I have seen, that uses JavaScript to a larger extend, is suffering from structural problems. People kind of forget their good engineering practices when they get down and dirty with JavaScript. That is just plain wrong; JavaScript is extremely dynamic and should be approached with caution.

We will build a Twitter client using the following technologies

* Require.js
* jQuery
* Backbone
* jQueryUI
* Underscore.js
* Blueprint.css

Each of the libraries will be explained below.

The lab uses the maven-lab-plugin and gradually progress towards a more or less working Twitter client.

**Upon finishing a step in the lab all of your changes will be replace by a correct solution**. That means you don't have to complete a step correctly in order to advance to the next step.

Hope you will have fun!

### Require.js ###

Require.js is a JavaScript library used for asynchronously loading application modules and handling dependency management.

You define modules the following way using the *define* method:

File `modules/world.js`:

define([], function () {
return "World";
});

File `modules/hello.js`:

define(['modules/world'], function (world) {
return "Hello " + world;
});

File `app.js`:

require(["modules/hello"], function (hello) {
console.log(hello);
});

File index.html





When you open index.html in a browser *Require.js* will load the file app.js, that in turn will require the *hello* module. The *hello* module requires the *world* module, *Require.js* therefore downloads `hello.js` and `world.js` files and evaluates the define method of `world.js`. Then the *define* method of hello.js is evaluated with the result of `world.js` as argument.

This will result in the following line it the browser log:

Hello World

### jQuery ###

jQuery is a JavaScript library for manipulating the HTML DOM in a browser independent way.

Open www.jayway.com in Firefox or Chrome. Try running the following lines in the developer console one by one. You can open the developer console in Chrome by pressing `CS-j` and `CS-k` in Firefox.

$('.topNavigation a').css({color: 'pink'})
$("h1 a").fadeOut("slow").fadeIn("slow")
$('#siteBody .homePageBox .content').css({border: 'thick solid pink'})
$('#siteBody .homePageBox .content h3:eq(1)').text(".:jQuery:.").css({color: 'red', textAlign: 'center', fontSize: '3em'});

Let's just take a look at what is going on above. `$` is a reference to jQuery, that is uses to select a group of elements to work on using a CSS3 selector. When we have selected a group of element, we tell jQuery what to do with the selected elements.

So the statement below means: Select all links that is nested within an element with the css class *topNavigation* and change the text color to pink.

$('.topNavigation a').css({color: 'pink'})

Here is a couple of selectors:

#foo // selects the element with the id foo
.foo // selects elements with the css class foo
#foo .bar // selects elements with the css class foo inside an element with the id bar
.bar > a // selects links that are a child of an element with the css class bar

There are more selectors, but these are the most important. You can find more information at http://api.jquery.com/category/selectors/

This page shows the selectors in effect: http://codylindley.com/jqueryselectors/

As you can see jQuery is a very powerful tool and is well worth learning.

### Backbone.js ###

jQuery is fine and all, but if you are going to make a larger JavaScript application it is just not sufficient. It is extremely easy to make spaghetti code with jQuery, some might say it's a kind of a pasta machine. To take care of the overall structuring of your application, you can use a framework like Backbone. Backbone is a MVC framework, it is a little different than a usual MVC as the C stands for collections instead of controller. Backbone is very light weight and easy to understand; therefore I think it is a good choice for your applications.

Backbone consist of different types of objects:

* Router - routing page fragments to handlers
* View - controlling a part of the web page
* Collection - maps a collections of models to a collection resource on the server
* Model - represent a resource on the server

When a request is received by the router, it choices an action to execute. Such an action could for example be to render a view. The view can consist of any number of sub views that each have connected models. The views can listen for event from the models, collections and DOM elements, and when a model is updated all the views that listen for events on that model are notified.

We will go much more into details with Backbone in the lab.

### jQueryUI ###

jQueryUI makes available a lot of useful components, with very nice theme framework. When standard components are not sufficient to solve your problems, jQuery provides a widget factory to create your own custom UI components. We will look into how that works later in the lab.

### Underscore.js ###

Backbone make use of the utility and collection library Underscore.js which by it self is a very nice library, but in conjunction with Backbone is just plain awesome. We will mainly make use of Underscores templating capabilities.

### Blueprint.css ###

CSS is also one of those spaghetti creators. To approach the CSS spaghetti problem, one could make use of something like Sass to get a better structuring, but to limit the technology overflow I left it out.

One thing you always must do in a project that uses CSS, is to use a style sheet that resets all the different browser to a common ground. Blueprint provides such a style sheet in addition to a grid layout that can also be useful.

A little more involved starting point for a project is Boilerplate, that is also good to read up on.

## Start the lab ##

Now we are ready to start that lab :-)

Run the following command in the console to start the lab:

mvn lab:init

## Step 0: Start the application ##

In this step we will start the application by requiring the Application Backbone Router using Require.js.

Notice that *index.html* has a reference to require.js with a *data-main* attribute pointing to the application entry point.

Open the file client/js/app.js and add the following lines to the end of the file:

require(['router'], function(appRouter){
Backbone.history.start();
});

This will require the router module asynchronously and call the given function with the loaded module.

In order to start listening for URL changes we start the Backbone history tracking when the router is loaded.

Start the application with the following command in a console, and let it run in the background. There should be no need to restart the server:

mvn jetty:run

Open the following address in a browser:

http://0.0.0.0:8080/7W17732/

You should see the following message:

You now have a running application

Go to the next step by running:

mvn lab:next

## Step 1: Redirect to login if not authenticated ##

Open the file `client/js/router` in your editor.

Notice how the router is defined as a Require.js module that depends on the module *redirectIfNot*.

In the module we create a Backbone router that is returned to other modules that depends on the router.

I added a call to navigate to the home page from the default route, but the home page should only be accessible to authenticated users. So we will redirect the user to the login page if they are not authenticated.

We will use the helper method *redirectIfNot* to make the redirect if the given constraints are not fulfilled. This method makes a call to the server in order to figure out the current state of the application, and users the navigate method on the router to handle the redirect.

Replace the following line in the home method:

$('body').append("

This is the home screen

");

with the following lines to

redirectIfNot(["authenticated"], function () {
$('body').append("

This is the home screen

");
});

We also need to add a new route *login* to the router. The *login* route should be connected to the *showLogin* method:

routes: {
'home': 'home',
'login': 'showLogin',
// Default
'*actions': 'defaultAction'
},

Finally we need to implement the *showLogin* method. Instead of loading all the views when we load the main module, we can chose to load modules on demand. Add the following method to the router:

showLogin: function(){
this.clearContainer();
require(['views/Login'], function (view) {
view.render();
});
},

Notice how we get the *login* view dependency in the callback method and call *render* on the view.

Before rendering each view we clear the container element to ensure that old DOM elements are removed and event handlers on the container is unbinded. See the *clearContainer* method for the details.

When you refresh the browser you should be redirected to the *login* page.

Go to the next step by running:

mvn lab:next

## Step 2: Login to application ##

Open the file `client/views/Login.js` in you editor.

This module has a special dependency, that uses the Require.js text plugin. This plugin is capable of loading text files as strings. This is really useful for loading html template files, and in this case the views/Login.html file.

In the module we define a new Backbone view that is attacted to the element in the *index.html* page with the id *container*. In the render function for this view we replace the content of the root element with the loaded template.

First of all, let's change the *Login* button to a jQuery UI button by adding the following line to the render method.

this.$('button').button();

Backbone provides us with a reference to jQuery that is relative to the root element of this view. You could achieve the same by issuing the following command:

$('button', this.el).button();

That means: find the button element in the root element of the view and turn it into a jQueryUI button.

It is a really good idea to make almost all you jQuery code be relative to an element that is a close ancestor to the elements you would like to work on. That makes the code much more modular and helps avoiding situations where different parts of the code affect other parts unexpectedly. The same can be said for CSS, always limit your styles as much as possible.

Now let's add a click handler for the button. With plain jQuery, you would just added the click handler to the element as seen below:

$('button', this.el).click(function () {
...
});

But as this is really common, Backbone support adding event handlers to elements below the root element of a view in an easy way.

Add the following code to the events field.

events: {
'click button' : 'click'
}

This binds the *click* event of the elements below the root element that matches the CSS selector *button* to the *click* method on this view.

Now add a click method to the view:

click : function () {
console.log('clicked');
}

Make sure that the click handler is fired when clicking on the button.

Finally we will make an AJAX call to the Spring authenticate method on the server.

Add the following *click* method to the view:

click : function () {
var that = this;

var success = function(data, textStatus, jqXHR) {
if (data === "success") {
require("router").navigate("home", true);
} else {
that.setErrorMessage("Error logging in");
}
};

var error = function (jqXHR, textStatus, errorThrown) {
that.setErrorMessage(textStatus);
};

$.ajax({
url : "signin/authenticate",
type : "POST",
data : that.$("form").serialize(),
success : success,
error : error
});

return false;
}

We serialize the form to be `www-form-urlencoded` and send it off. If the user was authenticated we redirect the router to *home*; otherwise we show an error.

Take a look at the *setErrorMessage* method to see how jQuery calls can be chained together.

Try to log in with a wrong user name and password. Then you should see an error message.

Then try to log in with one of the users shown on the *login* box. Then you should be redirected to the Twitter connect page. Please connect to you Twitter account.

When you are redirected to the home page you should see the following message:

Home - nothing to be seen here

Go to the next step by running:

mvn lab:next

## Step 3: Showing Twitter timeline ##

In this step we will use Backbone collections and models to retrieve tweets from the server and show them on the home page.

First we need to implement the model for a tweet.

Open the file `client/js/models/Tweet.js` in your editor.

Making a new model is easy, because t Backbone model provides sensible defaults and a lot of features out of the box.

You don't actually need to change anything here, because the defaults are sufficient. But notice that we return the prototype for a tweet instead of a new instance of the tweet. That is because we need to create multiple instances of the tweet model.

Now open the `client/js/collections/HomeTimeline.js` file in you editor.

As you can see the module depends on the tweet model and defines a new Backbone collection.

The first thing we need to specify is the model for the elements in the collection. Then we specify the URL on the server that the collection maps to:

var HomeTimeline = Backbone.Collection.extend({
model: Tweet,
url: 'twitter/timeline/home'
});

When the fetch method is called on the collection a HTTP GET will be issued to the URL of the collection. For each element in the returned JSON response a model will be created.

There is just one problem, the server does not return a JSON array but a root element containing an array. So we need to add a parse method to the collection to retrieve the array:

parse: function(response) {
return response.tweetList;
}

Finally we want to sort the tweets by their creation time:

comparator: function (tweet) {
return -tweet.get("createdAt");
}

Now we just need to render the tweets. Open the `client/js/views/TimelineView.js` file in your editor.

First of all bind the *reset* event on the *timeTimeline* collection to the render method and call *fetch* on the collection:

initialize: function() {
homeTimeline.on('reset', this.render, this);
homeTimeline.fetch();
}

When the elements are fetched from the server the *reset* event will be triggered on the collection and the render method will be called.

We can then in the render method display the tweets on the view. Add the following code to the render method after the template has been inserted in the root element.

var timeline = this.$("> ul");
homeTimeline.each(function (tweet) {
var view = new TweetView({model: tweet});
timeline.append(view.render());
});

First we find the *ul* element just below the root element. Notice it is important to be quite strict when selecting elements in views that contains sub views to avoid selecting elements in the sub views. Then we traverse all the elements of the timeline collection, create a new *TweetView* for each model, and append the rendered view to the timeline element.

Finally we need to implement the *TweetView*. Open the `client/js/TweetView.js` file in your editor.

We will use the template method of the Underscore.js library to render the tweet views.

Take a look at the file `client/views/TweetView.html` and notice that it contains inline JavaScript code.

We would like to compile the template against a JSON version of the model. That is achieved by placing the following code in the render method:

$(this.el).html(_.template(template, this.model.toJSON()));

This line turns the model into JSON compiles the template against the data using the Underscore.js library and places the HTML in the root element of the view.

If you refresh the home page, you should see your timeline.

Go to the next step by running:

mvn lab:next

## Step 4: Tweeting your first tweet ##

We will start by adding support in the timeline view to listen for *add* events on the timeline collection.

Open the `client/js/TimelineView.js` and add the following line to the top of the *initialize* method.

homeTimeline.on('add', this.add, this);

Now when new models are added to the timeline collection the *add* method on the view will be called.

In the *add* method we will prepend a new tweet view to the timeline.

var view = new TweetView({model: tweet});
var tweetEl = $(view.render());
tweetEl.hide().prependTo(this.$('> ul')).slideDown("slow");

First we create the view and render it. In order to get a nice slide down effect, we hide the newly created view prepend it to the timeline and call the slide down effect on the element.

Now we only need to send a tweet to server when the user clicks on the *Tweet* button in the tweet editor.

Open `client/js/views/TweetEditor.js` in you editor.

In the *tweet* method add a tweet to the timeline collection using the create method.

var textArea = this.$('textarea');
homeTimeline.create({
text: textArea.val()
}, {wait: true});

textArea.val('');

We retrieve the value of the text area, create a data map with the text value and send it of to the server using the create method. We instruct the collection to wait with adding the tweet to the collection until the created version has been received from the server. Finally we clear the text area.

Refresh the page and try tweeting something.

Go to the next step by running:

mvn lab:next

## Step 5: Creating a custom jQuery UI widget ##

Open the `client/js/components/LimitedTextarea.js` file in you editor.

Notice that this file is not a require.js module. It is included directly in index.html. That is because jQuery has it's own *namespace* mechanism.

I this step we are going to create a jQuery UI widget that will enrich a text field with a max length indicator. It is a little involved, so I'll only run through the important aspects.

Notice that this widget is not complete, error handling and destroy methods are not implemented.

The widget will read the *maxlength* attribute of the target textarea, if the attribute is not provided it will take the length from the options map. The *maxlength* attribute is removed to allow longer text then the max length; the validation should take care of the ensuring valid data. Then it will take the target text area and surround it with a div containing an indicator element. This indicator element will be placed in the bottom left corner of the text area and be updated on key presses.

All code should be added to the *_create* method that will be called when the widget is created.

We can retrieve the max length and delete the attribute on the text area the following way:

var maxLength = textarea.attr('maxlength') ||
this.options.maxLength;
textarea.removeAttr('maxlength');

Now we create the component and indicator:

var indicator = $('

'+maxLength+'

');
indicator.css({
color: 'green', position: 'absolute',
right: 30, bottom: -10
});

var component = $('
');
component.css({position: 'relative'});
component.append('');
component.append(indicator);

This will create the following structure:




200




Then we will append this structure just after the target text area, and the replace the text area in the structure with the target text area:

component.insertAfter(textarea);
component.find('textarea').replaceWith(textarea);

This will move the target text area into the structure.

Now we need to update the indicator on keystrokes:

var updateIndicator = function () {
var length = textarea.val().length;
var remains = maxLength - length;
indicator.text(remains);
indicator.css({
color: remains > 10 ? 'green' : 'red'
});
};

textarea.keyup(updateIndicator);
textarea.keydown(updateIndicator);

The *updateIndicator* function calculates how much of the max length remains and updates the indicator accordingly.

Finally we update the indicator:

updateIndicator();

No we have a working widget, let's attach it to the textarea in the render method of `client/js/views/TweetEditor.js`:

this.$('textarea').limitedTextarea();

Refresh the page and type something in the textarea area.

Go to the next step by running:

mvn lab:next

## Step 6: Creating a retweet dialog ##

I have added a reply and a retweet link to the TweetView. When clicked they should open a dialog with the appropriate text.

Take a look at the methods in `client/js/views/TweetView.js` and implement the retweet function.

The text of the retweet should be something like:

"RT @{fromUser}: {text}"

Open `client/js/views/TweetEditorDialog.js` to implement the dialog. Use the documentation on the jQuery UI page to implement this step.

Set the value of the text area to the *text* property of the view options, when the dialog opens. Remember to use *that* instead of *this* for code in a nested scopes when referring to the view.

Turn the text area into a *LimitedTextarea* in the open event.

When the dialog closes it should be destroyed and the root element of the view should be removed from the DOM. Tip: you call method on the dialog the following way:

el.dialog("method",[args...])

Ensure that the DOM is cleaned properly when the dialog is closed to avoid memory leaks.

Add a *Tweet* button to the dialog that retrieves the text of the text area and creates a new Tweet on the homeTimeline collection.
Close the dialog afterwards. Remember to wait for the tweet to be received from the server before adding it to the collection.

Add a *Cancel* button to the dialog that closes this dialog.

Finally set the title of the dialog to the *title* property of the options map.

Refresh the page and see if you can reply and retweet.

Go to the next step by running:

mvn lab:next

## Step 7: Add tweet validation ##

In this step you should add validation to the tweet model before it is send to the server. Read the documentation for the validate method, and add the validation method to the model. The validation should check that the text to send is no more than 140 characters.

When you have added the validation method, change the `client/js/views/TweetEditor.js` view to highlight the text area when an invalid model is submitted. Furthermore only clear the text area when the submit was successful. You can be inspired by the way the `client/js/views/TweetEditorDialog.js` handles errors.

Go to the next step by running:

mvn lab:next

## Step 8: Now you are on your own ##

Geek out and change the application in a way that suits you, just be aware the if you change steps in the lab, your changes will be thrown away.

Hope you had fun!

PS. If you are one of those geniuses that is finished in ten minutes, please use the remaining time to make an Emacs mode for the maven-lab-plugin syntax ;-)