Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
1555 lines (1163 sloc) 63.591 kb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Web App Code Lab</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="Pete LePage petele@google.com">
<!-- Le styles -->
<link href="docs/prettify.css" type="text/css" rel="stylesheet" />
<link href="finalproject/css/bootstrap.css" rel="stylesheet" />
<style type="text/css">
body {
padding-top: 60px;
padding-bottom: 40px;
}
.sidebar-nav {
padding: 9px 0;
}
</style>
</head>
<body>
<a href="https://github.com/petele/WebApp-CodeLab"><img style="z-index: 2000; position: fixed; top: 0; right: 0; border: 0;" src="https://a248.e.akamai.net/assets.github.com/img/e6bef7a091f5f3138b8cd40bc3e114258dd68ddf/687474703a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f7265645f6161303030302e706e67" alt="Fork me on GitHub"></a>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="#">Web App Code Lab</a>
<div class="nav-collapse">
<ul class="nav">
<li><a href="index.html">Instructions</a></li>
<li><a href="slides/index.html">Slides</a></li>
<li><a href="finalproject/">Completed App</a></li>
<li><a href="https://github.com/petele/WebApp-CodeLab">Github Repo</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<div class="container-fluid">
<div class="row-fluid">
<div class="span3">
<div class="well sidebar-nav">
<ul class="nav nav-list">
<li class="nav-header">Instructions</li>
<li class="active"><a href="#">Introduction</a></li>
<li><a href="#">Exercise 1</a></li>
<li><a href="#">Exercise 2</a></li>
<li><a href="#">Exercise 3</a></li>
<li><a href="#">Exercise 4</a></li>
<li><a href="#">Exercise 5</a></li>
<li><a href="#">Exercise 6</a></li>
<li><a href="#">Exercise 7</a></li>
<li><a href="#">Exercise 8</a></li>
<li><a href="#">Exercise 9</a></li>
<li><a href="#">Exercise 10</a></li>
<li><a href="#">Exercise 11</a></li>
<li><a href="#">Exercise 12</a></li>
<li class="nav-header">Updates &amp; Errata</li>
<li><a href="errata.html">Errata</a></li>
<li class="nav-header">Library Documentation</li>
<li><a href="http://html5boilerplate.com/">HTML5 Boiler Plate</a></li>
<li><a href="http://emberjs.com/">Ember.js</a></li>
<li><a href="http://momentjs.com/">Moment.js</a></li>
<li><a href="http://twitter.github.com/bootstrap/">Bootstrap CSS Framework</a></li>
<li><a href="http://westcoastlogic.com/lawnchair/">LawnChair.js</a></li>
</ul>
</div><!--/.well -->
</div><!--/span-->
<div class="span9">
<h1>WReader Code Lab</h1>
<div class="alert alert-info">
<strong>Attention!</strong> Be sure to check the <a href="errata.html">errata documention</a> for important updates and a list of currently broken features.
</div>
<h2>Introduction</h2>
<p>This codelab covers the techniques and design fundamentals required to create modern, 'lick-able' web applications. The exercises look at the fundamentals of building web applications:</p>
<ul>
<li>Using an MVC framework</li>
<li>Making cross-domain requests and handling JSON data</li>
<li>Creating user interfaces &amp; experiences that are action oriented and app-like</li>
<li>Enabling offline experiences</li>
</ul>
<h3>Prerequisites</h3>
<p>Before you begin, make sure you've downloaded the required <a href="https://github.com/petele/WebApp-CodeLab/zipball/master">codelab files</a> from <a href="https://github.com/petele/WebApp-CodeLab">https://github.com/petele/WebApp-CodeLab</a>, that you have a working development environment, and a working web server. You must be able to successfully open and run <code>http://server/codelab/finalproject/index.html</code></p>
<h3>How To Proceed</h3>
<p>Each exercise has it's own folder (for example <code>/exercise1/</code>). It includes a folder <code>_solution</code> with working code you can use if you run out of time or get stuck. Progressive exercises folders include the solution from the previous exercises.</p>
<p>For each exercise, it's <em>recommended</em> that you start with the working version provided in the exercise folder to ensure that you're always starting from a known position, and that a mistake in a previous exercise doesn't pop up in a later exercise.</p>
<hr>
<h2>Exercise 1 - Boiler plate</h2>
<p>For our application, we've chosen to use <a href="http://html5boilerplate.com/">HTML5Boilerplate</a> as our starting point. Take a few minutes to have a look around the included files, and customize title and description in index.html. For now, leave the Google Analytics UA as is; we'll update that later.</p>
<hr>
<h2>Exercise 2 - Setup Model, Controller, and View</h2>
<p>We've chosen to use <a href="http://emberjs.com/">Ember.js</a> as our MVC framework, and have added that to the <code>/js/libs/</code> folder. We've also added a <code>dev-helper.js</code> file for some simple testing and development use and will remove this before we complete the project.</p>
<p>In this exercise, we'll create our data model and controller, and then add a simple view to the page so that we can see the items in our controller.</p>
<h3>Step 1: Creating the Ember object</h3>
<p>The first thing we need to do is create an Ember object to store our data feeds in. We will extend the <code>Em.Object</code> and create a new <code>Item</code> object with the following properties: <code>read</code>, <code>starred</code>, <code>item_id</code>, <code>title</code>, <code>pub_name</code>, <code>pub_author</code>, <code>pub_date</code>, <code>short_desc</code>, <code>content</code>, <code>feed_link</code>, and <code>item_link</code>.</p>
<h4>Exercise 2.1 (js/app.js)</h4>
<pre><code>// Ember Object model for entry items
WReader.Item = Em.Object.extend({
read: false,
starred: false,
item_id: null,
title: null,
pub_name: null,
pub_author: null,
pub_date: new Date(0),
short_desc: null,
content: null,
feed_link: null,
item_link: null
});
</code></pre>
<h3>Step 2: Create a data controller to store our items</h3>
<p>Next, we need to create a data controller object (<code>dataController</code>) by extending the Ember array controller class. Ember leaves it up to us how we store the data, and add items to the controller, so we'll need to add an array to store the data (<code>content: []</code>), and a method (<code>addItem: function(item)</code>) to add items to the array. To save some time, we've already added a binary search method (<code>binarySearch: function(value, low, high)</code>) to make it easier to add them items to the array in a <code>pub_date</code> sorted order.</p>
<h4>Exercise 2.2 (js/app.js)</h4>
<pre><code>// content array for Ember's data
content: [],
// Adds an item to the controller if it's not already in the controller
addItem: function(item) {
// Check to see if there are any items in the controller with the same
// item_id already
var exists = this.filterProperty('item_id', item.item_id).length;
if (exists === 0) {
// If no results are returned, we insert the new item into the data
// controller in order of publication date
var length = this.get('length'), idx;
idx = this.binarySearch(Date.parse(item.get('pub_date')), 0, length);
this.insertAt(idx, item);
return true;
} else {
// It's already in the data controller, so we won't re-add it.
return false;
}
},
</code></pre>
<h3>Step 3: Add a summary list view</h3>
<p>Now that we've got data into our controller, we need a way to display it on screen with a view. For now, we'll just create a simple view that creates an article tag, with two classes <code>well</code> and <code>summary</code>.</p>
<h4>Exercise 2.3 (js/app.js)</h4>
<pre><code>tagName: 'article',
classNames: ['well', 'summary']
</code></pre>
<h3>Step 4: Add the view to our HTML</h3>
<p>We've got the view defined in our framework, we need to render it in the HTML somewhere. We'll put this in the <code>mainContent</code> <code>section</code> element and put all of these items into a child section with the class <code>summaries</code>.</p>
<h4>Exercise 2.4 (index.html)</h4>
<pre><code>&lt;script type="text/x-handlebars"&gt;
{{#each WReader.dataController}}
{{#view WReader.SummaryListView contentBinding="this"}}
{{content.title}} from {{content.pub_name}} on {{content.pub_date}}
{{/view}}
{{/each}}
&lt;/script&gt;
</code></pre>
<p>The script block tells Ember's template compiler that the code between the tags should be rendered with the appropriate content from the defined controllers and views. We use <code>{{#each WReader.dataController}}</code> to specify that we want every item in the <code>dataController</code> to be rendered with the <code>WReader.SummaryListView</code>.</p>
<h3>Step 5: Let's try it out!</h3>
<p>Let's try it out. When we open the app in Chrome, we won't see anything, because nothing has been added to the data controller yet. We'll use a function in dev-helper.js to do that, and Ember will automatically update our view.</p>
<ol>
<li>Open exercise 2 in Chrome (eg <code>http://localhost/wreader/exercise2/index.html</code>)</li>
<li>Open the dev tools and switch to the console window</li>
<li>In the console window and type <code>addNewItems(10)</code> then hit enter</li>
</ol>
<p><em>If everything worked, you should now see 10 new articles, listing the title, publisher, and date.</em></p>
<hr>
<h2>Exercise 3 - Enhance View &amp; Controllers</h2>
<p>In this exercise, we'll add some additional properties to our controller so we can quickly query for the number of items, how many have been read, starred. We'll also create a function to format the date into something a little more human friendly.</p>
<h3>Step 1: Add additional properties to the dataController</h3>
<p>We want to show the number of items that are in our data controller, including the total count, how many have been read, how many are unread, and how many are starred. Ember allows us to make a function act like a property by adding <code>.property()</code> to the end of the function. The value we pass to the property function tells Ember when the specified item is updated, it needs to update the value of the property.</p>
<p>Let's look at an example for readCount:
readCount: function() {
return 1;
}.property('@each')</p>
<p>Right now, it simply returns 1, but we want it to return the number of read items in our data controller. To do that, we need to get the list of read items, and then get the length of that filtered list. Ember provides an easy way to filter objects with the <code>filterProperty(name, value)</code> function. Let's replace the <code>return 1;</code> with <code>return this.filterProperty('read', true).get('length');</code></p>
<p>Now that you've got how to get the number of read items, let's add properties for <code>itemCount</code>, <code>readCount</code>, <code>unreadCount</code> and <code>starredCount</code>.</p>
<h4>Exercise 3.1 (js/app.js)</h4>
<pre><code>// A 'property' that returns the count of items
itemCount: function() {
return this.get('length');
}.property('@each'),
// A 'property' that returns the count of read items
readCount: function() {
return this.filterProperty('read', true).get('length');
}.property('@each.read'),
// A 'property' that returns the count of unread items
unreadCount: function() {
return this.filterProperty('read', false).get('length');
}.property('@each.read'),
// A 'property' that returns the count of starred items
starredCount: function() {
return this.filterProperty('starred', true).get('length');
}.property('@each.starred')
</code></pre>
<h3>Step 2: Show new count properties in HTML</h3>
<p>Let's add the counts to our <code>index.html</code> page so we can see the counts for items that are in our data controller. Ember's templating feature allow us to pull data straight from controllers in addition to views, by providing the full namespace. For example, <code>{{WReader.dataController.itemCount}}</code> will display the value for the <code>itemCount</code> property on the object <code>dataController</code>.</p>
<h4>Exercise 3.2 (index.html)</h4>
<pre><code>&lt;div&gt;
{{WReader.dataController.itemCount}} items &lt;br /&gt;
{{WReader.dataController.unreadCount}} are unread&lt;br /&gt;
{{WReader.dataController.readCount}} are read&lt;br /&gt;
{{WReader.dataController.starredCount}} are starred&lt;br /&gt;
&lt;/div&gt;
</code></pre>
<h3>Step 3: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 3 in Chrome (eg <code>http://localhost/wreader/exercise3/index.html</code>)</li>
<li>Open the dev tools and switch to the console window</li>
<li>In the console window, type <code>addNewItems(10)</code> and hit enter</li>
</ol>
<p><em>If everything worked, you should now see 10 new articles, listing the title, publisher, and date, as well as the counts read, unread starred and total items.</em></p>
<h3>Step 4: Add additional class bindings to SummaryListView</h3>
<p>We want Ember to add the <code>read</code> and/or <code>starred</code> class to items in the <code>SummaryListView</code> if they've already been read or starred. To do that, we need to add a <code>classNameBindings</code> property to the view, and add matching properties to the view object.</p>
<p>First, let's add the <code>classNameBinding</code> property, and bind the <code>read</code> and <code>starred</code> properties, if the property returns <code>true</code>, it'll add the bound class.</p>
<h4>Exercise 3.4a (js/app.js)</h4>
<pre><code>classNameBindings: ['read', 'starred']
</code></pre>
<p>Next, we need to add the properties for <code>read</code> and <code>starred</code> items. Since we already have a property on the item for <code>read</code> and <code>starred</code>, we can simply query that property and return its result. Like the counts, these are also properties, but instead of being updated every time <code>@each</code> object changes, we want them to update as the items change in the controller.</p>
<h4>Exercise 3.4b (js/app.js)</h4>
<pre><code>// Enables/Disables the read CSS class
read: function() {
var read = this.get('content').get('read');
return read;
}.property('WReader.itemsController.@each.read'),
// Enables/Disables the read CSS class
starred: function() {
var starred = this.get('content').get('starred');
return starred;
}.property('WReader.itemsController.@each.starred')
</code></pre>
<h3>Step 5: Custom date formatting</h3>
<p>Finally, let's format the date in to something a little more human readable. We've added <a href="http://momentjs.com/">Moment.js</a>, a date &amp; time library that makes date formatting a lot easier. Like we did for the <code>read</code> and <code>starred</code> properties on the view, we'll create a property on the view to return the formatted date.</p>
<h4>Exercise 3.5 (js/app.js)</h4>
<pre><code>// Returns the date in a human readable format
formattedDate: function() {
var d = this.get('content').get('pub_date');
return moment(d).format('MMMM Do, YYYY');
}.property('WReader.itemsController.@each.pub_date')
</code></pre>
<h3>Step 6: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 3 in Chrome (eg <code>http://localhost/wreader/exercise3/index.html</code>)</li>
<li>Open the dev tools and switch to the console window</li>
<li>In the console window, type <code>addNewItems(10)</code> and hit enter</li>
</ol>
<p><em>If everything worked, you should now see 10 new articles, listing the title, publisher, and a nicely formatted date. If you've got extra time, play around with moment.js to see what other formats you can put the date into.</em></p>
<hr>
<h2>Exercise 4 - Add New Controller &amp; NavBar</h2>
<p>In exercise 4, we'll add a new controller for filtering what's visible on screen and hook up a set of click events to the counters.</p>
<h3>Step 1: Create a new itemsController</h3>
<p>The <code>dataController</code> contains all of the items in our data store, but we don't always want to show that, sometimes we may only want to show the read items, or the starred items, or maybe just the unread items. To do that, we'll create a new controller (<code>itemsController</code>), that will contain only the visible items.</p>
<p>We also want to add two new functions to this controller, <code>clearFilter()</code> and <code>filterBy(key, value)</code>. Using <code>filterBy()</code> set's the content of <code>itemsController</code> to the filtered results of the <code>dataController</code>.</p>
<h4>Exercise 4.1 (js/app.js)</h4>
<pre><code>WReader.itemsController = Em.ArrayController.create({
// content array for Ember's data
content: [],
// Sets content[] to the filtered results of the data controller
filterBy: function(key, value) {
this.set('content', WReader.dataController.filterProperty(key, value));
},
// Sets content[] to all items in the data controller
clearFilter: function() {
this.set('content', WReader.dataController.get('content'));
},
// Shortcut for filterBy
showDefault: function() {
this.filterBy('read', false);
},
</code></pre>
<h3>Step 2: Add a navigation bar view</h3>
<p>The next step is to create a new view for the fixed navigation bar that will run across the top of the UI. Its primary purpose will be to show the number of items in the <code>dataController</code>, and update the <code>itemsController</code>.</p>
<p>In <code>NavBarView</code>, let's add properties for <code>itemCount</code>, <code>unreadCount</code>, <code>starredCount</code>, and <code>readCount</code> that returns the number of items in the <code>dataController</code>. For example, <code>itemCount</code> would return <code>WReader.dataController.get('itemCount');</code></p>
<h4>Exercise 4.2 (js/app.js)</h4>
<pre><code>// A 'property' that returns the count of items
itemCount: function() {
return WReader.dataController.get('itemCount');
}.property('WReader.dataController.itemCount'),
// A 'property' that returns the count of unread items
unreadCount: function() {
return WReader.dataController.get('unreadCount');
}.property('WReader.dataController.unreadCount'),
// A 'property' that returns the count of starred items
starredCount: function() {
return WReader.dataController.get('starredCount');
}.property('WReader.dataController.starredCount'),
// A 'property' that returns the count of read items
readCount: function() {
return WReader.dataController.get('readCount');
}.property('WReader.dataController.readCount')
</code></pre>
<h3>Step 3: Add the navigation bar to the index.html</h3>
<p>Now that we've created the view, we need a place for it to display in our HTML. Since this will be a fixed navigation bar across the top of the page, we'll put it in a <code>&lt;header&gt;</code> element just below the <code>body</code> element.</p>
<p>Note: that we're wrapping the counts in anchor elements, we'll deal with that in the next step.</p>
<h4>Exercise 4.3 (index.html)</h4>
<pre><code>&lt;header&gt;
&lt;script type="text/x-handlebars"&gt;
{{#view WReader.NavBarView}}
&lt;ul&gt;
&lt;li&gt;&lt;a&gt;{{itemCount}} Items&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a&gt;{{unreadCount}} Unread&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a&gt;{{starredCount}} Starred&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a&gt;{{readCount}} Read&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
{{/view}}
&lt;/script&gt;
&lt;/header&gt;
</code></pre>
<h3>Step 4: Add the click handlers for the anchors</h3>
<p>To change what's displayed in the <code>SummaryListView</code>, we need to add click handlers to the anchor elements. Adding a click handler is done by adding <code>{{action "function" on="click"}}</code> to each anchor element.</p>
<h4>Exercise 4.4a (index.html)</h4>
<pre><code>&lt;li&gt;&lt;a {{action "showAll" on="click"}}&gt;{{itemCount}} Items&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a {{action "showUnread" on="click"}}&gt;{{unreadCount}} Unread&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a {{action "showStarred" on="click"}}&gt;{{starredCount}} Starred&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a {{action "showRead" on="click"}}&gt;{{readCount}} Read&lt;/a&gt;&lt;/li&gt;
</code></pre>
<p>Next, we need to handle the clicks in the <code>NavBarView</code> by creating a new function for each of the methods we've indicated, <code>showAll</code>, <code>showUnread</code>, <code>showStarred</code>, and <code>showRead</code>.</p>
<h4>Exercise 4.4b (js/app.js)</h4>
<pre><code>// Click handler for menu bar
showAll: function() {
WReader.itemsController.clearFilter();
},
// Click handler for menu bar
showUnread: function() {
WReader.itemsController.filterBy('read', false);
},
// Click handler for menu bar
showStarred: function() {
WReader.itemsController.filterBy('starred', true);
},
// Click handler for menu bar
showRead: function() {
WReader.itemsController.filterBy('read', true);
}
</code></pre>
<h3>Step 5: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 4 in Chrome (eg <code>http://localhost/wreader/exercise4/index.html</code>)</li>
<li>Open the dev tools and switch to the console window</li>
<li>In the console window, type <code>addNewItems(10)</code> and hit enter</li>
<li>Click on each of the different counts to see the items list change</li>
</ol>
<p><em>If everything worked, you should now see 10 new articles, listing the title, publisher, and a nicely formatted date. Clicking on the different counts should update the displayed list.</em></p>
<hr>
<h2>Exercise 5 - Pull Data From Server</h2>
<p>In this exercise, we're going to make a cross domain request to get the RSS feed data from another server, and then insert it into our data controller. We'll use a Yahoo pipe to get the feed, since it supports CORS requests, and pull RSS feed from the Chromium blog.</p>
<h3>Step 1: Create the GetItemsFromServer function</h3>
<p>jQuery and the browser are smart enough to handle CORS requests for us and there is little extra that we need to do! We simply need to supply it with the URL we want to make the request to.</p>
<p>First, let's create the GetItemsFromServer function off the WReader namespace, then we'll craft our request URL.</p>
<h4>Exercise 5.1 (js/app.js)</h4>
<pre><code>WReader.GetItemsFromServer = function() {
// URL to data feed that I plan to consume
var feed = "http://blog.chromium.org/feeds/posts/default?alt=rss";
feed = encodeURIComponent(feed);
// Feed parser that supports CORS and returns data as a JSON string
var feedPipeURL = "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20xml%20where%20url%3D'";
feedPipeURL += feed + "'&amp;format=json";
//console.log("Starting AJAX Request:", feedPipeURL);
/* Exercise 5.2 */
};
</code></pre>
<h3>Step 2: Use jQuery to make the AJAX request</h3>
<p>Next, we can make the actual request using jQuery</p>
<h4>Exercise 5.2 (js/app.js)</h4>
<pre><code>$.ajax({
url: feedPipeURL,
dataType: 'json',
success: function(data) {
/* Exercise 5.3 */
});
</code></pre>
<h3>Step 3: Parse the returned data, create new items &amp; insert into controller</h3>
<p>Once the data has been returned from the server, we can parse the data into individual items and insert them into the <code>dataController</code>. Ember provides a <code>map</code> function that makes it easy to iterate through an array and do something with that array. In our case, we'll use the <code>map</code> function to iterate over the RSS feed items, create new a Ember object then insert it into the <code>dataController</code>.</p>
<h4>Exercise 5.3 (js/app.js)</h4>
<pre><code>// Get the items object from the result
var items = data.query.results.rss.channel.item;
// Get the original feed URL from the result
var feedLink = data.query.results.rss.channel.link;
// Use map to iterate through the items and create a new JSON object for
// each item
items.map(function(entry) {
var item = {};
// Set the item ID to the item GUID
item.item_id = entry.guid.content;
// Set the publication name to the RSS Feed Title
item.pub_name = data.query.results.rss.channel.title;
item.pub_author = entry.author;
item.title = entry.title;
// Set the link to the entry to it's original source if it exists
// or set it to the entry link
if (entry.origLink) {
item.item_link = entry.origLink;
} else if (entry.link) {
item.item_link = entry.link;
}
item.feed_link = feedLink;
// Set the content of the entry
item.content = entry.description;
// Ensure the summary is less than 128 characters
if (entry.description) {
item.short_desc = entry.description.substr(0, 128) + "...";
}
// Create a new date object with the entry publication date
item.pub_date = new Date(entry.pubDate);
item.read = false;
// Set the item key to the item_id/GUID
item.key = item.item_id;
// Create the Ember object based on the JavaScript object
var emItem = WReader.Item.create(item);
// Try to add the item to the data controller, if it's successfully
// added, we get TRUE and add the item to the local data store,
// otherwise it's likely already in the local data store.
WReader.dataController.addItem(emItem);
});
// Refresh the visible items
WReader.itemsController.showDefault();
</code></pre>
<h3>Step 4: Fire GetItemsFromServer() at application start</h3>
<p>Since we want the application to start and get the latest data from the server, we'll add <code>WReader.GetItemsFromServer();</code> to the <code>var WReader = Em.Application.create({ ... });</code> function.</p>
<h4>Exercise 5.4 (js/app.js)</h4>
<pre><code>WReader.GetItemsFromServer();
</code></pre>
<h3>Step 5: Add a refresh button to the NavBarView</h3>
<p>Our last step for this exercise will be to add a refresh button to the <code>NavBarView</code>, which means we need to add a click handler to <code>NavBarView</code> and an anchor tag in <code>index.html</code> that will fire the handler.</p>
<h4>Exercise 5.5a (js/app.js)</h4>
<pre><code>// Click handler for menu bar
refresh: function() {
WReader.GetItemsFromServer();
}
</code></pre>
<h4>Exercise 5.5b (index.html)</h4>
<pre><code>&lt;li&gt;&lt;a {{action "refresh" on="click"}}&gt;Reload&lt;/a&gt;&lt;/li&gt;
</code></pre>
<h3>Step 6: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 5 in Chrome (eg <code>http://localhost/wreader/exercise5/index.html</code>)</li>
<li>Click on each of the different counts to see the items list change</li>
</ol>
<p><em>If everything worked, you should now see 20 new articles loaded from the web service, listing the title, publisher, and a nicely formatted date. Clicking on the different counts should update the displayed list.</em></p>
<hr>
<h2>Exercise 6 - Setup Application UI and UX</h2>
<p>In exercise 6, we'll Twitter's Bootstrap UI framework, and a number of visual styling elements to get WReader to look like a real web application instead of the jumble of text that it is now.</p>
<h3>Step 1: Add Bootstrap's UI style sheet</h3>
<p>We've already downloaded <a href="http://twitter.github.com/bootstrap/">Twitter's Bootstrap</a>, so we need to include the style sheet in our HTML. For now, we'll only include the CSS style sheet, but it also comes with a small JavaScript that adds additional functionality.</p>
<h4>Exercise 6.1 (index.html)</h4>
<pre><code>&lt;link rel="stylesheet" href="css/bootstrap.css"&gt;
</code></pre>
<h3>Step 2: Add styling &amp; semantics to header</h3>
<p>Next, let's update the nav bar in <code>index.html</code> to be styled to use <a href="http://twitter.github.com/bootstrap/components.html#navbar">Twitter's NavBar</a> look and feel.</p>
<p>The navbar requires only a few divs to structure it well for static or fixed display.</p>
<pre><code>&lt;div class="navbar navbar-fixed-top"&gt;
&lt;div class="navbar-inner"&gt;
&lt;div class="container"&gt;
&lt;a class="brand"&gt;...&lt;/a&gt;
&lt;ul class="nav"&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h4>Exercise 6.2 (index.html)</h4>
<pre><code>&lt;header&gt;
&lt;script type="text/x-handlebars"&gt;
{{#view WReader.NavBarView}}
&lt;div class="navbar navbar-fixed-top"&gt;
&lt;div class="navbar-inner"&gt;
&lt;div class="container"&gt;
&lt;a class="brand"&gt;wReader&lt;/a&gt;
&lt;ul class="nav"&gt;
&lt;li class="itemCount"&gt;&lt;a {{action "showAll" on="click"}}&gt;{{itemCount}} Items&lt;/a&gt;&lt;/li&gt;
&lt;li class="itemCount"&gt;&lt;a {{action "showUnread" on="click"}}&gt;{{unreadCount}} Unread&lt;/a&gt;&lt;/li&gt;
&lt;li class="itemCount"&gt;&lt;a {{action "showStarred" on="click"}}&gt;{{starredCount}} Starred&lt;/a&gt;&lt;/li&gt;
&lt;li class="itemCount"&gt;&lt;a {{action "showRead" on="click"}}&gt;{{readCount}} Read&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;form class="navbar-search pull-left" id="navSearch"&gt;
&lt;input type="text" class="search-query" placeholder="Search"&gt;
&lt;/form&gt;
&lt;ul class="nav pull-right"&gt;
&lt;li&gt;&lt;a {{action "refresh" on="click"}}&gt;&lt;i class="icon-refresh icon-white"&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
{{/view}}
&lt;/script&gt;
&lt;/header&gt;
</code></pre>
<h3>Step 3: Apply appropriate classes to mainContent</h3>
<p>In order to prepare for our three column layout that we're about to add, we need to add two new divs to the <code>mainContent</code> <code>section</code>, one that will display the navigation controls on the left, and one to display the selected item on the right.</p>
<h4>Exercise 6.3a (index.html)</h4>
<pre><code>&lt;section class="controls"&gt;controls&lt;/section&gt;
</code></pre>
<h4>Exercise 6.3b (index.html)</h4>
<pre><code>&lt;section class="entries"&gt;entries&lt;/section&gt;
</code></pre>
<h3>Step 4: Set up 3 column layout for main panel</h3>
<p>Now it's time to set up the three column layout using the flex-box model. We've already setup a couple of classes in our <code>css.html</code> file to reduce some of the annoying styling that you'd need to do.</p>
<p>First, let's set the style on <code>mainContent</code> that contains all of our content</p>
<h4>Exercise 6.4a (style.css)</h4>
<pre><code>section.mainContent {
display: -webkit-box;
-webkit-box-orient: horizontal;
overflow: hidden;
height: 100%;
width: 100%;
box-sizing: border-box;
padding-bottom: 0px;
}
</code></pre>
<p>We also need to make sure that all <code>div</code>'s that are child elements of <code>section.mainContent</code> to be the full height of the window.</p>
<h4>Exercise 6.4b (style.css)</h4>
<pre><code>section.mainContent div {
height: 100%;
}
</code></pre>
<p>Finally, let's add the CSS to specify how we want the look and feel for each of the three columns</p>
<h4>Exercise 6.4c (style.css)</h4>
<pre><code>.controls {
box-sizing: border-box;
padding: 5px;
width: 50px;
margin: 0;
height: 100%;
}
.summaries {
box-sizing: border-box;
padding: 10px;
overflow-y: scroll;
width: 300px;
}
.entries {
padding: 10px;
box-sizing: border-box;
-webkit-box-flex: 1;
overflow-y: scroll;
padding-left: 10px;
}
</code></pre>
<h3>Step 5: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 6 in Chrome (eg <code>http://localhost/wreader/exercise6/index.html</code>)</li>
<li>Click on each of the different counts to see the items list change</li>
</ol>
<hr>
<h2>Exercise 7 - Add selected item controller &amp; view for controller</h2>
<p>In this exercise, we'll add the selected item controller to show which item are currently 'selected', and provide functionality to move to the previous, or next one, mark the current one as read/unread and add or remove the starred attribute.</p>
<h3>Step 1: Create the SelectedItemController</h3>
<p>Like we have with our previous controllers, we need to create a new controller by extending Ember's base object <code>WReader.selectedItemController = Em.Object.create({ ... });</code> In it we will need to add several properties, for example <code>selectedItem</code> to hold the selected item, <code>hasPrev</code> and <code>hasNext</code> to indicate if there are items in <code>itemsController</code> before or after the selected item.</p>
<h4>Exercise 7.1a (js/app.js)</h4>
<pre><code>// Pointer to the seclected item
selectedItem: null,
hasPrev: false,
hasNext: false,
// Called to select an item
select: function(item) {
this.set('selectedItem', item);
if (item) {
this.toggleRead(true);
// Determine if we have a previous/next item in the array
var currentIndex = WReader.itemsController.content.indexOf(this.get('selectedItem'));
if (currentIndex + 1 &gt;= WReader.itemsController.get('itemCount')) {
this.set('hasNext', false);
} else {
this.set('hasNext', true);
}
if (currentIndex === 0) {
this.set('hasPrev', false);
} else {
this.set('hasPrev', true);
}
} else {
this.set('hasPrev', false);
this.set('hasNext', false);
}
},
</code></pre>
<p>The <code>selectedItemController</code> sets <code>hasPrev</code> and <code>hasNext</code> when an item is selected by checking where in the array index our selected item falls.</p>
<p>Next, we want to be able to both toggle and explictly set the read and starred state for this item. We'll do that by creating two functions <code>toggleRead(bool)</code> and <code>toggleStar(bool)</code>. The parameter is optional, and if it's not set, we should toggle the flag.</p>
<h4>Exercise 7.1b (js/app.js)</h4>
<pre><code>// Toggles or sets the read state with an optional boolean
toggleRead: function(read) {
if (read === undefined) {
read = !this.selectedItem.get('read');
}
this.selectedItem.set('read', read);
var key = this.selectedItem.get('item_id');
},
// Toggles or sets the starred status with an optional boolean
toggleStar: function(star) {
if (star === undefined) {
star = !this.selectedItem.get('starred');
}
this.selectedItem.set('starred', star);
var key = this.selectedItem.get('item_id');
},
</code></pre>
<p>Finally, we want to have a way to move to the previous or next item in <code>itemsController</code></p>
<h4>Exercise 7.1c (js/app.js)</h4>
<pre><code>// Selects the next item in the item controller
next: function() {
// Get's the current index in case we've changed the list of items, if the
// item is no longer visible, it will return -1.
var currentIndex = WReader.itemsController.content.indexOf(this.get('selectedItem'));
// Figure out the next item by adding 1, which will put it at the start
// of the newly selected items if they've changed.
var nextItem = WReader.itemsController.content[currentIndex + 1];
if (nextItem) {
this.select(nextItem);
}
},
// Selects the previous item in the item controller
prev: function() {
// Get's the current index in case we've changed the list of items, if the
// item is no longer visible, it will return -1.
var currentIndex = WReader.itemsController.content.indexOf(this.get('selectedItem'));
// Figure out the previous item by subtracting 1, which will result in an
// item not found if we're already at 0
var prevItem = WReader.itemsController.content[currentIndex - 1];
if (prevItem) {
this.select(prevItem);
}
}
</code></pre>
<h3>Step 2: Adding our EntryItemView</h3>
<p>By this time, hopefully you've got the hang of Ember's views, so let's create <code>WReader.EntryItemView</code> by extending <code>Em.View</code> as we've done in the past. Since the content that will be represented is effectively an article from a blog, we'll specifically set the <code>tagName</code>, and add a few default classes like <code>well</code> and <code>entry</code>.</p>
<h4>Exercise 7.2a (js/app.js)</h4>
<pre><code>tagName: 'article',
contentBinding: 'WReader.selectedItemController.selectedItem',
classNames: ['well', 'entry'],
</code></pre>
<p>But, let's take this a step further, and instead of using the <code>classNameBindings</code> like we have in the past, let's actually specify the classes we want used for each item. That way, we can have one class use if an item is read, and a different one if it isn't. Twitter's Bootstrap library includes icons that will work perfectly for read/unread and starred/unstarred states. We've also used <a href="http://moment.js">Moment.js</a> again to provide some nice date formatting.</p>
<h4>Exercise 7.2b (js/app.js)</h4>
<pre><code>// Enables/Disables the active CSS class
active: function() {
return true;
}.property('WReader.selectedItemController.selectedItem'),
starClass: function() {
var selectedItem = WReader.selectedItemController.get('selectedItem');
if (selectedItem) {
if (selectedItem.get('starred')) {
return 'icon-star';
}
}
return 'icon-star-empty';
}.property('WReader.selectedItemController.selectedItem.starred'),
readClass: function() {
var selectedItem = WReader.selectedItemController.get('selectedItem');
if (selectedItem) {
if (selectedItem.get('read')) {
return 'icon-ok-sign';
}
}
return 'icon-ok-circle';
}.property('WReader.selectedItemController.selectedItem.read'),
// Returns a human readable date
formattedDate: function() {
var d = this.get('content').get('pub_date');
return moment(d).format("MMMM Do YYYY, h:mm a");
}.property('WReader.selectedItemController.selectedItem')
</code></pre>
<h3>Step 3: Add a click handler to the SummaryListView items</h3>
<p>Next we need some kind of way to select items from the <code>SummaryListView</code>. To do that, we'll add a click handler to the view that will set the <code>selectedItemController</code>'s item to the item that was clicked on. Ember is very helpful because it knows about certain event types (like <code>click</code>), which means we can add it to the <code>view</code>, but don't have to make any changes to our markup.</p>
<h4>Exercise 7.3 (js/app.js)</h4>
<pre><code>// Handle clicks on an item summary
click: function(evt) {
// Figure out what the user just clicked on, then set selectedItemController
var content = this.get('content');
WReader.selectedItemController.select(content);
},
</code></pre>
<p>If we try running this now, clicking on an item, it will select it by calling <code>WReader.selectedItemController.select(content);</code>, but because we don't have anything in our markup to display it yet, we won't see anything.</p>
<h3>Step 4: Adding the markup to display what's in selectedItemController</h3>
<p>Let's get the rendering for the EntryItemView on to our page. Like other views, it's content lives between <code>&lt;script type="text/x-handlebars"&gt;</code>, but this time, we're going to do something a little different. Ember's template language allows us to use <code>if</code> statements to see if a controller has content or not If it does, it uses one view, and if it doesn't it might use a different view, in our case, we're just going to use some simple markup if there's nothing there. For example, <code>{{#if WReader.selectedItemController.selectedItem}}</code> checks if there is an item selected in the <code>selectedItemController</code>.</p>
<p>For now, let's just concentrate on getting the content on the page, we'll make it look pretty in a minute.</p>
<h4>Exercise 7.4 (index.html)</h4>
<pre><code>&lt;script type="text/x-handlebars"&gt;
{{#if WReader.selectedItemController.selectedItem}}
{{#view WReader.EntryItemView}}
&lt;p&gt;{{formattedDate}}&lt;/p&gt;
&lt;p&gt;
&lt;i {{bindAttr class="readClass"}}&gt;&lt;/i&gt; &lt;i {{bindAttr class="starClass"}}&gt;&lt;/i&gt;
&lt;a target="_blank" {{bindAttr href="content.item_link"}}&gt;&lt;i class="icon-share"&gt;&lt;/i&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;h2&gt;{{content.title}}&lt;/h2&gt;
&lt;p&gt;by {{content.pub_author}} of {{content.pub_name}}&lt;/p&gt;
&lt;p&gt;{{{content.content}}}&lt;/p&gt;
{{/view}}
{{else}}
&lt;div class="nothingSelected"&gt;
&lt;img src="img/sadpanda.png" alt="Sad Panda"&gt;
&lt;p&gt;Nothing selected.&lt;/p&gt;
&lt;/div&gt;
{{/if}}
&lt;/script&gt;
</code></pre>
<p>You'll notice that we've got the <code>{{#if}}</code> statement, the <code>{{else}}</code> and finally the `{{/if}}, so if there isn't an item selected, we can show the sad panda. If we try to run the app now, when you click on an item in the summary list on the left, it should appear on the right with all of the information.</p>
<h3>Step 5: Add an active property to SummaryListView</h3>
<p>It would be nice if we could indicate in the <code>summaryListView</code> on the left, which item is currently selected, so that users have a better idea of what they're looking at. To do that, we'll add an additional class to the <code>classNameBindings</code> list, and an additional property to <code>summaryListView</code> to indicate if an item is selected in the <code>selectedItemController</code> or not.</p>
<h4>Exercise 7.5a (js/app.js)</h4>
<pre><code>classNameBindings: ['read', 'starred', 'active'],
</code></pre>
<h4>Exercise 7.5b (js/app.js)</h4>
<pre><code>// Enables/Disables the active CSS class
active: function() {
var selectedItem = WReader.selectedItemController.get('selectedItem');
var content = this.get('content');
if (content === selectedItem) {
return true;
}
}.property('WReader.selectedItemController.selectedItem')
</code></pre>
<p>Finally, we can add some additional styling for items that are read, unread, starred, or selected. We've provided styles for the read and active, and will leave it open to your creativity to add the starred style.</p>
<h4>Exercise 7.5c (style.css)</h4>
<pre><code>.summary.read {
opacity: 0.6;
}
.summary.active {
background-color: #e4e4e4;
border: 1px solid rgba(0, 0, 0, 0.05);
opacity: 1.0;
}
</code></pre>
<h3>Step 6: Add the left hand controls</h3>
<p>We're going to create another view that will provide some functionality down the left hand side to make it easy to move between articles, mark them read/unread, add stars, etc. Like before, we need to start by creating a new view (<code>WReader.NavControlsView</code>) that extends <code>Em.View</code>. We've already added handlers for most of the buttons (<code>navUp</code>, <code>navDown</code>, etc)</p>
<p>Let's first add the view to our HTML. We're going to use the flex box model in the next step, so for now, we're just getting things set up. We're going to need five buttons, <code>markAllRead</code>, <code>navUp</code>, <code>navDown</code>, <code>toggleStar</code>, <code>toggleRead</code> and <code>refresh</code>.</p>
<h4>Exercise 7.6a (index.html)</h4>
<pre><code>&lt;script type="text/x-handlebars"&gt;
{{#view WReader.NavControlsView}}
&lt;div class="tControls"&gt;
&lt;div class="top"&gt;
&lt;button {{action "markAllRead"}} class='btn'&gt;&lt;i class="icon-ok"&gt;&lt;/i&gt;&lt;/button&gt;
&lt;/div&gt;
&lt;div class="middle"&gt;
&lt;button {{action "navUp"}} class='btn'&gt;&lt;i class="icon-arrow-up"&gt;&lt;/i&gt;&lt;/button&gt;
&lt;button {{action "toggleStar"}} {{bindAttr disabled="buttonDisabled"}} class='btn'&gt;
&lt;i {{bindAttr class="starClass"}}&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;button {{action "toggleRead"}} {{bindAttr disabled="buttonDisabled"}} class='btn'&gt;
&lt;i {{bindAttr class="readClass"}}&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;button {{action "navDown"}} class='btn'&gt;&lt;i class="icon-arrow-down"&gt;&lt;/i&gt;&lt;/button&gt;
&lt;/div&gt;
&lt;div class="bottom"&gt;
&lt;button {{action "refresh"}} class='btn'&gt;&lt;i class="icon-refresh"&gt;&lt;/i&gt;&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
{{/view}}
&lt;/script&gt;
</code></pre>
<p>Notice in <code>toggleStar</code> button, we've got <code>{{bindAttr disabled="buttonDisabled"}}</code>, this binds the <code>buttons</code>'s disabled property to the <code>buttonDisabled</code> property in <code>NavControlsView</code>. This has the effect of enabling or disabling the button depending on if something is selected or not.</p>
<h4>Exercise 7.6b (js/app.js)</h4>
<pre><code>starClass: function() {
var selectedItem = WReader.selectedItemController.get('selectedItem');
if (selectedItem) {
if (selectedItem.get('starred')) {
return 'icon-star';
}
}
return 'icon-star-empty';
}.property('WReader.selectedItemController.selectedItem.starred'),
readClass: function() {
var selectedItem = WReader.selectedItemController.get('selectedItem');
if (selectedItem) {
if (selectedItem.get('read')) {
return 'icon-ok-sign';
}
}
return 'icon-ok-circle';
}.property('WReader.selectedItemController.selectedItem.read'),
nextDisabled: function() {
return !WReader.selectedItemController.get('hasNext');
}.property('WReader.selectedItemController.selectedItem.next'),
prevDisabled: function() {
return !WReader.selectedItemController.get('hasPrev');
}.property('WReader.selectedItemController.selectedItem.prev'),
buttonDisabled: function() {
var selectedItem = WReader.selectedItemController.get('selectedItem');
if (selectedItem) {
return false;
}
return true;
}.property('WReader.selectedItemController.selectedItem')
</code></pre>
<h3>Step 7: Style the controls on the left side</h3>
<p>We'll use the flexbox layout again, but this time, instead of laying things out horizontally, we'll do it vertically. Like before, we set the display of our parent element (<code>.tControls</code>) to <code>display: -webkit-box;</code>, but this time, we set the orientation to <code>-webkit-box-orient: vertical;</code> Next, we need to set the heights on the elements and tell them how to grow.</p>
<p>We also want the buttons to have a particular size, so let's set that as well to ensure that they fit properly and look nice.</p>
<h4>Exercise 7.7a</h4>
<pre><code>.tControls {
display: -webkit-box;
-webkit-box-orient: vertical;
}
.controls div.top {
min-height: 50px;
display: -webkit-box;
-webkit-box-align: start;
-webkit-box-flex: 1;
}
.controls div.middle {
min-height: 185px;
display: -webkit-box;
-webkit-box-align: center;
-webkit-box-flex: 2;
}
.controls div.bottom {
min-height: 50px;
-webkit-box-flex: 1;
display: -webkit-box;
-webkit-box-align: end;
}
.controls .btn {
width: 40px;
height: 40px;
margin-top: 5px;
}
</code></pre>
<h3>Step 8: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 7 in Chrome (eg <code>http://localhost/wreader/exercise7/index.html</code>)</li>
<li>Click on each of the different counts to see the items list change</li>
</ol>
<hr>
<h2>Exercise 8 - Add UI Enhancements</h2>
<p>Right now our content is just being rendered on screen in a very boring way, in this exercise you'll update the content to look a little more styled and eye catching.</p>
<h3>Step 1: Making the SummaryListView look more exciting</h3>
<p>We'll use Bootstrap's grid layout to get our content looking exactly like we want. For the summary list, we want two rows, one that will contain <code>pub_name</code> and one for <code>pub_date</code>, on the second row, we'll list the item <code>title</code>.</p>
<h4>Exercise 8.1a (index.html)</h4>
<pre><code>{{#each WReader.itemsController}}
{{#view WReader.SummaryListView contentBinding="this"}}
&lt;div class="row-fluid"&gt;
&lt;div class="span6 pub-name"&gt;
{{content.pub_name}}
&lt;/div&gt;
&lt;div class="span6 pub-date"&gt;
{{formattedDate}}
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 class="pub-title"&gt;{{content.title}}&lt;/h3&gt;
{{/view}}
{{/each}}
</code></pre>
<p>A little extra font styling would be nice, but certainly isn't required.</p>
<h4>Exercise 8.1b (style.css)</h4>
<pre><code>.summary .pub-name {
font-size: 0.9em;
}
.summary .pub-date {
font-size: 0.9em;
text-align: right;
}
</code></pre>
<h3>Step 2: Making the EntryItemView look better</h3>
<p>The EntryItemView could look so much better, and we want to add a set of controls to allow us easily change the state of the item. We've already implemented the handlers for these events, so you don't need to worry about that for now.</p>
<h4>Exercise 8.2a (index.html)</h4>
<pre><code>{{#view WReader.EntryItemView}}
&lt;div class="row-fluid"&gt;
&lt;div class="span8"&gt;{{formattedDate}}&lt;/div&gt;
&lt;div class="span4 actions"&gt;
&lt;button type="button" {{bindAttr class="readButtonClass"}} {{action "toggleRead"}}&gt;
&lt;i {{bindAttr class="readClass"}} class="icon-white"&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;button type="button" {{bindAttr class="starButtonClass"}} {{action "toggleStar"}}&gt;
&lt;i {{bindAttr class="starClass"}}&gt;&lt;/i&gt;
&lt;/button&gt;
&lt;a target="_blank" {{bindAttr href="content.item_link"}} class="btn"&gt;&lt;i class="icon-share"&gt;&lt;/i&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2&gt;{{content.title}}&lt;/h2&gt;
&lt;span class="author"&gt;{{content.pub_author}}&lt;/span&gt; - &lt;span class="pub-name"&gt;{{content.pub_name}}&lt;/span&gt;
&lt;hr /&gt;
&lt;p class="post-content"&gt;{{{content.content}}}&lt;/p&gt;
{{/view}}
</code></pre>
<h4>Exercise 8.2b (style.css)</h4>
<pre><code>.entry {
box-shadow: 5px 5px 5px rgba(210, 210, 210, 0.6);
}
.entry.active {
border: 0px solid #222;
}
.entry .pub-date {
font-size: 0.9em;
}
.entry .actions {
text-align: right;
}
.entry .post-content img {
margin: 5px;
}
.nothingSelected {
text-align: center;
}
</code></pre>
<h3>Step 3: Add a toggleRead and toggleStar to the EntryItemView</h3>
<p>Let's go ahead and add two handlers to toggle the read and star states of the selected item.</p>
<h4>Exercise 8.3 (js/app.js)</h4>
<pre><code>toggleRead: function() {
WReader.selectedItemController.toggleRead();
},
toggleStar: function() {
WReader.selectedItemController.toggleStar();
}
</code></pre>
<h3>Step 4: Add more UI and UX embelishments</h3>
<p>Take a bit of time to play with any additional UI embelishments that you may want to add. Be sure to check out <a href="http://twitter.github.com/bootstrap">Bootstrap's</a> documentation to see what all is available to you!</p>
<h3>Step 5: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 8 in Chrome (eg <code>http://localhost/wreader/exercise8/index.html</code>)</li>
<li>Click on each of the different counts to see the items list change</li>
</ol>
<hr>
<h2>Exercise 9 - Adding LawnChair for persistent data storage</h2>
<p>In exercise 9, we're going to use <a href="http://westcoastlogic.com/lawnchair/">LawnChair</a> as a simple data storage library. LawnChair is good, but has some pretty significant issues that would prevent it from being used in production code, but they're fixable with a little effort.</p>
<h3>Step 1: Create a LawnChair instance to store our data</h3>
<p>The first thing we need to do is create our LawnChair instance so that we've got quick and easy access to it.</p>
<h4>Exercise 9.1 (js/app.js)</h4>
<pre><code>// Create or open the data store where objects are stored for offline use
var store = new Lawnchair({name: 'entries', record: 'entry'}, function() {});
</code></pre>
<h3>Step 2: Create a new method to pull any existing data from the local data store</h3>
<p>We need to create a method (<code>WReader.GetItemsFromDataStore</code>) to pull out any existing data from the data store before we go up to the server and ask it if it has anything new. That method makes a request to LawnChair to get all of the items, the does a <code>forEach</code> over the resulting array and adds those items to the dataController.</p>
<p>We're using <code>store.all</code> instead of <code>store.each</code> because LawnChair returns ther results asyncronously, which might lead to items being added from the server and the local store at the same time, something we don't want. Once we've finished adding all of the items from the local data store, we'll ask the server if it has any new data by calling <code>WReader.GetItemsFromServer();</code></p>
<h4>Exercise 9.2 (js/app.js)</h4>
<pre><code>// Get all items from the local data store.
var items = store.all(function(arr) {
arr.forEach( function(entry) {
var item = WReader.Item.create(entry);
WReader.dataController.addItem(item);
});
console.log("Entries loaded from local data store:", arr.length);
// Set the default view to any unread items
WReader.itemsController.showDefault();
// Load items from the server after we've loaded everything from
// the local data store
WReader.GetItemsFromServer();
});
</code></pre>
<h3>Step 3: Update the start up flow to load local data first, then the server</h3>
<p>We want to remove the call to <code>WReader.GetItemsFromServer();</code> and replace it with <code>WReader.GetItemsFromDataStore();</code>, which will call <code>WReader.GetItemsFromServer();</code> when it's done.</p>
<h4>Exercise 9.3 (js/app.js)</h4>
<pre><code>WReader.GetItemsFromDataStore();
</code></pre>
<h3>Step 4: Update dataController.addItem to prevent duplicates and store new items locally</h3>
<p>We don't want the local data store to contain duplicate items, so we need to be careful about adding them. Our <code>dataController.addItem(item)</code> function returns true if the item is unique (and not currently saved locally) or false if it's already in the <code>dataController</code>. That means we can use the result of <code>addItem</code> to determine if we should save the new item in the <code>store</code>.</p>
<h4>Exercise 9.4 (js/app.js)</h4>
<pre><code>if (WReader.dataController.addItem(emItem)) {
store.save(item);
}
</code></pre>
<h3>Step 5: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 9 in Chrome (eg <code>http://localhost/wreader/exercise9/index.html</code>)</li>
<li>Comment out the <code>WReader.GetItemsFromServer();</code> and see if data is loaded from the local store</li>
</ol>
<hr>
<h2>Exercise 10 - Adding AppCache to enable offline experiences</h2>
<h3>Step 1: Create the cache manifest</h3>
<h4>Exercise 10.1 (wreader.appcache)</h4>
<pre><code>CACHE MANIFEST
# version 0.0.10
CACHE:
index.html
js/libs/jquery-1.7.1.min.js
js/dev-helper.js
js/libs/bootstrap.js
js/libs/lawnchair-0.6.1.js
js/libs/lawnchair-adapter-indexed-db-0.6.1.js
js/libs/moment-1.5.0.js
js/libs/ember-0.9.5.js
js/plugins.js
js/app.js
css/bootstrap.css
css/style.css
NETWORK:
*
</code></pre>
<h3>Step 2: Add the manifest to index.html</h3>
<h4>Exercise 10.2 (index.html)</h4>
<pre><code>&lt;html class="no-js" lang="en" manifest="wreader.appcache"&gt;
</code></pre>
<h3>Step 3: Add event handlers (optional)</h3>
<h4>Exercise 10.3a (js/app.js)</h4>
<pre><code>window.applicationCache.addEventListener('updateready', function(e) {
if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
$("#modalUpdate").modal({"show":true});
}
}, false);
WReader.swapCache = function(value) {
if (value === true) {
window.applicationCache.swapCache();
window.location.reload();
} else {
$("#modalUpdate").modal('hide');
}
};
</code></pre>
<h4>Exercise 10.3b (index.html)</h4>
<pre><code>&lt;div class="modal fade" id="modalUpdate"&gt;
&lt;div class="modal-header"&gt;
&lt;a class="close" data-dismiss="modal"&gt;×&lt;/a&gt;
&lt;h3&gt;Update Application&lt;/h3&gt;
&lt;/div&gt;
&lt;div class="modal-body"&gt;
&lt;p&gt;A new version of WReader is available, would you like to upgrade now?&lt;/p&gt;
&lt;/div&gt;
&lt;div class="modal-footer"&gt;
&lt;button type="button" class="btn btn-primary" onclick="WReader.swapCache(true);"&gt;Yes&lt;/button&gt;
&lt;button type="button" class="btn" onclick="WReader.swapCache(false);"&gt;Later&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h3>Step 5: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 10 in Chrome (eg <code>http://localhost/wreader/exercise10/index.html</code>)</li>
<li>Open the dev tools and switch to the console window</li>
<li>Verify everything is cached as expected</li>
<li>Disable your network and try to reload the page</li>
</ol>
<h3>Step 6: Disable AppCache for further development</h3>
<ol>
<li>Re-enable the network if you haven't already</li>
<li>Rename <code>wreader.appcache</code> to <code>_disabled_wreader.appcache</code></li>
<li>Refresh WReader in the browser</li>
<li>Verify that caching fails and items are loaded as expected.</li>
</ol>
<hr>
<h2>Exercise 11 - Adding keyboard and touch events</h2>
<h3>Step 1: Add keyboard event handler</h3>
<h4>Exercise 11.1a (js/app.js)</h4>
<pre><code>function handleBodyKeyDown(evt) {
if (evt.srcElement.tagName === "BODY") {
switch (evt.keyCode) {
case 34: // PgDn
case 39: // right arrow
case 40: // down arrow
case 74: // j
WReader.selectedItemController.next();
break;
case 32: // Space
WReader.HandleSpaceKey();
evt.preventDefault();
break;
case 33: // PgUp
case 37: // left arrow
case 38: // up arrow
case 75: // k
WReader.selectedItemController.prev();
break;
case 85: // U
WReader.selectedItemController.toggleRead();
break;
case 72: // H
WReader.selectedItemController.toggleStar();
break;
}
}
}
</code></pre>
<h4>Exercise 11.1b (js/app.js)</h4>
<pre><code>WReader.HandleSpaceKey = function() {
var itemHeight = $('.entry.active').height() + 60;
var winHeight = $(window).height();
var curScroll = $('.entries').scrollTop();
var scroll = curScroll + winHeight;
if (scroll &lt; itemHeight) {
$('.entries').scrollTop(scroll);
} else {
WReader.selectedItemController.next();
}
};
</code></pre>
<h4>Exercise 11.1c (js/app.js)</h4>
<pre><code>document.addEventListener('keydown', handleBodyKeyDown, false);
</code></pre>
<h3>Step 2: Let's try it out!</h3>
<p>Let's try it out!</p>
<ol>
<li>Open exercise 11 in Chrome (eg <code>http://localhost/wreader/exercise11/index.html</code>)</li>
<li>Try pressing j, k, u or h</li>
</ol>
<h3>Step 3: Add touch events to the SummaryListView</h3>
<p>Since we want the same experience if a user touches an item in the summary view as if they clicked on it, we'll just have the <code>touchEnd</code> event pass directly to the <code>click</code> event handler.</p>
<h4>Exercise 11.3 (js/app.js)</h4>
<pre><code>touchEnd: function(evt) {
this.click(evt);
}
</code></pre>
<h2>Exercise 12 - Performance Tips &amp; Techniques</h2>
<p>Be sure to check out <a href="http://developer.yahoo.com/performance/rules.html">Best Practices for Speeding Up Your Web Site</a> from the good folks at <a href="http://developer.yahoo.com/">Yahoo</a>, who have done a lot of great work around this.</p>
<h3>Step 1: Use the audit tool in Chrome</h3>
<ol>
<li>Open exercise 12 in Chrome (eg <code>http://localhost/wreader/exercise12/index.html</code>)</li>
<li>Open the Chrome Developer Tools, and switch to the <code>Audit</code> tab</li>
<li>Press the Audit button</li>
<li>Review the Network Utilization tab and make any applicable updates</li>
<li>Review the Web Page Performance tab and make any applicable updates</li>
</ol>
<h3>Step 2: Scripts at the bottom, CSS at the top</h3>
<p>We've already done this in our boiler plate, so there's nothing to change here! Yay!</p>
<h3>Step 3: Reduce the number of HTTP requests</h3>
<ol>
<li>Combine your CSS files in the a single CSS file and replace with the new combined file</li>
<li>Combine any JavaScript files that you can</li>
</ol>
<h4>Exercise 12.3a - Concatenate files</h4>
<pre><code>cat css/bootstrap.css css/style.css &gt; css/wreader.css
cat js/libs/bootstrap.js js/libs/lawnchair-0.6.1.js js/libs/moment-1.5.0.js js/plugins.js &gt; js/libs/libraries.js
</code></pre>
<h4>Exercise 12.3b (index.html)</h4>
<pre><code>&lt;link rel="stylesheet" href="css/wreader.css"&gt;
</code></pre>
<h4>Exercise 12.3c (index.html)</h4>
<pre><code>&lt;script src="js/libraries.js"&gt;&lt;/script&gt;
&lt;script src="js/libs/ember-0.9.5.js"&gt;&lt;/script&gt;
&lt;script src="js/app.js"&gt;&lt;/script&gt;
</code></pre>
<h3>Step 4: Minify the code to reduce file sizes</h3>
<h4>Exercise 12.4a</h4>
<pre><code>java -jar ../tools/yuicompressor-2.4.7.jar css/wreader.css -o css/wreader.min.css
java -jar ../tools/yuicompressor-2.4.7.jar js/libraries.js -o js/libraries.min.js
java -jar ../tools/yuicompressor-2.4.7.jar js/app.js -o js/app.min.js
</code></pre>
<p><em>Note</em>: For these couple of files, our request size decreased from <em>170k</em> to <em>62k</em></p>
<h4>Exercise 12.4b</h4>
<ol>
<li>Update <code>wreader.css</code> to <code>wreader.min.css</code>. (Dropped by <em>14k</em>)</li>
<li>Update <code>libraries.js</code> to <code>libraries.min.js</code> (Dropped by <em>48k</em>)</li>
<li>Update <code>ember-0.9.5.js</code> to <code>ember-0.9.5.min.js</code> (Dropped by <em>332k</em>)</li>
<li>Update <code>app.js</code> to <code>app.min.js</code> (Dropped by <em>10k</em>)</li>
<li>Update and enable <code>wreader.appcache</code> for the new minified files</li>
</ol>
</div><!--/span-->
</div><!--/row-->
<hr>
<footer>
</footer>
</div><!--/.fluid-container-->
<!-- Le javascript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="finalproject/js/libs/jquery-1.7.1.min.js"></script>
<script src="finalproject/js/libs/bootstrap.js"></script>
<script type="text/javascript" src="docs/prettify.js"></script>
<script type="text/javascript">
$("pre").addClass("prettyprint");
prettyPrint();
</script>
<script>
var _gaq=[['_setAccount','UA-29459980-1'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</body>
</html>
Jump to Line
Something went wrong with that request. Please try again.