Skip to content
Dave Strus edited this page Aug 20, 2015 · 2 revisions

Cookies

We can log in, but the apiKey will be forgotten on every new page load, because we didn't store it anywhere but a temporary variable. Our variables won't persist across HTTP requests.

So we want to save the API key somewhere that we can access when a new page is loaded. We've used localStorage for that sort of thing, but something like an api key is more commonly stored in a cookie.

Cookies aren't much more than a text file in your internet cache that is associated with the website it came from. This is one of the primary ways most websites keep track of who is logged in from one page request to the next.

Let's save the apiKey to a cookie whenever we load it over the API, and look for it whenever our app boots up.

To do this, we'll use Angular's ngCookies $cookies.

This is an optional AngularJS module, and it wasn't included with the seed project we started with. So let's add it to bower.json.

bower.json

    "angular-cookies": "~1.4.0",

Run npm install and, and find the new module in bower_components to get the path for the <script> tag:

app/index.html

<script src="bower_components/angular-cookies/angular-cookies.min.js"></script>

Now, let's add the hooks to save it in the NotesBackend service.

First, we need to add the dependency to ngCookies in the module declaration:

app/app.js

var app = angular.module('notely', [
  'ngRoute',
  'ngCookies',

Then dependency inject it into the NotesBackend service:

app/app.js

app.service('NotesBackend', function NotesBackend($http, $cookies) {

Then save the API key to a cookie whenever someone logs in:

app/app.js

this.fetchApiKey = function(userData, callback) {
...
  }).success(function(sessionData) {
    ...
    $cookies.put('apiKey') = apiKey;

Then try to load from the cookie when the site is reloaded.

app/app.js

  var notes = [];
  var apiKey = $cookies.get('apiKey') || '';

Now if we login and refresh, our notes will get loaded again. This is a single-page app, so we're not interested in doing a hard refresh. Let's load the notes into the sidebar as soon as we login.

Let's call fetchNotes from within the success callback in fetchApiKey.

app/app.js

    }).success(function(userData){
      ...
      $cookies.put('user', userData)
      self.fetchNotes();

That will certainly fetch the notes, but it won't do anything with them afterward. Luckily, fetchNotes already takes a callback, so we can perform another action when it finishes. What do we want to happen when fetchNotes finishes? We want it to invoke the callback that was passed to fetchApiKey!

Yo, dawg. I heard you like callbacks, so I put a callback in your callback.

app/app.js

    }).success(function(userData){
      ...
      $cookies.put('user', userData)
      self.fetchNotes(function() {
        typeof callback === 'function' && callback(notes);
      });
    });

Notice that I passed the newly-loaded notes to the callback. That way we can do something else with them back in LoginController.

Let's update the callback that we pass in LoginController to receive our notes as an argument.

app/login/login.js

  $scope.submit = function() {
    NotesBackend.fetchApiKey($scope.user, function(notes){
      $location.path('notes');
    });
  };

We really want to call refreshNotes, but that's way over in NotesController. How can we call it from here?

Remember our custom directive way back when? We learned a new trick there: We listened for a message using $scope.$on, and broadcast the message with $scope.broadcast? We can do that again, but this time our separate controllers each have their own distinct $scope. Thankfully, AngularJS provides $rootScope, which we can use here to make things visible across controllers.

As you might guess, we need to inject another dependency to do so.

app/login/login.js

.controller('LoginController', function($scope, $rootScope, $location, NotesBackend) {
  ...
});

Now, after we've fetched the API key and changed our location, we'll broadcast a message, letting the world know that notes have been loaded. We'll pass along the notes themselves with that message.

app/login/login.js

  $scope.submit = function() {
    NotesBackend.fetchApiKey($scope.user, function(notes){
      $location.path('notes');
      $rootScope.$broadcast('notesLoaded', notes);
    });
  };

What good is a message if no one is listening? Let's be sure to listen for that message in NotesController.

app/notes/notes.js

noteApp.controller('NotesController', function NotesController($scope, $rootScope, $filter, NotesBackend) {
  ...
  NotesBackend.fetchNotes(this.refreshNotes);
  $rootScope.$on('notesLoaded', function(ev, notes) {
    self.refreshNotes(notes);
  });
});

Notice that I once again added $rootScope as a dependency, and I used $rootScope.$on in the same manner that we once used $scope.$on. As soon as that message is broadcast, this will be triggered.

The callback that we define here will always receive an event as its first argument. We're not actively doing anything with that event, but we assign it to ev in the function signature, so we at least know what that first argument holds. Any other arguments we passed in when we broadcast get defined after the event. In this case, notes is the additional argument we're expecting. Our whole goal with this broadcasting business is to call refreshNotes, and we need some notes to pass to it.

Now if you delete your cookie (using the Resources tab in the Chrome Developer Tools) and reload the login page, you should see the notes loaded in the sidebar when you login successfully.

That's worth a commit.

Since we get a more fully-featured user object back from the API when we authenticate (login), let's save that to a cookie and rename the fetchApiKey method to fetchUser to better describe what we are doing here. We'll add a getUser function as well.

app/login/login.js

  $scope.submit = function() {
    NotesBackend.fetchUser($scope.user, function(notes) { // instead of fetchApiKey
      $location.path('notes');
      $scope.user = user;
      $rootScope.$broadcast('notesLoaded', notes);
    });
  };

app/app.js

  this.getUser = function() {
    return user;
  };

  this.fetchUser = function(user, callback) { // instead of fetchApiKey
    var self = this;
    $http.post(nevernoteBasePath + 'session', {
      user: {
        username: user.username,
        password: user.password
      }
    }).success(function(userData){
      apiKey = userData.api_key;
      $cookies.put('apiKey', apiKey);
      $cookies.put('user', JSON.stringify(userData)); // serialize the user hash
      self.fetchNotes(function() {
        typeof callback === 'function' && callback(notes);
      });
    });
  };

Now that we are storing the entire user, it seems silly to hang onto the apiKey separately. It's already on the user object. So let's refactor a bit and change all calls to apiKey to user.api_key.

We can remove the top-level apiKey variable and getApiKey function, too (since we can just chain off of user, or getUser if necessary).

app/app.js

  var notes = [];
  var user = $cookies.get('user') ? JSON.parse($cookies.get('user')) : {};
...
    $http.get(nevernoteBasePath + 'notes?api_key=' + user.api_key)
...
    $http.post(nevernoteBasePath + 'notes', {
      api_key: user.api_key,
...
  this.fetchUser = function(user, callback) {
    var self = this;
    $http.post(nevernoteBasePath + 'session', {
      user: {
        username: user.username,
        password: user.password
      }
    }).success(function(userData){
      $cookies.put('user', JSON.stringify(userData));
      self.fetchNotes(function() {
        typeof callback === 'function' && callback(notes);
      });
    });
  };

You may have noticed that it still tries to load notes with an undefined api_key when you first load the login page without a cookie. Let's make a small adjustment to fetchNotes to only load if there is a user and the user has an api_key.

app/app.js

  this.fetchNotes = function (callback) {
    if (user.api_key) { // only run if user is logged in and has an API key
      $http.get(nevernoteBasePath + 'notes?api_key=' + user.api_key)
        .success(function(notesData) {
          notes = notesData;
          typeof callback === 'function' && callback(notes);
        });
    }
  };

While we are at it, let's hide the sidebar (including the new note button) when a user is not logged in.

Let's add a $scope.user function to NotesController and use it to grab the user object from NotesBackend.

notes/notes.js

  $scope.user = function() {
    return NotesBackend.getUser();
  };

Now we can use this function in an ng-show directive:

app/sidebar.html

<nav id="sidebar" ng-show="user().id">

If the object returned by getUser() has no id property, ng-show will be false, and the sidebar—not just the list of notes—will be hidden.

Make sure there's no button on /login after you delete the cookie, and that the button is back when you log in.

If it's working, commit it!