Building Widgets

(with AngularJS)

Aaron N. Smith,  Principal @ Modus Create
@aaron_n_smith  |  aaron@moduscreate.com

What do we mean by 'widgets'?


  • Data visualizations
  • Interactive
  • Reusable
  • Usually not full screen
  • Think jQueryUI, Bootstrap


What Qualities do we seek?


  • Functions consistently across a wide range of browsers
  • Easy integration into applications
  • Works reliably - i.e. as documented
  • Good test coverage (unit + functional)


Why Angular?

This is where Angular should shine!

<sun-image/>

Custom markup with declarative
binding of data and event handlers.

Which Angular?

  • 1.0 & 1.1 - I certainly hope not
  • 1.2 - Need to support IE8
  • 1.3 - Don't need it in production for a bit
  • 2.0 - Keep dreaming




The angular widget community





The problem with many Angular widgets


is that they aren't Angular widgets

Wrapping jQuery UI or Bootstrap widgets with Angular directives is not the same as creating Angular widgets.



This doesn't mean they're not valid
but we should aim higher.


Enter AngularUI

http://angular-ui.github.io/



Great examples of widgets done right with Angular.

AngularUI Good and bad

  • Clean package structure
  • Good setup and usage docs  
  • Good test coverage



  • JS files combined too much
  • JS and HTML are separated
  • Not all production ready




Best practices

widgets are directives

So let's look at best practices for directives:

  1. Restrict to element or attribute
  2. Use isolate scope or transclude
  3. Expose clear, discrete inputs rather than complex objects
  4. Use external templates
  5. Prefer discrete CSS classes over pseudo-elements
  6. Avoid monolithic directives - break them down!

Element vs. Attribute, etc.


Element is for widgets.
Attribute is for decorators.
Comment and Class are for the birds.

End of story.

app.directive('myWidget', function() {
    return {
        restrict: 'E'
        //...
    };
}

SCOPE





Isolate Scope
  • Helps you avoid conflicts
  • Delivers a clear API

Inherited Scope
  • Sometimes necessary with directives
  • Bad idea for widgets
Transclude Scope
  • For wrapper widgets
  • Combine with Isolate Scope!

Inputs

  • Clear, discrete options for inputs
  • Inputs should have defaults
  • Allow global overrides via Providers

app.directive('myWidget', function(MyWidgetConfig) {
    return {
        restrict: 'E',
        scope: {
            param1: '=',
            param2: '='
        },
        link: function(scope) {
            scope.param1 = scope.param1 || MyWidgetConfig.param1;
            scope.param2 = scope.param2 || MyWidgetConfig.param2;
        }
    };
});

Templates

  • Use templateUrl instead of inline templates
  • Bundle templates in $templateCache

app.directive('myWidget', function() {
    return {
        templateUrl: 'myWidget.tpl.html'
    };
});

Markup/CSS

Use clear CSS classes instead of pseudo-elements.
Prefix names to avoid conflicts.

<div class="combo">
   <div class="combo-label"></div> <!-- label would be bad here -->
   <div class="combo-input"></div> <!-- input would be bad here -->
   <div class="combo-trigger"></div>
</div>

Aim for flexibility.
 

PUtting it all together

  • Dependencies
  • Module organization
  • Licensing
  • GitHub
  • Bower

Recommended organization

Source
    - forms
          - form.js
          - form.spec.js
          - form.tpl.html
          - form.css
          - input.js
          - input.spec.js
          - input.tpl.html
          - input.css
    - calendar
          - calendar.js
          - calendar.spec.js
          - calendar.tpl.html
          - calendar.css
    - app.js
Output
ShinyWidgetBundle-0.0.1.js
ShinyWidgetBundle-tpl-0.0.1.js




QUestions?


Diving in


  • Exposing functions
  • Using 'track by'
  • Transclusion
  • Example walk-through

Exposing functions

// Directive definition
app.directive('superForm', {
  return {
    restrict: 'E',
    scope: {
      onSubmit: '&' // <--- & for binding functions
    },
    template: '<form><button ng-click="submit()">Submit</button></form>',
    link: function(scope) {
      scope.submit = function() {
        scope.onSubmit({firstName: 'Aaron', lastName: 'Smith'});
      };
    }
  };
});
<!-- Directive usage -->
<super-form onSubmit="submitHandler(firstName, lastName)"></super-form>
// Handler definition
$scope.submitHandler = function(firstName, lastName) {
   console.log(firstName + ' ' + lastName); //Prints 'Aaron Smith'
});

Track by is your friend


// Allow dupes
<div ng-repeat="item in items track by $index">{{item.value}}</div>

// Avoid rewrites (and dupes)
<div ng-repeat="item in items track by item.id">{{item.value}}</div>

Regarding Transclude...

A simple, but often misunderstood, concept.

"That's not a word you'll find in a dictionary."
- Omkar Patil


"Transclusion is almost a gift to people criticizing Angular."
- Matt Briggs


Transclusion

Wikipedia says: 
"In computer science, transclusion is the inclusion of a document or part of a document into another document by reference."

Therefore, a transclude directive wraps arbitrary content.

Think of a window that can contain anything.

See: AngularUI's modal directive for an example.



Example walk-through


Let's reinvent the wheel

a Naive <select> menu


In my experience, the template is a good place to start.

Naive <select> Template

<div class="combo">
  <div class="combo-input">{{displayValue}}</div>
  <div class="combo-trigger" ng-click="showOptions = !showOptions"></div>
  <div class="combo-options" ng-show="showOptions">
    <div class="combo-option" 
         ng-repeat="option in options track by option[valueField]"
         ng-class="{selected: option[valueField] == value}"
         ng-click="onSelectItem(option)">
         {{option[displayField]}}
    </div>
  </div>
</div>

Naive <select> Directive

 app.directive('combo', function(ComboConfig) {
    return {
        restrict: 'E',
        scope: {
            value: '=',
            options: '=',
            valueField: '=',
            displayField: '=',
            onChange: '&'
        },
        templateUrl: 'forms/combo.tpl.html',
        link: function($scope, element, attrs, controller) {
           //... 
        }
    }
})

naive <select> Directive link()

$scope.valueField = $scope.valueField || ComboConfig.id;
$scope.displayField = $scope.displayField || ComboConfig.id;
$scope.options = $scope.options || [];
$scope.showOptions = false;
$scope.displayValue = updateDisplayValue();
$scope.$watch('value', updateDisplayValue);

$scope.onSelectItem = function(item) {
    $scope.showOptions = false;
    $scope.value = item[$scope.valueField];
    $scope.onChange({value: item});
};

function updateDisplayValue() {
    $scope.displayValue = ''; //Make sure it's reset
    if ($scope.value) {
        $scope.options.forEach(function(option) {
            if (option[$scope.valueField] === $scope.value) {
                $scope.displayValue = option[$scope.displayField];
            }
        });
    }
}

Naive <select> CSS

.combo {
    position: relative;
}
.combo-input {
    border: 1px solid #555; padding: 4px;
}
.combo-trigger {
    border: 1px solid #555; border-left-width: 0px;
}
.combo-options {
    position: absolute; top: 30px; border: 1px solid #555;
}
.combo-option.selected {
    background-color: #00c; color: #fff;
}

Naive <select> Usage

<combo value="country" options="countries" 
       on-change="onCountryChange(value)"></combo>





Now the customer says that some 
combos need to 'autocomplete'...



Our combobox directive is monolithic.

What do we do?

In other frameworks, we might use inheritance.

In AngularJS, we use composition.

Options-List + xyz-Field

Now we can use different fields but reuse the options behavior without baking all our functionality into one giant widget.

<!-- our AutoComplete combo template -->
<div class="combo autocomplete">
   <autocomplete-field></autocomplete-field>
   <options-list></options-list>
</div>

<!-- our standard combobox template -->
<div class="combo">
    <combo-field></combo-field>
    <options-list></options-list>
</div>




Composition Over Inheritance FTW


<success-kid-meme/>




questions

Building Widgets

By Aaron N. Smith