Skip to content

Instantly share code, notes, and snippets.

@NickHeiner
Last active December 14, 2015 12:28
Show Gist options
  • Save NickHeiner/5086302 to your computer and use it in GitHub Desktop.
Save NickHeiner/5086302 to your computer and use it in GitHub Desktop.
op-attr usage

Our QA relies on data-test- attributes for their automated testing.

<span data-test-user-name="Bob">Hi, Bob</span>

In our angular app, we wanted to be able to set data-test- attributes dynamically:

<span data-test-{{key}}={{value}}>{{ 'greeting.prefix' | resolveMessageProperty }}{{value}}</span>

(resolveMessageProperty is a filter that takes a message property key and looks up the corresponding value.)

However, angular doesn't let you put angular expressions in the name of html attributes. Thus, we developed op-attr (op- is our internal equivalent of ng-.) It lets you do the following:

<span op-attr="{name: 'data-test-' + key, value: value}">
    {{ 'greeting.prefix' | resolveMessageProperty }}{{value}}
</span>

which then compiles back to:

<span data-test-user-name="Bob">Hi, Bob</span>

The full code of op-attr.js:

angular.module('opower').directive('opAttr',
    function() {
        return {
            link: function(scope, elm, attrs) {
            
                // `attrs.opAttr` is the value that the user specified as
                //     op-attr="{foo: bar}"
                // We want to watch for that value changing, so we can 
                // update the element we're on accordingly.
                // When we specify a function as an argument to $watch,
                // that function will be called on every digest,
                // so it must be fast and idempotent.
                scope.$watch(function() {
                        // scope.$eval will evaluate `attrs.opAttr`
                        // as an angular expression, given `scope`
                        // as the context
                        return scope.$eval(attrs.opAttr);
                    },
                    
                    // `scope.$watch` passes the new value and old value
                    // of evaluating our watch function
                    // so we know what to add and remove
                    function(newVal, oldVal) {
                        // coerce oldVals and newVals into a singleton array 
                        // if they are just a single value
                        var oldVals = [].concat(oldVal),
                            newVals = [].concat(newVal);

                        // wipe out the old attrs
                        angular.forEach(oldVals, function(attr) {
                            angular.element(elm[0]).removeAttr(attr.name, '');
                        });
                        
                        // add the new attrs
                        angular.forEach(newVals, function(attr) {
                            if (!angular.isDefined(attr.pred) || attr.pred) {
                                angular.element(elm[0]).attr(attr.name, attr.value);
                            }
                    });
                    
                // !! It is important to pass `true` here,
                // because it means that angular will use 
                // structural equality rather than referential equality.
                // Because our watch function returns a new object each time,
                // referential equality will never detect that they are the same,
                // and this function will be called over and over again until
                // we hit the digest limit.
                }, true);
            }
        };
    });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment