Raymond Camden's Blog Rss

My first AngularJS application

12

Posted in Mobile, JavaScript, HTML5 | Posted on 11-29-2011 | 4,655 views

Earlier this year I blogged about the AngularJS framework. I thought the docs and the tutorial were cool and overall it felt like a real cool way to build a JavaScript "Application" - and yes, with the quotes there I'm talking about something more than a simple one page, Ajaxified file. Unfortunately, I never got the time to play more with AngularJS until this week. Yesterday I reviewed the tutorial again and decided to take a stab at building my own application. I ran into a few hiccups, but in general, the process was straight forward, and after actually building something with the framework, I love it, and can't wait to try more with it. Let's take a look at my application. (And of course, please keep in mind that this is my first AngularJS application. It's probably done completely wrong. Keep that in mind....)

For my first application, I decided to build a simple Note manager. This is something I've done many times before. I'm a huge fan of Evernote and I keep hundreds of notes stored in it. My application will allow for adding, editing, deleting, and viewing of simple notes. In this first screen shot you can see the basic UI.

You can click a note to see the details...

And of course basic editing is supported...

So what does the code look like? First, I've got my core index page:

view plain print about
1<!DOCTYPE html>
2<html xmlns:ng="http://angularjs.org/">
3<head>
4    <meta charset="utf-8">
5    <title>Note Application</title>
6    <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
7    <link rel="stylesheet" href="css/main.css"/>
8</head>
9
10<body ng:controller="NoteCtrl">
11
12    <div class="container">
13
14        <ng:view></ng:view>
15
16    </div>
17    
18    <script src="lib/angular/angular.js" ng:autobind></script>
19    <script src="js/controllers.js"></script>
20    <script src="js/services.js"></script>
21</body>
22</html>

All of the real display will happen within the view, which is managed in my controller. Let's look at that file next.

view plain print about
1/* App Controllers */
2
3function NoteCtrl($route) {
4 var self = this;
5
6 $route.when('/notes',
7 {template: 'partials/note-list.html', controller: NoteListCtrl});
8 $route.when('/notes/add',
9 {template: 'partials/note-edit.html', controller: NoteEditCtrl});
10 $route.when('/notes/edit/:noteId',
11 {template: 'partials/note-edit.html', controller: NoteEditCtrl});
12 $route.when('/notes/:noteId',
13 {template: 'partials/note-detail.html', controller: NoteDetailCtrl});
14 $route.otherwise({redirectTo: '/notes'});
15
16 $route.onChange(function(){
17 self.params = $route.current.params;
18 });
19
20 $route.parent(this);
21}
22
23function NoteListCtrl(Note_) {
24    var self = this;
25    self.orderProp = 'title';
26    self.notes = Note_.query();
27
28    self.delete = function(id) {
29        console.log("delete "+id);
30        Note_.delete(id);
31        self.notes = Note_.query();
32        //refreshes the view
33        self.$root.$eval();    
34    }
35    
36    self.edit = function(id) {
37        window.location = "./index.html#/notes/edit/"+id;
38    }
39}
40
41function NoteDetailCtrl(Note_) {
42 var self = this;
43 self.note = Note_.get(self.params.noteId);
44
45 if(typeof self.note === "undefined") window.location = "./index.html";
46}
47
48function NoteEditCtrl(Note_) {
49    console.log('EDIT CTRL');
50    var self = this;
51        
52    if(self.params.hasOwnProperty("noteId")) self.note = Note_.get(self.params.noteId);
53    else self.note = { title:"", body:""};
54
55    self.cancel = function() {
56        window.location = "./index.html";
57    }
58
59    self.save = function() {
60        Note_.store(self.note);
61        window.location = "./index.html";
62    }
63
64}

Essentially, this file consists of the main controller which simply handles routing based on the URL. The other three controls handle listing, editing, and the detail view. Now let's look at the note service.

view plain print about
1/* http://docs.angularjs.org/#!angular.service */
2
3angular.service('Note', function(){
4    return {
5        query:function() {
6            var notes = [];
7            for(var key in localStorage) {
8                if(key.indexOf("note_") == 0) {
9                    notes.push(JSON.parse(localStorage[key]));
10                }
11            }
12            console.dir(notes);
13            return notes;
14        },
15        delete:function(i) {
16            localStorage.removeItem("note_"+i);
17        },
18        get:function(i) {
19            if(localStorage["note_"+i]) return JSON.parse(localStorage["note_"+i]);
20            console.log("no note for "+i);
21        },
22        store:function(note) {
23            if(!note.hasOwnProperty('id')) {
24                //yep, hack, get all notes and find highest id
25                var notes = this.query();
26                var highest = 0;
27                for(var i=0; i<notes.length; i++) {
28                    if(notes[i].id >
highest) highest=notes[i].id;
29                }
30                note.id = ++highest;
31            }
32            note.updated = new Date();
33            localStorage["note_"+note.id] = JSON.stringify(note);
34        }
35    }
36    
37});

As you can see, my note service is simply a collection of methods. My application makes use of HTML5's Local Storage feature. It looks for any key that begins with note_ and considers that application data. It stores notes as JSON strings and the service handles both deserializing and serializing the information. What's cool is that I could - in theory - replace this with a SQL version and nothing else would change.

All that's really left are the views. Here is the first view - and keep in mind - this is the entire file for the display of notes on the first screen.

view plain print about
1Search: <input type="text" name="query"/>
2 Sort by:
3 <select name="orderProp">
4 <option value="title">Title</option>
5 <option value="-updated">Newest</option>
6 </select>
7
8
9<h2>Notes</h2>
10
11<table class="bordered-table">
12    <tr>
13        <th>Title</th>
14        <th>Updated</th>
15        <th>&nbsp;</th>
16    </tr>
17    <tr ng:repeat="note in notes.$filter(query).$orderBy(orderProp)">
18        <td><a href="#/notes/{{note.id}}">{{note.title}}</a></td>
19        <td>{{note.updated | date:'MM/dd/yyyy @ h:mma'}}</td>
20        <td><a href="" ng:click="edit(note.id)" title="Edit"><img src="images/page_edit.png"></a> <a href="" ng:click="delete(note.id)" title="Delete"><img src="images/page_delete.png"></a></td>
21    </tr>
22</table>
23
24<p ng:show="notes.length==0">
25    Sorry, there are no notes yet.
26</p>
27
28<a href="#/notes/add" class="btn primary">Add Note</a>

Take note of the ng:repeat and the embedded tokens wrapped in {}. Also note how easy it is to format the date. The detail view is incredibly boring.

view plain print about
1<h1>{{note.title}}</h1>
2<p>
3<b>Last Updated:</b> {{note.updated | date:'MM/dd/yyyy @ h:mma'}}
4</p>
5
6{{note.body}}

And the edit one is also pretty trivial:

view plain print about
1<h1>Edit Note</h1>
2
3<form class="form-stacked" onSubmit="return false" id="editForm">
4    <div class="clearfix">
5        <label for="title">Title:</label>
6        <div class="input">
7            <input type="text" id="title" name="note.title" class="xlarge">
8        </div>
9    </div>
10
11    <div class="clearfix">
12        <label for="body">Body</label>
13        <div class="input">
14            <textarea class="xxlarge" id="body" name="note.body" rows="3"></textarea>
15        </div>
16    </div>
17
18    <div>
19        <button ng:click="save()" class="btn primary">Save</button> <button ng:click="cancel()" class="btn">Cancel</button>
20    </div>
21
22</form>

Pretty simple, right? For the heck of it, I ported this over into a PhoneGap application, and it worked well. The UI is a bit small, but I was able to add, edit, delete notes just fine on my Xoom table. I've included a zip file of the core code for the application and the APK as well if you want to try it. You may demo the app yourself below, and note, it makes use of console for debugging, so don't run it unless you've got a proper browser.

Download attached file

Comments

[Add Comment] [Subscribe to Comments]

Awesome!

(Also you probably shouldn't use delete as a key for an object, or as a method name. delete is a keyword in JS and some browsers won't allow you to use it like that).
Good point Elliott. I didn't notice that until I put it into Android. Eclipse complained about it. I just deleted the errors though and it ran on my device.
I wonder if AngularJS has been used by any major company. There are so many javascript mvc frameworks, I'm confused which one to use. The two main ones that I hear a lot are backbone.js and JavaScript MVC.
Hopefully Elliott will chime in here. I'm not sure who out there is using it.
I convinced my company to go with Angularjs for a new project and so far so good. There is a bit of a learning curve but I think it is worth it. Anyway great tut!

For those interested I converted a standard JS application (Kevin's HTML5 Quiz) to Angularjs in a couple of hours. Link: http://www.webappstogo.com/html5quiz
Nice - and yes - I viewed source so I could cheat.
Thanks! I forgot to add I have the source on github -> http://bit.ly/rCVLXO
I've been using angular on a few projects and really like it. Apparently angular is used quite a lot internally at google.

Some pointers:

- Use ng:href in your templates for links that have bindings

- Rather than using a function to change browser location you could just use an href with the path, for example to edit: ng:href="#/notes/edit/{{note.id}}";

- If you do want to set location in your controller use: $location.path('/notes/edit' + self.noteId);

- you can access the $routeParams in your controller and use that to load a note using something like this
self.noteid = $routeParams.noteId as alternative to self.params.hasOwnProperty("noteId")
Thanks for the tips Johan. I've been trying to find time to build another Angular app - I just haven't had the time.
No worries Ray - given the diversity and amount of useful stuff you explore/publish I'll cut you a bit of slack on getting to your next angular post ;-)
@Dmitry - yes there seems to be a new JavaScript MVC framework each day. I tried a few and found the TODO MVC resource from Addy Osmani useful for comparision:
https://github.com/addyosmani/todomvc/tree/master/...

Angular was the best fit for me. The community is still small compared to backbone but core developers (they work for google) are very helpful and answer questions on the forums promptly.

For angular I'd recommend using latest release (unstable - 10.x) available here:
https://github.com/angular/angular.js/tags

Make sure to consult the latest docs here:
http://docs-next.angularjs.org/api
"Angular was the best fit for me." - And isn't that the most important thing? I know for me in ColdFusion, there were multiple frameworks I tried until I found the one that made me effective.

[Add Comment] [Subscribe to Comments]