-
Notifications
You must be signed in to change notification settings - Fork 1
09 A Better Sidebar
Now that we can create and list notes, let's show more useful information in the sidebar.
Inside the <nav>
element, let's display the body_text
returned in the JSON...which is there because the API server helpfully transformed the body_html
into a sanitized tag-free format, that won't mess up our layout when printed into the HTML of our document.
app/index.html
<nav id="sidebar" ng-show="notes.length > 0">
<div class="well">
<ul>
<li ng-repeat="note in notes" on-last-repeat>
{{ note.title }}
</li>
</ul>
</div>
</nav>
Notice we are using a few tricks here. The ng-show
directive actually has some javascript code, that will show the <ul>
if if evaluates to true. If there are no notes at all, the <ul>
will not be shown. The reverse also works: ng-hide
.
As you can see from notes.length > 0
, we can actually do some javascript in the directives, but this feels a little like it shouldn't be in our HTML templates. It is view-related, but it still feels like too much logic for a view, so I'm going to move it to the controller and see how it feels there.
app/notes/notes.js
$scope.hasNotes = function() {
return this.notes.length > 0;
};
app/views/index.html
<nav id="sidebar" ng-show="hasNotes()">
<div class="well">
<ul>
<li ng-repeat="note in notes" on-last-repeat>
{{ note.title }}
</li>
</ul>
</div>
</nav>
I do think that feels a little cleaner, so I'm going to leave it like this. While we're at it, let's get rid of that div.well
(as well as the one around our main ng-view
) and add an id to each li
, containing the ID of the record in Nevernote.
Let's also wrap the title and body in their own divs, for style's sake.
<nav id="sidebar">
<ul id="notes" ng-show="hasNotes()">
<li id="note_{{ note.id }}" ng-repeat="note in notes">
<div class="note-title">{{ note.title }}</div>
<div class="note-body">{{ note.body_text }}</div>
</li>
</ul>
</nav>
Angular has another useful directive that we can use to extract some HTML into a separate file, even if it has event handlers, scope variables, and directives. It's called ng-include
.
Create a new file named app/sidebar.html
, and extract <nav id="sidebar">
and its content into the new file.
app/sidebar.html
<nav id="sidebar" ng-show="hasNotes()">
<ul id="notes" ng-show="hasNotes()">
<li id="note_{{ note.id }}" ng-repeat="note in notes">
<div class="note-title">{{ note.title }}</div>
<div class="note-body">{{ note.body_text }}</div>
</li>
</ul>
</nav>
In its place in index.html
, add a <div>
with the ng-include
directive:
app/index.html
<div ng-include="'sidebar.html'"></div>
Notce that we actually added single quotation marks inside the double-quotes. Without them, AngularJS would look for a scope item named sidebar
with an html
property. That doesn't exist, so we return the file name as a string literal.
When we click on a note in the sidebar, we want to load that note back into the form.
Let's bind an event to run when you click on a note <li>
to a function to do just that.
<li ng-repeat="note in notes" ng-click="loadNote()">
$scope.loadNote = function() {
console.log('Clickety click');
};
Reload the page, and you should see Clickety click
in the console every time you click a note in the sidebar.
What we really want to do is find the note (in $scope.notes
). We might be able to find it if we can somehow access the id of the clicked note.
Luckily, we can pass a property of the note into loadNote()
, so let's make loadNote
accept such an argument.
app/index.html
<li ng-repeat="note in notes" ng-click="loadNote(note.id)">
app/notes/notes.js
$scope.loadNote = function(noteID) {
Now we can get the specific note JSON from the server with an API call, but since we already have it in $scope.notes
, let's look for it there.
Let's build a method to search our array of notes and return the note we want:
app/notes/notes.js
$scope.findNoteById = function(noteID) {
var notes = this.notes;
for (var i=0, len=notes.length; i < len; i += 1) {
if (notes[i].id === noteID) {
return notes[i];
}
}
};
We'll replace our console.log
in loadNote
with a call to findNoteById
, and use it to set the value of $scope.note
.
app/notes/notes.js
$scope.loadNote = function(noteID) {
$scope.note = this.findNoteById(noteID);
};
Niiiiiice. We can simplify findNote
a bit though.
We're going to use our trick for accessing the scope object again, so let's extract that into a separate function and call that new function from our existing refreshNotes
function.
this.sidebarScope = function() {
return angular.element(document.getElementById("sidebar")).scope();
};
this.refreshNotes = function(notes) {
self.sidebarScope().notes = notes;
};
We again have to use self
, because of the way refreshNote
is invoked.
Now for our new method of searching through the notes.
Angular comes with a convenience method called $filter, let's use that to search for the matching element. The syntax is a little weird, but it is shorter and more Angular-ish than our loop, so let's update findNote
to use it.
app/notes/notes.js
$scope.findNoteById = function(noteID) {
return $filter('filter')(self.sidebarScope().notes, { id: noteID }, true)[0];
};
You may not see this kind of function construct very much in the wild, but it is kind of interesting.
The first argument to $filter()
is a string containing the name of the filtering function that we want to invoke.
The second set of parens contains three arguments to that function: The array of things to search through, an object with the property or properties we want to match on (since we are searching objects), and true, which specifies that we want an exact match (otherwise we could put a comparison function here).
The syntax is possible because the $filter()
function returns another function, and not the result of a value or expression like we are used to seeing.
This is conceptually the same as this, where the function result is saved to a variable, then called:
var func = $filter('filter');
return func(self.sidebarScope().notes, { id: noteID }, true)[0];
Once again, we must use self
. Although findNoteById
isn't itself bound to an HTML element, it is being invoked from loadNote
, which is bound to the HTML.
If we refresh the page and click one of the notes, we should now see the note content loaded into the form. Success!
But we can still dramatically simplify it. Why not just pass the note object itself to loadNote
, rather than just the id?
app/sidebar.html
ng-click="loadNote(note)">
app/notes/notes.js
$scope.loadNote = function(note) {
$scope.note = note;
};
We don't really even need findNoteById
anymore, but it might be handy later, so let's keep it.
A slight issue remains. The note loaded into the form ($scope.note
) is not only identical to the note in the sidebar, it is actually the exact same JavaScript object. That means that the sidebar will be updated while we type changes in the form. We want the sidebar to reflect only changes that have actually been saved. Let's change loadNote
to load a copy of a note object, rather than actually pointing at the existing note object.
As silly as it sounds, the simplest way to create a copy of a JavaScript object containing simple key/value properties is to serialize the object as JSON, and then parse the JSON to turn it back into an object.
$scope.loadNote = function(note) {
$scope.note = JSON.parse(JSON.stringify(note));
};
That does the trick, but let's extract that object-copying behavior into its own function.
$scope.cloneNote = function(note) {
return JSON.parse(JSON.stringify(note));
};
$scope.loadNote = function(note) {
$scope.note = this.cloneNote(note);
};
Let's also sort our notes by id (descending) and limit the sidebar to show at most 10 notes.
app/sidebar.html
<nav id="sidebar">
<div class="well">
<h3 ng-show="hasNotes()">My Notes</h3>
<ul ng-show="hasNotes()">
<li id="note_{{ note.id }}"
ng-click="loadNote(note.id)"
ng-repeat="note in notes | orderBy: '-id' | limitTo: 10">
<div class="note-title">{{ note.title }}</div>
<div class="note-body">{{ note.body_text }}</div>
</li>
</ul>
</div>
</nav>
Go ahead and commit this as a work-in-progress.