Dynamic data-test from opower

Our QA relies on data-test- attributes for their automated testing. In our angular app, we wanted to be able to set data-test- attributes dynamically. 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-.)

by angularjsdc

HTML

<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.1/angular.min.js"></script>
<div ng-app="myApp">
    <span op-attr="{name: 'data-test-' + key, value: value}">
        {{ 'greeting.prefix' | resolveMessageProperty }}{{value}}
    </span>
</div>

JavaScript

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,
              ...