Matt RaibleMatt Raible is a Web Architecture Consultant specializing in open source frameworks.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

Developing with AngularJS - Part I: The Basics

There's many, many different introductions to AngularJS available on the internet. This article is not another introduction, but rather a story about my learning experience. It all started way back in January of this year. I was working as a UI Architecture Consultant at Taleo/Oracle, my client for the last 21 months. My gig there ended last month, but they agreed to let me publish a series of articles about the knowledge I gained.

Project Background

The Director of Product Management had been working on the concepts for a new project - codenamed "Visual MyView". Below is a mockup he created for our kickoff meeting on January 4th.

My Dashboard - Original Mockup

From his original email about the above mockup:

The intent here is that one of the columns has rows that have a similar width. The rows could be dragged and dropped into a different order – or potentially the two columns could also be reordered. The rows will basically be comprised of similar widgets. You can see in the mockup how the first two rows might look – and sample widgets. The widgets shown can be configured by the end user, as well as the order in which they are displayed. Other requirements given to us were the following.

  • Row 1 is comprised of 'summary' widgets that are 'todo' items. Reviews needing done – approvals required – etc.
  • Row 2 will be a graph row – having graphs and charts to display information – larger squares will build this row.
  • Row 3's content was not determined yet.

I started the initial layout with static HTML and CSS and had a wireframe to show by mid January.

Wireframe

By the end of January, we'd renamed the project to My Dashboard and had a working prototype using CoolClock and moment.js for the clock in the top right, AngularJS to display widget data, jQuery UI for drag-n-drop of rows and widgets, Bootstrap's Carousel for holding charts and Highcharts for rendering charts. For this prototype, we included 4 types of widgets:

  1. Summary
  2. Tasks
  3. Charts
  4. Reports

To create widgets, we had to decide on a common schema for them.

"id": 1, // not necessary for display, but likely needed if we modify and save preferences
"title": "Appointments Today",
"type": "summary", // others include: task, chart, report
"value": 3, 
"description": "10:30 Jim Smith",
"events": "url", // this can have click events
"order": 1 // used to determine order

Below is a screenshot of our wireframe with some sample widgets.

Wireframe with Data

Angular Basics

The decision to use AngularJS came early on in the project, after I read Tyler Renelle's Rant: Backbone, Angular, Meteor, Derby. To learn AngularJS, I briefly looked at its homepage documentation and played with some examples. Then I stumbled upon Misko Hevery and Igor Minar's AngularJS Presentation from Devoxx 2012. At that time, the video wasn't publicly available (it's free now), so I had to buy a Parley's subscription ($79). It was well worth the money because that one hour video greatly contributed to my understanding of how AngularJS works. Another resource I used frequently to figure out how to do things was John Lindquist's egghead.io.

To begin with, we wrote the JSON for a bunch of sample widgets and embedded them into the page as a widgetData JavaScript variable.

var widgetData = [
    {"id": 1, "title": "Appointments Today", "type": "summary", "value": 3, "description": "10:30 Jim Smith", "events": {"click": "alert('foo');"}, "order": 1},
    {"id": 12, "order": 2, "title": "Offer Approvals", "type": "task", "class": "sticky-note", "value": 1},
    {"id": 103, "title": "Browser market shares at a specific website, 2010", "order": 1, "type": "chart", "chartType": "pie", 
         "tooltip": {"pointFormat": "{series.name}: <b>{point.percentage}%</b>", "percentageDecimals": 1}, "series": [
         {"type": "pie", "name": "Browser share", "data": [
             ["Firefox", 45.0],
             ["IE", 26.8],
             {"name": "Chrome", "y": 12.8, "sliced": true, "selected": true},
             ["Safari", 8.5],
             ["Opera", 6.2],
             ["Others", 0.7]
         ]}
    ]},
    ...
];

I used angular-seed to create the initial structure of the prototype, and continued using the same JavaScript file names when we moved it into the product I worked on. Since the application takes a while to login and render the My Dashboard page (when working remotely), I decided not to use the Karma testing framework that ships with Angular. Below is what our directory structure looked like for our prototype.

Angular Seed Directory Structure

The JavaScript files in the "js" folder are the most important for Angular. The first file, app.js, loads the other files:

angular.module('dashboard', ['dashboard.filters', 'dashboard.services', 'dashboard.directives']);

The controllers.js file contains the Controllers (functions) that get the data and make it available to the page. Here's the code for our first controller:

'use strict';

/* Controllers */
function WidgetController($scope) {
    $scope.widgets = widgetData;
}

This puts the widgets in scope and then we were able to render them using Angular's ngRepeat directive and the following HTML:

<div ng-app="dashboard" class="dashboard">
    <div class="container-widgets" ng-controller="WidgetController" ng-cloak>
        <div class="row-fluid">
            <div class="span9">
                <ul class="widgets">
                    <li id="summary-bar">
                        <div class="heading">Summary</div>
                        <ul class="tiles">
                            <li class="span3" ng-repeat="widget in widgets | filter:{type: 'summary'} | orderBy: 'order'">
                                <h3 class="events">{{widget.value}}</h3>
                                <div class="title">{{widget.title}}</div>
                                <div class="desc">{{widget.description}}</div>
                            </li>
                        </ul>
                    </li>
                    <li id="task-bar">
                        <div class="heading">My Tasks</div>
                        <ul class="tasks">
                            <li class="task {{widget.class}}" ng-repeat="widget in widgets | filter: {type: 'task'} | orderBy: 'order'">
                                <div class="title events">{{widget.title}}</div>
                                <div class="value">{{widget.value}}</div>
                            </li>
                        </ul>
                    </li>
                    <li id="chart-bar">
                        <div class="heading">Charts</div>
                        <div id="chartCarousel" class="carousel slide">
                            <ol class="carousel-indicators">
                                <li data-target="#chartCarousel"
                                    ng-repeat="widget in widgets | filter: {type: 'chart'} | orderBy: 'order'"
                                    data-slide-to="{{$index}}" ng-class="{active: $index == 0}"></li>
                            </ol>
                            <div class="carousel-inner">
                                <div class="item chart"
                                     ng-repeat="widget in widgets | filter: {type: 'chart'} | orderBy: 'order'"
                                     ng-class="{active: $index == 0}">
                                    <chart class="widget" value="{{widget}}" type="{{widget.chartType}}"></chart>
                                </div>
                            </div>
                            <a class="left carousel-control" href="#chartCarousel" data-slide="prev">‹</a>
                            <a class="right carousel-control" href="#chartCarousel" data-slide="next">›</a>
                        </div>
                    </li>
                </ul>
            </div>
            <div class="span3">
                <!-- clock and reports -->
            </div>
    </div>
</div>

The beginning of this HTML shows how Angular is instantiated: ng-app matches the name defined in app.js, ng-controller instantiates the WidgetController and ng-cloak is used to hide everything until its processed.

<div ng-app="dashboard" class="dashboard">
    <div class="container-widgets" ng-controller="WidgetController" ng-cloak>

If you take a closer look at the way ng-repeat attributes, you'll see how filters are used to filter data. There's filter and orderBy filters that are built in and allow you to filter data. The filter filter allows you to query arrays by strings, objects and even functions. In the following code block, "task" widgets are filtered, ordered and displayed.

<li class="task {{widget.class}}" ng-repeat="widget in widgets | filter: {type: 'task'} | orderBy: 'order'">
    <div class="title events">{{widget.title}}</div>
    <div class="value">{{widget.value}}</div>
</li>

This was pretty straightforward, but we quickly noticed that if a widget had HTML in its title, it didn't display correctly (rendering the raw HTML). To process the HTML, we had to use the ngBindHtml directive (tip: directives are camelCase, but written with dashes in HTML).

<div class="title events" ng-bind-html="widget.title"></div>

After getting this to work, we noticed that some titles weren't fully rendered because they were hidden with overflow: hidden. We tried adding a tooltip with title="{{widget.title}}", but ran into the same issue. I sent an email to the AngularJS Google Group and received a solution: create an htmlTitle directive:

.directive('htmlTitle', function ($sanitize) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            attrs.$observe('htmlTitle', function (title) {
                // convert &value; to HTML
                var html = angular.element('<div></div>').html($sanitize(title)).text();
                element.attr('title', html);
                element.html(html);
            });
        }
    }
})

Usage:

<div class="title events" ng-bind-html="widget.title" html-title="{{widget.title}}"></div>

Drag-and-Drop

To implement drag-and-drop functionality, I originally used jQuery UI's sortable. At the bottom of the page, the following code initialized sorting for the various lists:

$(document).ready(function() {
    $('.widgets').sortable({
        cursor: "move",
        handle: ".heading"
    }).disableSelection();
    $('.tiles,.tasks').sortable();
    var carousel = $('.carousel');
    $(carousel).carousel({
        interval: 0
    });
};

As you can see, it also initializes the carousel and stops it from cycling automatically.

The first problem I ran into with Bootstrap's Carousel was a strange error from Highcharts. If you look in the above HTML, you'll see there's a <chart> element. This is processed by a highcharts directive. When I tried to use this directive for Highcharts in a carousel, it results in the following error:

TypeError: Cannot read property 'length' of undefined at Object.ob.setMaxTicks

This seemed to be caused by the following css in Bootstrap:

.carousel-inner > .item { display: none }

When I added an override with "display: block" to my stylesheet, everything worked, but the charts were stacked instead of in a carousel. To fix this, I modified the directive to show/hide the "item" element so Highcharts was able to write to it. I also logged an issue for this.

if (element.parent().not(':visible')) {
    element.parent().show();
}
var chart = new Highcharts.Chart(newSettings);
element.parent().attr('style', '');

ngRepeat and Grouping

The last thing I accomplished in our end-of-January prototype was rendering 2 charts side-by-side. I got it working with plain HTML, created a "groupBy" filter for Angular and tried to get it to work with the following:

<div id="chartCarousel" class="carousel slide">
    <ol class="carousel-indicators">
        <li data-target="#chartCarousel" ng-repeat="widget in widgets | filter: {type: 'chart'} | groupBy"
            data-slide-to="{{$index}}" ng-class="{active: $index == 0}"></li>
    </ol>
    <div class="carousel-inner">
        <div class="item" ng-repeat="widget in widgets | filter: {type: 'chart'} | groupBy" ng-class="{active: $index == 0}">
            <div class="widget">{{widget[0].title}}</div>
            <div class="widget">{{widget[1].title}}</div>
        </div>
    </div>
    <a class="left carousel-control" href="#chartCarousel" data-slide="prev">‹</a>
    <a class="right carousel-control" href="#chartCarousel" data-slide="next">›</a>
</div>

This all worked like I expected it to in Chrome, but I the following errors showed in my console.

Error: 10 $digest() iterations reached. Aborting! Watchers fired in the last 5 iterations:

I sent an email to the Angular Google Group and received a link to a discussion where I found a "chunk" filter that solved the problem. This worked great, but I wanted to make it more responsive.

  1. If the user has a screen size big enough to fit 2 charts, show 2 charts and paginate by 2.
  2. If the user has a small screen size that only fits 1 chart, show 1 and paginate by 1.

To solve #1 and #2, I ended up rendering two different sections (with classes .oneup and .twoup) and displayed them based on screen size.

<li id="chart-bar">
    <div class="heading">Charts</div>
    <div id="chartCarousel1" class="carousel slide oneup" style="display: none">
        <ol class="carousel-indicators">
            <li data-target="#chartCarousel1"
                ng-repeat="widget in widgets | filter: {type: 'chart'} | orderBy: 'order'"
                data-slide-to="{{$index}}" ng-class="{active: $index == 0}"></li>
        </ol>
        <div class="carousel-inner">
            <div class="item chart"
                 ng-repeat="widget in widgets | filter: {type: 'chart'} | orderBy: 'order'"
                 ng-class="{active: $index == 0}">
                <chart class="widget" value="{{widget}}" type="{{widget.chartType}}"></chart>
            </div>
        </div>
        <a class="left carousel-control" href="#chartCarousel1" data-slide="prev">‹</a>
        <a class="right carousel-control" href="#chartCarousel1" data-slide="next">›</a>
    </div>
    <div id="chartCarousel2" class="carousel slide twoup" style="display: none">
        <ol class="carousel-indicators">
            <li data-target="#chartCarousel2"
                ng-repeat="widget in widgets | filter: {type: 'chart'} | chunk: 2 | orderBy: 'order'"
                data-slide-to="{{$index}}" ng-class="{active: $index == 0}"></li>
        </ol>
        <div class="carousel-inner">
            <div class="item chart"
                 ng-repeat="widget in widgets | filter: {type: 'chart'} | chunk: 2 | orderBy: 'order'"
                 ng-class="{active: $index == 0}">
                <chart class="widget" value="{{widget[0]}}" type="{{widget[0].chartType}}"></chart>
                <chart class="widget" value="{{widget[1]}}" type="{{widget[1].chartType}}"></chart>
            </div>
        </div>
        <a class="left carousel-control" href="#chartCarousel2" data-slide="prev">‹</a>
        <a class="right carousel-control" href="#chartCarousel2" data-slide="next">›</a>
    </div>
</li>

The JavaScript to show the correct number of charts is below:

var chartBar = $('#chart-bar');
function showCharts() {
    if (chartBar.width() < 960) {
        chartBar.find('.oneup').show();
        chartBar.find('.twoup').hide();
    } else {
        chartBar.find('.twoup').show();
        chartBar.find('.oneup').hide();
    }
}

$(document).ready(function () {
    showCharts();
});

$(window).resize(showCharts);

Summary

Even though I got everything to work for our initial prototype using Angular and jQuery, it didn't quite feel like I was taking full advantage of Angular's power. In particular, I learned that Angular UI Bootstrap had their own carousel and Angular UI had a sortable directive. My suspicions were confirmed when I read How do I "think in AngularJS" if I have a jQuery background?

In the next article, I'll talk about how I migrated to use Angular UI's carousel and sortable directives, as well as integrating dialogs.

Posted in The Web at Jun 18 2013, 09:06:52 AM MDT 9 Comments
Comments:

Thank you, looking forward to your next article.

Posted by huzzi on June 18, 2013 at 10:09 AM MDT #

Matt,
given your experience with both GWT and AngularJS how would you compare the web app development with both ?
What approach would you chose for developing of a new web app if you had a choice? And what would be your main decision criteria if the choice may vary?

Posted by Leo on June 18, 2013 at 02:05 PM MDT #

Hi Matt,

thank your for sharing your experiences with us.

From my experience I have found it quite difficult to integrate jQuery and AngularJS properly without messing up event handling and DOM manipulations.

I guess you will get into Angular directives in your next posts ?

However, I am wondering whether and to what extent jQuery still plays a vital role in your application.

Regards
Metin

Posted by Metin on June 18, 2013 at 03:44 PM MDT #

@Leo - I think GWT makes a lot of sense for Java developers, particularly those with a Swing background. AngularJS makes more sense for web developers (a.k.a. those that like to write JavaScript and CSS). However, I think Java devs will be attracted to Angular because of its emphasis on file structure, modularity and testability.

While I do enjoy writing with both frameworks, I still find that doing a straight-up Grails app gets me into the most productive flow. So I think there's still life in server-side frameworks because they've been so refined and perfected over the years.

@Metin - yes, I hope to touch on jQuery and AngularJS in my next article. jQuery still played a vital role in this particular application, even after refactoring to be more Angular-esque. In Part 4, I'll show how I ended up using jQuery for a lot of new design features we implemented. I could have written them as directives, but I found it faster to simply use jQuery.

Posted by Matt Raible on June 18, 2013 at 09:54 PM MDT #

Matt, thanks for your response!

I also think that Grails gives the best productivity for an experienced Java developer and this is my choice as well.
But Grails does not provide you rich UI out of the box. To achieve rich user experience one have to mingle GSPs with JQuery or use GWT or Angular or other similar alternative.

Posted by Leo on June 20, 2013 at 02:56 AM MDT #

[Trackback] A couple of days ago, I wrote an article on how I started developing with AngularJS . I used AngularJS for several months to develop a "My Dashboard" feature for a client's product and learned a whole bunch of stuff along the way. This article pro...

Posted by Raible Designs on June 20, 2013 at 09:21 AM MDT #

It would seem that after authoring and using the htmlTitle directive, you wouldn't have to use the ngBindHtml one - at least that's what i get from the source code... isn't that true?

Posted by Andreas Andreou on June 20, 2013 at 04:29 PM MDT #

Andreas - the htmlTitle is for the "title" attribute of the tag, while the ngBindHtml one is for the innerHTML part of the tag. Looking at the code, I agree that both might not be necessary.

Posted by Matt Raible on June 20, 2013 at 07:03 PM MDT #

Yep, exactly that last line of element.html(html) in htmlTitle caught my eye :D

Posted by Andreas Andreou on June 20, 2013 at 07:12 PM MDT #

Post a Comment:
  • HTML Syntax: Allowed