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);
}
};
});