TLT: Create a RESTful save/unsave toggle link with Angular JS

I like how Reddit works. Its feeds, although extremely simple, have lots of useful functions built in. One of those functions is the ability to save/unsave a story via a link found in each object (story description) row. A good chunk of the initial design of one of the sites I'm building is based on (at-launch/alpha) Reddit, and one of the features I'm stealing is that 'save/unsave' link.

Problem

I have a REST API with some favorite/unfavorite links built in. Those links can be used to save (favorite) a story or delete a saved story from a saved story list. In this case, those links look like this:

Favorite (save) a story

../storyapi/{story_id}/favorite

Unfavorite (unsave) a story

../storyapi/{story_id}/favorite/delete/

In each story object (in the feed) row, I want there to be a toggle link/button a user can click to save (favorite) or unsave (unfavorite) a story.

As an example of what I want, refer to Reddit -- there's a 'save' link/button under each story's title.

Solution

index.html

<div class="container" data-ng-controller="PostController">  
  <div class="row row-centered" data-ng-repeat="story in stories">

    <div class=""><h4><a href="/story/{{ story.id }}/">
    {{story.title}}</a></h4>
    </div>

    <a class="" data-story-id="{{ story.id }}" ng-click="saveStory(story)">{{ story.saved_status }}</a>

    </div>
</div>  

app.js

// note: I used Angular Moment, but you won't see its use here
// only minor indications of use.

var app = angular.module('feed', ['angularMoment'])

.config(['$httpProvider', function($httpProvider) {
    $httpProvider.defaults.xsrfCookieName = 'csrftoken';
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
}])


.factory('Stories',function($http, moment){
  return {
    // handles loading the feed
    load: function(){
      return $http.get('storyapi/stories/popular/').
          success(function(data, status, headers, config) {
            return data.results;
          }).
          error(function(data, status, headers, config) {
            console.log("poop");
        });
    }, // end load

    // handles saving (favoriting) a story
    save: function(story) {
        var fav_link = "storyapi/" + story + "/favorite/";
        var csrftoken = $.cookie('csrftoken');

        $http.post(fav_link, {'csrfmiddlewaretoken': csrftoken }).
              success(function(data, status, headers, config) {
                console.log("saved");
                story.saved = true;
              }).
              error(function(data, status, headers, config) {
                alert(data);
              });

        story.saved = true;
    }, // end save

    // handles unfavoriting (unsaving) a story
    unsave: function(story) { 

      var fav_link = "story/" + story + "/favorite/delete/";
      var csrftoken = $.cookie('csrftoken');

      $http.post(fav_link, {'csrfmiddlewaretoken': csrftoken }).
            success(function(data, status, headers, config) {
              console.log("unsaved");
              // $scope.saved_status = "save";
              story.saved = true;
            }).
            error(function(data, status, headers, config) {
              alert(data);
            });

    }
  }
})

.controller("PostController", function($scope, moment, Stories) {
  Stories.load().then(function(stories){
    $scope.stories = stories.data.results;
    // this can be done differently, but as an example this is what you need
    angular.forEach($scope.stories, function (story) {
      story.saved_status = story.saved // if saved, say unsave and vice versa
        ? 'Unsave'
        : 'Save'
    });
  });

  $scope.saveStory = function(story) {
    var savedStatus = story.saved_status
    // if the button said 'Save', call Stories.save(), otherwise do opposite
    if (savedStatus === 'Save') {
      Stories.save(story.id);
      story.saved_status = 'Unsave'
    }
    else {
      Stories.unsave(story.id);
      story.saved_status = 'Save'
    }
  }
})

Note I'm using Django and the Django REST framework in the back to produce the API -- hence, the CSRF token.

A note on DRY code

So, in the previous TLT, I made a mistake by making an $http request inside of my controller.

The reason one shouldn't make $http calls in controllers (apparently) is that controllers are not really meant to be reused while factories are. Writing those calls in factories also makes sharing fetched resources possible between controllers, rather than forcing you to call the controllers multiple times for the same data.

Greg

Software Engineer

Subscribe to GregBlogs