Skip to content
January 4, 2013 / jphoward

End to end web app in under an hour–Part 2

Here is the video tutorial that goes with this post:

Continued from Part 1.

Search

Let’s add a basic search form to the top of list.html:

<form class="form-search">
    <div class="input-append">
        <input type="text" ng-model="query" class="input-medium search-query" placeholder="Search">
        <button ng-click="reset()" type="submit" class="btn"><i class="icon-search"></i></button>
    </div>
    <button ng-click="query=''; reset()" ng-disabled="!query" type="submit" class="btn">Reset</button>
</form>

“ng-model” is perhaps the most important and useful AngularJS directive: it creates a 2-way binding between a property in $scope and the value of an HTML element. In this case, our text box’s value is bound to $scope.query. Furthermore, the Reset button will be disabled automatically if $scope.query is empty, due to the use of the ng-disabled directive.

All we need now on the client side is to define $scope.reset() in our controller.

var ListCtrl = function ($scope, $location, Todo) {
    $scope.reset = function() {
        $scope.items = Todo.query({q: $scope.query});
    };

    $scope.reset();
};

Unfortunately, the query method that WebAPI creates for us does not support searching, sorting, or paginating. (Oddly enough, the pre-release versions of WebAPI did, but the functionality was stripped just before release!) Therefore, we will need to edit TodoController.cs to remove GetTodoItems(), and replace it with this:

public IEnumerable<TodoItem> GetTodoItems(string q = null, string sort = null, bool desc = false,
                                                        int? limit = null, int offset = 0) {
    var list = ((IObjectContextAdapter) db).ObjectContext.CreateObjectSet<TodoItem>();

    IQueryable<TodoItem> items = string.IsNullOrEmpty(sort) ? list.OrderBy(o=>o.Priority)
        : list.OrderBy(String.Format("it.{0} {1}", sort, desc ? "DESC" : "ASC"));

    if (!string.IsNullOrEmpty(q) && q != "undefined") items = items.Where(t => t.Todo.Contains(q));

    if (offset > 0) items = items.Skip(offset);
    if (limit.HasValue) items = items.Take(limit.Value);
    return items;
}

(Although we are not using sorting or pagination yet, we may as well include it in our method for later.) The optional parameters to the method are automatically mapped to the querystring by WebAPI – so e.g. index.html?q=something will pass ‘something’ as the value of the ‘q’ parameter. $scope.reset() sets this parameter to $scope.query. So, we now have working sort functionality!image

Pagination

Let’s now add pagination. That’s pretty simple actually. As you can see from GetTodoItems above, we can pass in an offset and a limit, so we just need to modify ListCtrl to only request 20 items at a time, and keep track of whether we have got all the items available (i.e. if we get less than 20 items in response, there is nothing more to retrieve). Note that we are now using the 2nd parameter to query(), which is a callback which is called on success. This allows us to append the additional items to the existing list.

$scope.search = function () {
    Todo.query({ q: $scope.query, limit: $scope.limit, offset: $scope.offset },
        function (items) {
            var cnt = items.length;
            $scope.no_more = cnt < 20;
            $scope.items = $scope.items.concat(items);
        }
    );
};

$scope.reset = function () {
    $scope.offset = 0;
    $scope.items = [];
    $scope.search();
};

$scope.show_more = function () { return !$scope.no_more; };

$scope.limit = 20;

$scope.reset();

That’s the entirety of the controller at this point. The only other thing we need is a link in details.html to grab another page of data.

<a href="" ng-click = "offset = offset + limit; search()" 
    ng-show ="show_more()">Show more</a>

The ng-show directive ensures that this link will not be shown when there is no further data (when show_more() returns false).

Sorting

In order to allow sorting, we’ll need to store sort order and direction in $scope, and then add to the Todo.query() params: sort: $scope.sort_order, desc: $scope.sort_desc . After adding those two parameters, be sure to initialize the order to whatever you prefer as the default.

$scope.sort_order = 'Priority';
$scope.desc = false;

Let’s now add a sort_by function that sets sort_order to whatever it is passed, and toggles the direction if it is called multiple times with the same order.

$scope.sort_by = function (ord) {
    if ($scope.sort_order == ord) { $scope.sort_desc = !$scope.sort_desc; }
    else { $scope.sort_desc = false; }
    $scope.sort_order = ord;
    $scope.reset();
};

All we need to do now is to make the table header clickable.

<th><a ng-click="sort_by('Todo')">Todo</a></th>

Finally, let’s add an icon that is shown depending on whether we are sorting by that column – add this just before </th>:

<span ng-show="do_show(true, 'Todo')"><i class="icon-circle-arrow-down"></i></span>
<span ng-show="do_show(false, 'Todo')"><i class="icon-circle-arrow-up"></i></span>

and the required js method:

$scope.do_show = function (asc, col) {
    return (asc != $scope.sort_desc) && ($scope.sort_order == col);
};

Creating a directive

That’s quite a lot of HTML for each individual TH! Let’s simplify it by writing a ‘sorted’ directive. Once the directive is written, we’ll be able to write our header like this:

<th sorted="Todo">Todo</th>

To start our directive, call the appropriate method, and set the options to create a new $scope in the directive, and also to transclude the directive content:

TodoApp.directive('sorted', function() {
    return {
        scope: true,
        transclude: true,

Now we are ready to include our template, which is simply the contents of the TH we created for the Todo header, with a couple of minor changes.

template: '<a ng-click="do_sort()" ng-transclude></a>' +
    '<span ng-show="do_show(true)"><i class="icon-circle-arrow-down"></i></span>' +
    '<span ng-show="do_show(false)"><i class="icon-circle-arrow-up"></i></span>',

Here, the ng-transclude directive tells AngularJS where we want the contents of the element put (they are put immediately after the element containing this attribute).

Then, we can add a controller to the directive containing the logic we wrote earlier:

controller: function($scope, $element, $attrs) {
    $scope.sort = $attrs.sorted;

    $scope.do_sort = function() { $scope.sort_by($scope.sort); };

    $scope.do_show = function(asc) {
        return (asc != $scope.sort_desc) && ($scope.sort_order == $scope.sort);
    };
}

That’s it! We now have a list display that sorts searching, sorting, and pagination.

In the next part, we’ll take a look at creating, deleting, and updating items.

Continued in Part 3.

Advertisements

18 Comments

Leave a Comment
  1. frankiebguitar / Feb 27 2013 12:01 pm

    There is a complete lack of consistency between your video and your text. The first part was great and leaves me wanting the second part here to be workable. Would it be possible to simply post your working code?

  2. Pinal Bhatt (@pbdesk) / Mar 7 2013 11:50 pm

    once again gr8 work. Nicely explained the concept of Directives in Angular.

  3. Peter Ennis / May 7 2013 11:41 am

    Nice tutorial. At 42.21 click Todo header repeatedly and the list gets longer with the same content added each time. It happens in other places also, but this is the most obvious. Header sorting should only operate on the list displayed. Not sure if it is your code or due to some minor name changes I made. There is no project source for me to verify against. Everything else works fine so far.

    • peterennis / May 7 2013 1:41 pm

      OK – ignore that – following on shows that you fixed it. More haste, less speed?

  4. Zbigniew Kossowski (@ZKossowski) / Jun 10 2013 9:34 pm

    Tnx for great article.
    I found if I use 2 json queries one after another the vars from first query are not usable in second one eg. I take $scope.limit and $scope.offset form one json and produce url …?limit=undefined&offset=undefined in the second json query. NB These vars are viewable in html and refreshes nice after I use button with reset().

  5. Peter Hirt (@roestigraben) / Jun 22 2013 12:37 am

    I have used this tutorial to learn about Angularjs which is a great MVC I must say.
    My problem is with the interface into MySQL. As I do not have the MS tools available, I want to use the arrest PHP based REST API to talk to the MySQL.
    GET and DELETE works just fine. Now I am at the PUT stage and get into problems.

    I use Chrome developers tools to look into the network activity. The PUT transaction is transmitting a void object while the GET transactions are having all parameters nicely transmitted such as todos?limit=20&order=desc…
    Consequently I think the arrest API is not the one in question. It has more to do that Angularjs does not “spit” out the needed request.

    Can somebody give me some hint on how to proceed, please?

    as controller I use the exact same code as Jeremy Howard is proposing. The console.log’s are all OK
    var CreateCtrl = function ($scope, $location, Todo) {
    $scope.save = function(){
    console.log(‘you clicked the save button with item info like:’);
    console.log(‘IP name: ‘ + $scope.item.name);
    console.log(‘IP provider: ‘ + $scope.item.provider);
    console.log(‘technology: ‘ + $scope.item.technology);
    Todo.save($scope.item, function(){
    $location.path(‘/’);
    });
    };
    };

  6. Rafael Pyasetsky / Jun 26 2013 4:51 am

    Correction the previous my comment:
    It is not needed to retrieve data from a server for implementation of sorting.
    You need to make the following change to apply filter orderBy in list.html:
    $lt tr ng-repeat=”todo in items | orderBy:sort_order:is_desc”&gt

  7. Karthiraj Periaswamy / Sep 18 2013 2:40 am

    Thanx for one of the great article.

    I am trying to convert

    Todo

    to a directive
    Like :

    I struck up in defining Template for that..

    • Karthiraj Periaswamy / Sep 18 2013 2:43 am

      Thanx for one of the great article.
      I am trying to convert <th> <a ng-click="sort('Text')">Todo</a> <span ng-show="sort_order=='Text' && desc==false"><i class="glyphicon glyphicon-sort-by-alphabet"></i></span> <span ng-show="sort_order=='Text' && desc==true"><i class="glyphicon glyphicon-sort-by-alphabet-alt"></i></span> </th>

      to a directive Like :
      <th todoField="Text"><th> <th todoField="Priority"><th> <th todoField="Priority"><th>

      I struck up in defining Template for that..

  8. Karthiraj Periaswamy / Sep 18 2013 7:01 am

    i got it….

    —-JS—-

    TodoApp.directive('ngfieldheader', function ()

    { return

    {

    restrict: 'A',

    replace: true,

    transclude: true,

    template: '<span><a ng-click="$parent.sort(value)" ng-transclude></a> ' + ' <span ng-show="$parent.sort_order==value && $parent.desc==false"><i class="glyphicon glyphicon-arrow-down"></i></span>' + ' <span ng-show="$parent.sort_order==value && $parent.desc==true"><i class="glyphicon glyphicon-arrow-up"></i></span></span>',

    scope: { value: "@myfield", }

    }

    });

     

    —-HTML—-

    <th> <span ngfieldheader myfield="Text">Todo</span> </th>

    <th><span ngfieldheader myfield="Priority">Priority</span> </th>

    <th><span ngfieldheader myfield="DueDate">Date</span> </th>

  9. Rebecca Love / Nov 16 2013 7:05 am

    This tutorial is fantastic! Thank you very much for putting this together.

    Maybe you have a suggestion to help me out with the concat data addition. When I added the line

    $scope.concat = [];

    to the app.js file, I still get the error

    Cannot call method ‘concat’ of undefined.

    It runs with this function (20 rows per page per click):
    function(data) {
    $scope.more = data.length === 20;
    $scope.todos =
    data;
    });

    It does not run with this function:
    function(data) {
    $scope.more = data.length === 20;
    $scope.todos =
    $scope.todos.concat(data);
    });

    • Rodney Lewis / Feb 3 2014 1:47 pm

      You should try $scope.todos +=
      $scope.todos.concat(data);

  10. Sandy | WebSite Designer & Developer / Sep 11 2014 11:42 am

    The tutorial is amazing:)

  11. javierglc / Jan 27 2015 9:14 am

    I got stuck at minute 33: the displayed web page now just shows the headers and the “show more link”, but in between the data is gone.
    it appears like the concat() function is not working.
    Outside the TodoApp module the concat() function gets called, but inside seems is not working.
    Please help!

Trackbacks

  1. End to end web app in under an hour–Part 3 « Jeremy Howard
  2. End to end web app in under an hour–Part 1 « Jeremy Howard
  3. Todo Web App Part 2 - ASP.NET MVC Angular Web API | Share You All

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: