November 16, 2013

Angular in the small

Applying Angular.js at jQuery scale

For those of you that aren’t familiar, Angular.js is one of a new breed of emerging frameworks dedicated to building rich, responsive in-browser interfaces. I’ve had the privilege of working with both Ember.js and Angular.js, and enjoyed them both, so this won’t be a showdown (if that’s what you came for, here’s a nice one).

So why Angular? Unlike Ember, Angular’s base implementation is a (relatively) svelte 80kb, minified. Ember weighs in at ~230kb, and depends on handlebars.js on top of that. Additionally, Angular is available on Google’s CDN (go figure).

By comparison, jQuery is 93kb minified.

Code Comparison

Today, I want to present an application of Angular.js that probably doesn’t spring to mind immediately: as a replacement for jQuery for small, event-driven apps. Today, the target of my exercise is a small tool I made to drive traffic to my niche email-marketing app, Address Bin. It’s a small utility that takes the content of a textarea, and calculates the Gunning-Fog Index for that text.

I’ve written two versions if this simple app: one using jQuery, and one using Angular.

First, the relevant HAML (it’s like HTML but less). It’s got the Angular stuff in there, which doesn’t impact the jQuery implementation.

%div(ng-app="" ng-controller="GfiCtrl")
  .sixteen.columns
    .twelve.columns.alpha
      %textarea#gfi-input(placeholder="Enter your text here" ng-model="corpus")
    .four.columns.omega
      %p
        The
        %a(href="https://en.wikipedia.org/wiki/Gunning_fog_index")
          Gunning-Fog Index
        is a measure of the complexity of your writing. A GFI of 12
        corresponds approximately to a Grade 12 reading level.
      %p
        Marketing copy for a general audience should have a reading level
        no higher than 8 or 9. Enter your text at the left to see how you score!

  .sixteen.columns
    #valid-score(ng-show="gfi > 0")
      %p
        Your Gunning-Fog index is
        %strong
          %span#gfi-value {{gfiScore}}.
        This corresponds to a
        %strong
          Grade
          %span#gfi-grade {{gfiGrade}}
        reading level

    #invalid-score(ng-show="gfi <= 0")
      Your score will be displayed after enough text is entered
      to calculate a meaningful score.

Then, some functions common to both implementations. Since I’m too good for Javascript, I used Coffeescript.

# Trim spaces from text
_trim = (text) ->
    text.replace(/^[^\w]*/, '').replace(/[^\w]*$/, '')

# Split text into words
_words = (text) ->
    (_trim(text).replace(/['";:,.?¿\-!¡]+/g, '').match(/\S+/g) || [])

# Split text into sentences and count them
num_sentences = (text) ->
    (_trim(text).split('.') || []).length

# Count the number of "complex" words
num_complex_words = (words) ->
    count = 0
    for word in words
        if word.length > 10
            count += 1
    count

# Calculate the GFI for the given text
gfi = (text) ->
    words = _words(text)
    word_count = words.length

    sentence_count = num_sentences(text)
    complex_word_count = num_complex_words(words)
    switch
        when word_count <= 50 then -1
        when sentence_count <= 3 then -1
        else 0.4 * ((word_count / sentence_count) + 100 * (num_complex_words(words) / word_count))

We need our dom-participating code to do 2 things as the text is updated:

  1. Show/hide the relevant informational panel at the bottom of the page
  2. Update the score and grade displays with the current calculated value

The jquery implementation goes like this:

(($) ->

    # Hide the score pane on ready
    $('#valid-score').hide()

    # Update the score on keyup
    $('#gfi-input').keyup ->
        score = gfi($('#gfi-input').val())

        # Toggle the score panel
        if score < 0
            $('#valid-score').hide()
            $('#invalid-score').show()
        else
            $('#valid-score').show()
            $('#invalid-score').hide()

            # Insert the values into the page
            $('#gfi-value').text(score.toFixed(2))
            $('#gfi-grade').text(parseInt(score + 1))

)(jQuery)

And now, in Angular:

window.GfiCtrl = ($scope) ->
    # Update the score when $scope.corpus changes
    $scope.$watch 'corpus', ->
        $scope.gfi = gfi($scope.corpus)

        # Update the bound variables gfiScore and gfiGrade
        $scope.gfiScore = $scope.gfi.toFixed(2)
        $scope.gfiGrade = parseInt($scope.gfi) + 1

Suddenly, the work of showing and hiding the explanatory div is transferred to the html, and the relevant fields are updated automatically when the bound textarea is edited.

More succint, more extensible, and more complete. The lesson here? That you don’t need to be developing a major application to benefit from Angular’s features. There’s more than one way to reach into the document from javascript.

Bonus: Angular plays (relatively) nice with clojurescript too:

(set! (.GfiCtrl js/window)
  (array "$scope" (fn [$scope]
    (.$watch $scope "corpus"
      #(let [score (gfi (.corpus $scope))]
        (set! (.gfi $scope) score)
        (set! (.gfiScore $scope) (.toFixed score 2))
        (set! (.gfiGrade $scope) (inc (js/parseInt score))))))))