This repository has been archived by the owner on Jul 30, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 88
/
index.html
1554 lines (1163 loc) · 62.1 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!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 & 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 & 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><script type="text/x-handlebars">
{{#each WReader.dataController}}
{{#view WReader.SummaryListView contentBinding="this"}}
{{content.title}} from {{content.pub_name}} on {{content.pub_date}}
{{/view}}
{{/each}}
</script>
</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 & 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><div>
{{WReader.dataController.itemCount}} items <br />
{{WReader.dataController.unreadCount}} are unread<br />
{{WReader.dataController.readCount}} are read<br />
{{WReader.dataController.starredCount}} are starred<br />
</div>
</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 & 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 & 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><header></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><header>
<script type="text/x-handlebars">
{{#view WReader.NavBarView}}
<ul>
<li><a>{{itemCount}} Items</a></li>
<li><a>{{unreadCount}} Unread</a></li>
<li><a>{{starredCount}} Starred</a></li>
<li><a>{{readCount}} Read</a></li>
</ul>
{{/view}}
</script>
</header>
</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><li><a {{action "showAll" on="click"}}>{{itemCount}} Items</a></li>
<li><a {{action "showUnread" on="click"}}>{{unreadCount}} Unread</a></li>
<li><a {{action "showStarred" on="click"}}>{{starredCount}} Starred</a></li>
<li><a {{action "showRead" on="click"}}>{{readCount}} Read</a></li>
</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 + "'&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 & 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><li><a {{action "refresh" on="click"}}>Reload</a></li>
</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><link rel="stylesheet" href="css/bootstrap.css">
</code></pre>
<h3>Step 2: Add styling & 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><div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand">...</a>
<ul class="nav">
<li>...</li>
</ul>
</div>
</div>
</div>
</code></pre>
<h4>Exercise 6.2 (index.html)</h4>
<pre><code><header>
<script type="text/x-handlebars">
{{#view WReader.NavBarView}}
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand">wReader</a>
<ul class="nav">
<li class="itemCount"><a {{action "showAll" on="click"}}>{{itemCount}} Items</a></li>
<li class="itemCount"><a {{action "showUnread" on="click"}}>{{unreadCount}} Unread</a></li>
<li class="itemCount"><a {{action "showStarred" on="click"}}>{{starredCount}} Starred</a></li>
<li class="itemCount"><a {{action "showRead" on="click"}}>{{readCount}} Read</a></li>
</ul>
<form class="navbar-search pull-left" id="navSearch">
<input type="text" class="search-query" placeholder="Search">
</form>
<ul class="nav pull-right">
<li><a {{action "refresh" on="click"}}><i class="icon-refresh icon-white"></i></a></li>
</ul>
</div>
</div>
</div>
{{/view}}
</script>
</header>
</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><section class="controls">controls</section>
</code></pre>
<h4>Exercise 6.3b (index.html)</h4>
<pre><code><section class="entries">entries</section>
</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 & 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 >= 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><script type="text/x-handlebars"></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><script type="text/x-handlebars">
{{#if WReader.selectedItemController.selectedItem}}
{{#view WReader.EntryItemView}}
<p>{{formattedDate}}</p>
<p>
<i {{bindAttr class="readClass"}}></i> <i {{bindAttr class="starClass"}}></i>
<a target="_blank" {{bindAttr href="content.item_link"}}><i class="icon-share"></i></a>
</p>
<h2>{{content.title}}</h2>
<p>by {{content.pub_author}} of {{content.pub_name}}</p>
<p>{{{content.content}}}</p>
{{/view}}
{{else}}
<div class="nothingSelected">
<img src="img/sadpanda.png" alt="Sad Panda">
<p>Nothing selected.</p>
</div>
{{/if}}
</script>
</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><script type="text/x-handlebars">
{{#view WReader.NavControlsView}}
<div class="tControls">
<div class="top">
<button {{action "markAllRead"}} class='btn'><i class="icon-ok"></i></button>
</div>
<div class="middle">
<button {{action "navUp"}} class='btn'><i class="icon-arrow-up"></i></button>
<button {{action "toggleStar"}} {{bindAttr disabled="buttonDisabled"}} class='btn'>
<i {{bindAttr class="starClass"}}></i>
</button>
<button {{action "toggleRead"}} {{bindAttr disabled="buttonDisabled"}} class='btn'>
<i {{bindAttr class="readClass"}}></i>
</button>
<button {{action "navDown"}} class='btn'><i class="icon-arrow-down"></i></button>
</div>
<div class="bottom">
<button {{action "refresh"}} class='btn'><i class="icon-refresh"></i></button>
</div>
</div>
{{/view}}