After spending substantial time working in-depth in Angular land, and with further influence from incursions into Flux & React, I've come to develop certain opinions on how to best architect non-trivial, data-driven UI flows in an Angular application.

What follows is a case study of a real-world UI problem, solved with the guidance of well-established principles and patterns in software design.

Read the code on Github • See the live demo

The User Story

Jill works for The Widget Factory, a company in the business of making widgets. Oftentimes, she wants to be able to test how slightly different widgets perform against each other.

Rather than waste time creating the otherwise-same widget several times over, she would like to be able to quickly generate the different permutations, and be done with it.

Some Guiding Principles

Before we begin, I'd like to highlight some design principles that will guide our implementation. While the purpose of this article is to demonstrate rather than explain these topics in detail, I've included links for further reading.

The Single Responsibility Principle (SRP) 1 2

Separation of Concerns (SoC) 3

Don't Repeat Yourself (DRY) 4

We'll proceed iteratively. First, make it work—then, refactor.

First: how do we generate permutations?

TDD for this type of stuff is a must. Consider this object of permutable attributes:

1 2 3 permutable_attributes = name: [ 'Foobar' , 'Bizbat' ] description: [ 'I pity the foo.' , 'Lorem ipsum.' ]

From this, we would generate 4 possible permutations:

1 2 3 4 5 6 7 8 9 10 11 12 13 [ name: 'Foobar' description: 'I pity the foo.' , name: 'Foobar' description: 'Lorem ipsum.' , name: 'Bizbat' description: 'I pity the foo.' , name: 'Bizbat' description: 'Lorem ipsum.' ]

Let's expand on those expectations: permutation-factory.spec.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 describe 'permutationFactory:' , -> permutationFactory = null beforeEach module 'app.permutation' beforeEach inject ( _permutationFactory_ ) -> permutationFactory = _permutationFactory_ describe 'permute:' , -> # Expected result for 2*2 result_2x2 = [ name: 'foo' attr: 'biz' , name: 'foo' attr: 'bat' , name: 'bar' attr: 'biz' , name: 'bar' attr: 'bat' ] it 'Should generate 4 permutations from 2*2 attributes.' , -> permutable_attributes = name: [ 'foo' , 'bar' ] attr: [ 'biz' , 'bat' ] permutations = permutationFactory . permute permutable_attributes expect ( permutations ). toEqual result_2x2 it 'Should ignore attributes without any values.' , -> # Some attributes won't be required--we want to skip those. permutable_attributes = name: [ 'foo' , 'bar' ] empty: [] attr: [ 'biz' , 'bat' ] permutations = permutationFactory . permute permutable_attributes expect ( permutations ). toEqual result_2x2 it 'Should ignore empty attributes at the end of the object.' , -> permutable_attributes = name: [ 'foo' , 'bar' ] attr: [ 'biz' , 'bat' ] empty: [] permutations = permutationFactory . permute permutable_attributes expect ( permutations ). toEqual result_2x2 it 'Should generate 6 permutations from 2*1*0*3 attributes.' , -> permutable_attributes = name: [ 'foo' , 'bar' ] type: [ 'biz' ] empty: [] desc: [ 'bing' , 'bang' , 'boom' ] permutations = permutationFactory . permute permutable_attributes expect ( permutations ). toEqual [ name: 'foo' type: 'biz' desc: 'bing' , name: 'foo' type: 'biz' desc: 'bang' , name: 'foo' type: 'biz' desc: 'boom' , name: 'bar' type: 'biz' desc: 'bing' , name: 'bar' type: 'biz' desc: 'bang' , name: 'bar' type: 'biz' desc: 'boom' ] it 'Should invoke an optional callback for each permutation' , -> callback = jasmine . createSpy 'callback' permutable_attributes = name: [ 'foo' , 'bar' ] attr: [ 'biz' , 'bat' ] permutations = permutationFactory . permute permutable_attributes , callback expect ( callback . calls . count ()). toBe 4 for i in [ 0 .. 3 ] expect ( callback . calls . argsFor ( i )). toEqual [ result_2x2 [ i ]]

With tests in place, now we can do our implementation: permutation-factory.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 ### @name permutationFactory @description Utility service for generating permutations of a resource. ### angular . module 'app.permutation' . factory 'permutationFactory' , -> ### @name permute @description Generates permutations from a permutable_attributes object. @param {Object} permutable_attributes @param {[Callback]} callback - Optional, invoked for each permutation @callback callback @param {Object} permutation @returns {Array} - Collection of permutations @example ```coffeescript permutations = permutationFactory.permute name: ['Foobar', 'Bizbat'] description: ['I pity the foo.', 'Lorem ipsum.'] ``` ### permute: (permutable_attributes, callback) -> permutations = [] recurse = (keys, payload = {}, position = 0) -> # We've finished constructing the permutation, exit call stack if position is keys . length permutation = _ . clone payload callback ? permutation permutations . push permutation return # Grab the current key key = keys [ position ] # There are no values for this attribute, skip it if permutable_attributes [ key ]. length is 0 recurse keys , payload , position + 1 # Otherwise, recurse for each possible value of this attribute else for value in permutable_attributes [ key ] payload [ key ] = value recurse keys , payload , position + 1 recurse Object . keys permutable_attributes return permutations

With our permute algorithm complete, now we can wire up a UI.

A First Iteration

One of the most common pitfalls seen in Angular apps are bloated controllers. It can be tempting to wedge bits of logic here and there, as it's easy at the time. Unfortunately, the controller quickly turns into a tangled mess. Consider the following implementation of our permutation builder:

spaghetti-widget-builder-controller.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 angular . module 'app.widget' . controller 'SpaghettiWidgetBuilderController' , ( $scope $state permutationFactory widgetFactory widgetStore ) -> # Placeholder object to hold references for our ngForm instances $scope.forms = {} ### @name initialize @description Resets state, called if the user hits the reset button. The `do` immediately invokes our method to initialize state when controller first loads. ### do $scope.initialize = -> $scope.permutations = [] $scope.permutable_attributes = name: [] description: [] # For binding to the form with ngModel $scope.attributes = name: '' description: '' # We need to specify which fields are required. We can't just use a # 'required' attribute on the input tag, because it will no longer # be required once at least 1 value has been entered. $scope.required = name: true description: false ### @name buildPermutations @description Private function called every time an attribute is added or removed. ### buildPermutations = -> $scope.permutations.length = 0 # At least we have the `permutationFactory` and `widgetFactory`, # which are more obvious as candidates for separate services. permutationFactory . permute $scope . permutable_attributes , (permutation) -> if widgetFactory . validate permutation $scope . permutations . push permutation ### @name createPermutations @description Persists the permutations and redirects to home page. ### $scope.createPermutations = -> widgetStore . addWidgets $scope . permutations $state . go 'home' ### @name addAttribute @description Adds an attribute to generate permutations from, then empties the form input. @param {String} key - Attribute name ### $scope.addAttribute = (key) -> # Tokenize the value $scope . permutable_attributes [ key ]. push $scope . attributes [ key ] # Then empty the form input $scope . attributes [ key ] = '' buildPermutations () ### @name isRequired @description Used with ng-required, determines if at least 1 value has been entered or not. This view logic would fit much more nicely in a directive. @param {String} key - Attribute name @returns {Boolean} ### $scope.isRequired = (key) -> return ( $scope . required [ key ] and $scope . permutable_attributes [ key ]. length is 0 ) ### @name isDisabled @description We don't want the submit button to be enabled if input is empty. Also a good candidate for inclusion in a directive. @param {String} key - Attribute name @returns {Boolean} ### $scope.isDisabled = (key) -> return _ . isEmpty $scope . attributes [ key ] ### @name removeAttribute @description Removes a permutable attribute, then re-generates permutations. @param {String} key - Attribute name @param {Integer} index - Index in the permutable attribute array. ### $scope.removeAttribute = (key, index) -> $scope . permutable_attributes [ key ]. splice index , 1 buildPermutations ()

spaghetti-widget-builder.jade

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 //- Can you spot all the repeating markup? Just imagine if we had more fields! .container-fluid .row .col-md-12 p .lead Let's build some widgets. .row .col-lg-6.col-md-8.col-sm-6 form .form-group ( ng-submit = "addAttribute('name')" name = "forms.name" ng-class = "{ 'has-error': forms.name.$invalid && forms.name.value.$touched }" ) label .control-label ( for = "widget_name" ) Name* .input-group input .form-control ( id = "widget_name" name = "value" type = "text" ng-model = "attributes.name" ng-required = "isRequired('name')" ) .input-group-btn button .btn.btn-default ( type = "submit" ng-disabled = "isDisabled('name')" ) Add form .form-group ( ng-submit = "addAttribute('description')" name = "forms.description" ng-class = "{ 'has-error': forms.description.$invalid && forms.description.value.$touched }" ) label .control-label ( for = "widget_description" ) Description .input-group input .form-control ( id = "widget_description" name = "value" type = "text" ng-model = "attributes.description" ng-required = "isRequired('description')" ) .input-group-btn button .btn.btn-default ( type = "submit" ng-disabled = "isDisabled('description')" ) Add .col-lg-4.col-md-4.col-sm-6 .panel.panel-default .panel-heading h4 .panel-title {{permutations.length}} Widgets Built .panel-body .panel.panel-default .panel-heading strong Name ({{permutable_attributes.name.length}}) ul .list-group li .list-group-item ( ng-repeat = "name in permutable_attributes.name" ) span {{name}} button .btn.close ( ng-click = "removeAttribute('name', $index)" ) × .panel.panel-default .panel-heading strong Description ({{permutable_attributes.description.length}}) ul .list-group li .list-group-item ( ng-repeat = "description in permutable_attributes.description" ) span {{description}} button .btn.close ( ng-click = "removeAttribute('description', $index)" ) × .panel-footer .btn-group button .btn.btn-primary ( type = "button" ng-click = "createPermutations()" ng-disabled = "permutations.length === 0" ) Submit button .btn.btn-default ( type = "button" ng-click = "initialize()" ng-disabled = "permutations.length === 0" ) Reset

It works! Cool! But, there are a few problems here:

There's poor separation of concerns: view logic and state ( isRequired , isDisabled ) are intermingled with business logic and state.

, ) are intermingled with business logic and state. What if we want to add new features, like permutable images? Or videos? This controller and template will keep getting bigger.

What if we want to add an additional step, to review the permutations we've generated before saving them? Having the data model so tightly coupled to the controller becomes problematic.

What we have isn't very reusable. What if we want to create another permutation builder for Gadgets?

Note that we still have the permutationFactory and widgetFactory, which were more obvious candidates for separate services.

Teasing Out The Layers

By isolating our concerns into separate layers, we can create something that is both easier to maintain and reusable. There are two modes of thinking that I like to employ:

1. Think purely in terms of business logic and state

Without even considering a UI, how would you describe the state of our permutation builder? How would you design an API to manipulate that state?

Consider Flux's idea of a store:

Stores contain the application state and logic. Their role is somewhat similar to a model in a traditional MVC, but they manage the state of many objects — they do not represent a single record of data like ORM models do. Nor are they the same as Backbone's collections. More than simply managing a collection of ORM-style objects, stores manage the application state for a particular domain within the application.

In an Angular app, we can implement something similar with a service, isolating the business logic and state for our permutation builder. This gives us a number of advantages:

It's easier to test,

It becomes easier to reuse and extend, and

We avoid controller bloat, by properly isolating our concerns.

permutation-builder-service.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 ### @name PermutationBuilderService @description A base class that can be extended for use with different permutable resources. ### angular . module 'app.permutation' . factory 'PermutationBuilderService' , ( permutationFactory ) -> class PermutationBuilderService constructor: -> @initialize () ### @name initialize @description Method used to reset service to an empty state. Override this method to define attributes for a permutable resource. ### initialize: -> # Regular attributes that get added to each permutation @attributes = {} # Permutable attributes @permutable_attributes = {} # Collection of permutations @permutations = [] ### @name addAttribute @description Adds a permutable attribute @param {String} key - Name of attribute @param {String|Number|Object|Array} value - Can be of any type @returns {Boolean} Whether attribute was added successfully or not. ### addAttribute: (key, value) -> bucket = @permutable_attributes [ key ] throw Error "Invalid key: ' #{ key } '" unless bucket ? if _ . contains bucket , value console . warn "' #{ value } ' already entered." return false # Add the value to permute against bucket . push value # And generate the permutations @buildPermutations () return true ### @name buildPermutations @description Builds permutations of the resource. @returns {Array} Collection of permutations ### buildPermutations: -> # Empty our collection of permutations from previous runs @permutations.length = 0 permutationFactory . permute @permutable_attributes , (permutation) => # Extend common attributes onto each permutation resource = _ . extend permutation , @attributes @permutations . push resource return @permutations ### @name createPermutations @description Abstract method. Override to define how a permutable resource gets persisted. ### createPermutations: -> ### @name removeAttribute @description Removes a permutable attribute by index @param {String} key - Attribute name @param {Integer} index - Index of value in the permutable attribute array. @returns {Boolean} - true if item successfully removed ### removeAttribute: (key, index) -> bucket = @permutable_attributes [ key ] throw Error "Invalid key: ' #{ key } '" unless bucket ? removed = bucket . splice index , 1 @buildPermutations () return removed . length > 0

2. Think of your UI in terms of a tree of components

Look at your design. Look at your markup. Do you see any patterns? These parts of our UI are ripe for refactoring into directives.

Imagine being able to compose our view as such:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 kc-permutation-builder ( service = "PermutationBuilderService" ) .main kc-permutable-input ( name = "name" type = "text" required ) Name kc-permutable-input ( name = "description" type = "text" ) Description .sidebar kc-permutable-attribute ( name = "name" ) Name kc-permutable-attribute ( name = "description" ) Description button ( type = "submit" ng-click = "submit()" ) Create Permutations

That's a lot more succinct, expressive, and reusable.

permutation-builder-directive.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ### @name kcPermutationBuilder @description This serves as a way to bind an instance of a PermutationBuilderService and expose its API to a group of `kcPermutableInput` directives. Even though it has an isolate scope, it doesn't have any template, so it doesn't introduce an isolate scope in the template in which its used. @param {PermutationBuilderService} service - Or a subclass thereof ### angular . module 'app.permutation' . directive 'kcPermutationBuilder' , -> restrict: 'E' controller: 'KcPermutationBuilderController' scope: service: '=' . controller 'KcPermutationBuilderController' , ( $scope ) -> @permutable_attributes = $scope . service . permutable_attributes @addAttribute = angular . bind $scope . service , $scope . service . addAttribute @removeAttribute = angular . bind $scope . service , $scope . service . removeAttribute

permutable-input-directive.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 ### @name kcPermutableInput @description Encapsulates templating and view logic for a permutable input, which is its own mini form. Makes for a flexible component that can be used to compose the view for any type of permutable resource. @param {String} name - Permutable attribute key @example `kc-permutable-input(name="title", type="text", required) Label` ### angular . module 'app.permutation' . directive 'kcPermutableInput' , -> require: '^kcPermutationBuilder' restrict: 'E' templateUrl: '/permutation/_permutable-input.html' transclude: true scope: name: '@name' link: (scope, element, attrs, kcPermutationBuilder) -> # Ensure input IDs are unique scope.input_id = _ . uniqueId 'permutatable_input_' scope.state = value: '' is_required: attrs . required ? ### @name isDisabled @description We don't want the submit button to be enabled if input is empty. @returns {Boolean} ### scope.isDisabled = -> return _ . isEmpty scope . state . value ### @name isRequired @description An input is no longer required if at least 1 value has already been entered. @returns {Boolean} ### scope.isRequired = -> return attrs . required ? and kcPermutationBuilder . permutable_attributes [ scope . name ]. length is 0 ### @name submit @description Adds attribute and clears the input. ### scope.submit = -> is_added = kcPermutationBuilder . addAttribute ( scope . name scope . state . value ) if is_added scope.state.value = ''

_permutable-input.jade

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 form .form-group ( ng-submit = "submit()" name = "form" ng-class = "{ 'has-error': form.$invalid && form.value.$touched }" ) label .control-label ( for = "{{input_id}}" ) span ( ng-transclude ) span ( ng-if= "state.is_required" ) * .input-group input .form-control ( id = "{{input_id}}" name = "value" type = "text" ng-model = "state.value" ng-required = "isRequired()" ) .input-group-btn button .btn.btn-default ( type = "submit" ng-disabled = "isDisabled()" ) Add

permutable-attribute-directive.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ### @name kcPermutableAttribute @description Displays the values entered for a permutable attribute. Allows user to remove a value. @param {String} name - Permutable attribute key @example kc-permutable-attribute( name="description" ) Description ### angular . module 'app.permutation' . directive 'kcPermutableAttribute' , -> require: '^kcPermutationBuilder' restrict: 'E' templateUrl: '/permutation/_permutable-attribute.html' transclude: true scope: name: '@name' link: (scope, element, attrs, kcPermutationBuilder) -> scope.permutable_attribute = kcPermutationBuilder . permutable_attributes [ scope . name ] ### @name removeAttribute @description Calls on service to remove attribute. @param {Integer} index - Value's index in the permutable attribute array. ### scope.removeAttribute = (index) -> kcPermutationBuilder . removeAttribute scope . name , index

_permutable-attribute.jade

1 2 3 4 5 6 7 8 9 10 11 12 13 .panel.panel-default .panel-heading strong ( ng-transclude ) strong ({{permutable_attribute.length}}) ul .list-group li .list-group-item ( ng-repeat = "attribute in permutable_attribute" ) span {{attribute}} button .btn.close ( ng-click = "removeAttribute($index)" ) ×

The New and Improved Widget Builder

Now with the abstracted modules ready for use, look at how much leaner all of our widget-specific code is.

widget-builder-service.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 ### @name WidgetBuilderService @description Extends PermutationBuilderService for use with widgets. Drives the business logic of the widget builder UI flow. NOTE: injection returns an instance, not the constructor (see end of file). ### angular . module 'app.widget' . factory 'WidgetBuilderService' , ( PermutationBuilderService widgetFactory widgetStore $q ) -> class WidgetBuilderService extends PermutationBuilderService ### @name initialize @description Defines permutable attributes for widgets. ### initialize: -> super @permutable_attributes = name: [] description: [] ### @name buildPermutations @description Here, we extend the base method to ensure that we only build valid widgets. ### buildPermutations: -> super @permutations = _ . filter @permutations , widgetFactory . validate ### @name createPermutations @description Specifies how a permutation gets persisted. @returns {Promise} - Fulfilled with the newly created permutations. ### createPermutations: -> # Grab the permutations before we empty them permutations = @permutations # Implement your AJAX call here widgetStore . addWidgets permutations # Reset our service @initialize () return $q . when permutations return new WidgetBuilderService ()

widget-builder-controller.coffee

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ### @name WidgetBuilderController @description Our controller and its template become a very thin layer that glue the pieces together. ### angular . module 'app.widget' . controller 'WidgetBuilderController' , ( $scope $state WidgetBuilderService ) -> $scope.WidgetBuilderService = WidgetBuilderService ### @name submit @description Calls on the service to create permutations, then redirects to home page. ### $scope.submit = -> WidgetBuilderService . createPermutations (). then (widgets) -> console . log 'look at all these widgets we built!' console . table widgets $state . go 'home'

widget-builder.jade

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 .container-fluid .row .col-md-12 p .lead Let's build some widgets. kc-permutation-builder ( service = "WidgetBuilderService" ) .row .col-lg-6.col-md-8.col-sm-6 kc-permutable-input ( name = "name" type = "text" required ) Name kc-permutable-input ( name = "description" type = "text" ) Description .col-lg-4.col-md-4.col-sm-6 .panel.panel-default .panel-heading h4 .panel-title {{WidgetBuilderService.permutations.length}} Widgets Built .panel-body kc-permutable-attribute ( name = "name" ) Name kc-permutable-attribute ( name = "description" ) Description .panel-footer .btn-group button .btn.btn-primary ( type = "button" ng-click = "submit()" ng-disabled = "WidgetBuilderService.permutations.length === 0" ) Submit button .btn.btn-default ( type = "button" ng-click = "WidgetBuilderService.initialize()" ng-disabled = "WidgetBuilderService.permutations.length === 0" ) Reset

Going Forward

Now, it's trivial to implement a permutation builder for Gadgets. Or, say we wanted to support permutations of images? The surface area for changes needed is minimal: we just need a new kcPermutableImage directive, and the rest would work pretty much as-is.

Neat, huh?