I ran into some code from a project that looked give or take like this...
namespace.directive('caNavigation', function () {
"use strict";
return {
restrict: 'E',
scope: {
inLinks: "@",
collapseNav: "="
},
// ...
controller:
["$scope", /* more jive */],
function ($scope /* more jive */)
{
$scope.$watch('inLinks', function (value) {
// omgwtfbbq?
// ...
Well, that's @ and = stuff is pretty cryptic, especially since I hadn't used a directive in AngularJS before (Angular 2+, sure, but still learning ye olde AngularJS -- going backwards, I know. Don't forget I had my first run-in with Perl in 2014 [sic]).
Let's see what ye olde docs say:
[Having to have a separate controller to change direct scopes] is clearly not a great solution.What we want to be able to do is separate the scope inside a directive from the scope outside, and then map the outer scope to a directive's inner scope. We can do this by creating what we call an isolate scope. To do this, we can use a directive'sscope
option:...
Let's take a closer look at the scope option://... scope: { customerInfo: '=info' }, //...
The scope option is an object that contains a property for each isolate scope binding. In this case it has just one property:
- Its name (
customerInfo
) corresponds to the directive's isolate scope property,customerInfo
.- Its value (
=info
) tells$compile
to bind to theinfo
attribute.
Good heavens, AngularJS. Not the most discoverable. Seems like in AngularJS in general there's lots of magic stringing. Perhaps I shouldn't be surprised, but that doesn't mean I like it. (Here I mean largely inline array notation, which is too hipster by half. I mean, I get that you don't what to lose information when you minimize, but then how about pass real references, not refer to objects by their original names? Sheesh.* )
But let's go a little deeper so that we know what the @ symbol means too.
If you've used Angular 2+ (what a failed naming scheme, btw. Why not "Angular.IO" after the website or just NGular or something so that googling this jive would be easier?), you can already see our steady progression to banana boxes.The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the directive's element. These local properties are useful for aliasing values for templates. The keys in the object hash map to the name of the property on the isolate scope; the values define how the property is bound to the parent scope, via matching attributes on the directive's element:
@
or@attr
- bind a local scope property to the value of DOM attribute. The result is always a string since DOM attributes are strings. If noattr
name is specified then the attribute name is assumed to be the same as the local name. Given<my-component my-attr="hello {{name}}">
and the isolate scope definitionscope: { localName:'@myAttr' }
, the directive's scope propertylocalName
will reflect the interpolated value ofhello {{name}}
. As thename
attribute changes so will thelocalName
property on the directive's scope. Thename
is read from the parent scope (not the directive's scope).=
or=attr
- set up a bidirectional binding between a local scope property and an expression passed via the attributeattr
. The expression is evaluated in the context of the parent scope. If noattr
name is specified then the attribute name is assumed to be the same as the local name. Given<my-component my-attr="parentModel">
and the isolate scope definitionscope: { localModel: '=myAttr' }
, the propertylocalModel
on the directive's scope will reflect the value ofparentModel
on the parent scope. Changes toparentModel
will be reflected inlocalModel
and vice versa. If the binding expression is non-assignable, or if the attribute isn't optional and doesn't exist, an exception ($compile:nonassign
) will be thrown upon discovering changes to the local value, since it will be impossible to sync them back to the parent scope.By default, the$watch
method is used for tracking changes, and the equality check is based on object identity. However, if an object literal or an array literal is passed as the binding expression, the equality check is done by value (using theangular.equals
function). It's also possible to watch the evaluated value shallowly with$watchCollection
: use=*
or=*attr
But for now, what a mess.
- If a scope property is a string that starts with @, then we'll initialize that scope property with the directive's DOM attribute that matches what comes after the @.
- That is, this is a one-way process. Props down, and set up
events if changes need to go back up.
- If the prop starts with an =, we're doing the equivalent of
Angular.IO's banana boxing, it appears.
- So if we change whatever's in here, the parent is changing
too.
- The
listener
is called only when the value from the currentwatchExpression
and the previous call towatchExpression
are not equal (with the exception of the initial run, see below). Inequality is determined according to reference inequality, strict comparison via the!==
Javascript operator, unlessobjectEquality == true
(see next point)- When
objectEquality == true
, inequality of thewatchExpression
is determined according to theangular.equals
function. To save the value of the object for later comparison, theangular.copy
function is used. This therefore means that watching complex objects will have adverse memory and performance implications.
Guess that works. Let's ignore the =* shallow comparison jive for now. All we've really got is one prop called inLinks that will be passed... um... "in" that we'll update whenever it changes, probably after the page finishes loading and making some resource calls. And sure enough...
<li ng-repeat="subLink in link.SubLinks|filter: {ShowInNav: true}" ...
That means once we've loaded the correct navigational links for this context, we'll start pushing them into the DOM.
And our code makes sense. Much rejoicing.
* I actually kinda like AngularJS. It's one clean step away from the inefficient, overly-engineered land of enterprise development Bjarnason discusses and that at least the social convention surrounding Angular 2+ seems to require. But there are a few stupid* conventions like this one that really do AngularJS in conceptually.
* Sorry, I've tried to come up with a good synonym, but "stupid" fits here. My earlier use of "hipster" was my kind attept at an alternative, but this stuff really feels a little too much like a first-pass solution to be in a nice, mature templating library.