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:
<!DOCTYPE html>
<html xmlns:ng="http://angularjs.org/">
<head>
<meta charset="utf-8">
<title>Note Application</title>
<link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
<link rel="stylesheet" href="css/main.css"/>
</head> <body ng:controller="NoteCtrl"> <div class="container"> <ng:view></ng:view> </div> <script src="lib/angular/angular.js" ng:autobind></script>
<script src="js/controllers.js"></script>
<script src="js/services.js"></script>
</body>
</html>
All of the real display will happen within the view, which is managed in my controller. Let's look at that file next.
function NoteCtrl($route) {
var self = this; $route.when('/notes',
{template: 'partials/note-list.html', controller: NoteListCtrl});
$route.when('/notes/add',
{template: 'partials/note-edit.html', controller: NoteEditCtrl});
$route.when('/notes/edit/:noteId',
{template: 'partials/note-edit.html', controller: NoteEditCtrl});
$route.when('/notes/:noteId',
{template: 'partials/note-detail.html', controller: NoteDetailCtrl});
$route.otherwise({redirectTo: '/notes'}); $route.onChange(function(){
self.params = $route.current.params;
}); $route.parent(this);
} function NoteListCtrl(Note_) {
var self = this;
self.orderProp = 'title';
self.notes = Note_.query(); self.delete = function(id) {
console.log("delete "+id);
Note_.delete(id);
self.notes = Note_.query();
//refreshes the view
self.$root.$eval();
} self.edit = function(id) {
window.location = "./index.html#/notes/edit/"+id;
}
} function NoteDetailCtrl(Note_) {
var self = this;
self.note = Note_.get(self.params.noteId); if(typeof self.note === "undefined") window.location = "./index.html";
} function NoteEditCtrl(Note_) {
console.log('EDIT CTRL');
var self = this; if(self.params.hasOwnProperty("noteId")) self.note = Note_.get(self.params.noteId);
else self.note = { title:"", body:""}; self.cancel = function() {
window.location = "./index.html";
} self.save = function() {
Note_.store(self.note);
window.location = "./index.html";
} }
/* App Controllers */
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.
/* http://docs.angularjs.org/#!angular.service */ angular.service('Note', function(){
return {
query:function() {
var notes = [];
for(var key in localStorage) {
if(key.indexOf("note_") == 0) {
notes.push(JSON.parse(localStorage[key]));
}
}
console.dir(notes);
return notes;
},
delete:function(i) {
localStorage.removeItem("note_"+i);
},
get:function(i) {
if(localStorage["note_"+i]) return JSON.parse(localStorage["note_"+i]);
console.log("no note for "+i);
},
store:function(note) {
if(!note.hasOwnProperty('id')) {
//yep, hack, get all notes and find highest id
var notes = this.query();
var highest = 0;
for(var i=0; i<notes.length; i++) {
if(notes[i].id > highest) highest=notes[i].id;
}
note.id = ++highest;
}
note.updated = new Date();
localStorage["note_"+note.id] = JSON.stringify(note);
}
} });
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.
Search: <input type="text" name="query"/>
Sort by:
<select name="orderProp">
<option value="title">Title</option>
<option value="-updated">Newest</option>
</select> <h2>Notes</h2> <table class="bordered-table">
<tr>
<th>Title</th>
<th>Updated</th>
<th> </th>
</tr>
<tr ng:repeat="note in notes.$filter(query).$orderBy(orderProp)">
<td><a href="#/notes/{{note.id}}">{{note.title}}</a></td>
<td>{{note.updated | date:'MM/dd/yyyy @ h:mma'}}</td>
<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>
</tr>
</table> <p ng:show="notes.length==0">
Sorry, there are no notes yet.
</p> <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.
{{note.body}}
<h1>{{note.title}}</h1>
<p>
<b>Last Updated:</b> {{note.updated | date:'MM/dd/yyyy @ h:mma'}}
</p>
And the edit one is also pretty trivial:
<h1>Edit Note</h1> <form class="form-stacked" onSubmit="return false" id="editForm">
<div class="clearfix">
<label for="title">Title:</label>
<div class="input">
<input type="text" id="title" name="note.title" class="xlarge">
</div>
</div> <div class="clearfix">
<label for="body">Body</label>
<div class="input">
<textarea class="xxlarge" id="body" name="note.body" rows="3"></textarea>
</div>
</div> <div>
<button ng:click="save()" class="btn primary">Save</button> <button ng:click="cancel()" class="btn">Cancel</button>
</div> </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.