Directives callbacks and scope - javascript

I'm trying to wrap my head around scopes in angularjs, particularly when it comes to invoking callbacks on the module that is using a directive. I've found 3 different ways to accomplish the same thing and I'm trying to understand the pros/cons to each approach.
Given this plnkr
When is it appropriate to use &, =, or calling functions directly on the parent?
I prefer binding with the '=' sign, as there is less code required in the directive and in the module hosting the directive, but according to the documentation (Understanding Transclusion and Scopes) it seems that binding to callbacks with the & is the preferred method, why?

Good question. These kinds of decisions should be made first from the perspective of trying to separate your concerns. So we have to eliminate calling a method on the parent scope - the directive has to know too much about the parent.
Next, we look at purpose. Callbacks are methods by definition. & evaluates an expression in the context of the parent scope, while a bi-directional binding is just a variable name. & is a lot more powerful and gives the user of your directive more flexibility. Sure, they could do this, like in your example:
<my-dir cb="callMe()"></my-dir>
But they could also do this:
<my-dir cb="myVar = false"></my-dir>
We don't have to pass in the name of a variable - it's any AngularJS expression. So the user of the component is free to react to your event in any way that suits them. Awesome!
But also, the directive can react to state changes. For example, you could check for a condition:
<my-dir cb="myVar"></my-dir>
And myVar can evaluate to any value and your directive can be made aware anytime this changes and react accordingly. Instead of sharing a variable, you're sharing an expression. In this case, a bi-directional binding would work, but if the directive doesn't (and maybe shouldn't) change that variable, why do we need a two-way binding?
But again, it needn't be a variable. How about a expression that evaluates to true or false?
<my-dir cb="myVar == myOtherVar"></my-dir>
Our directive doesn't have to care how the parent scope arrives at a value - only what the value eventually is.
So we can use it for the parent scope to react to the directive or for the directive to react to the parent - all with flexible expressions. Neat.
So, = is for ensuring data binding; it ensures that the scope where the directive was used and the directive itself stay in sync on a certain variable. & allows for evaluating expressions in the context of the parent scope and for either the directive or the parent scope to react to potentially complex state changes.

Related

Angular directive disposal

I'm trying to figure out what would be the best way to perform cleanup on different angular directives.
I have different types of directives, some do not define their own scope, some have an isolated scope, and some have a child scope.
I need a generic mechanism that will take care of the cleanup in a separate component that my directives use.
So basically I'm looking at two different options, either register on angular's element.on('$destroy', function() {...}) or on jquery's scope.$on('$destroy', function () {...}).
Here's the problem:
If I register on the underlying element destruction then I miss destruction of directives that they're element was not destructed (not sure exactly how's that possible, noticed it via testing...).
If I register on the underlying scope destruction then (I think) I miss destruction of directives that they're element was destructed, for instance when the directive is not defining its own scope and is using its parent scope.
Looking at angular's directives documentation I came across this:
Best Practice: Directives should clean up after themselves. You can use element.on('$destroy', ...) or scope.$on('$destroy', ...) to run a clean-up function when the directive is removed.
There's no mention as far as I can tell about which strategy to use when.
Also, looking at Angular's documentation I came across this:
When child scopes are no longer needed, it is the responsibility of the child scope creator to destroy them via scope.$destroy() API.
I don't understand the scenario in which I am supposed to call the scope.$destroy() API on my own.

Why can't multiple directives ask for an isolated scope on the same element?

If Angularjs - Multiple directives on element with one being isolate scope is right, the isolated scope is bound to the directive, so why would there be any clashes? The documentation for this error states that processing them would result in a collision or an unsupported configuration. I don't buy this. Multiple directives already share the element's scope, which is surely where clashes/unsupported configurations would come in. I've tried looking for the "why" on this, but have come up empty handed.
Can someone explain / give an example where this would indeed create a collision or an unsupported configuration?
Why can't multiple directives on the same element get separate isolated scopes
The answer is simple - there needs to be only one scope to bind the child elements to (see source), because assignments to scope properties done in descendant elements need to have a clear target. The rest is a question of wording.
While it is appropriate, in a way, to refer to the isolate scope being created "for that particular directive" (as the linked answer does), it is only in the sense that the directive that requested the isolation is the only one of the directives on that element to have access to the isolated scope. So, the scope is created to isolate the directive and the child elements from the rest of that "level" of DOM.
Why can't multiple directives get the same isolated scope
Giving multiple directives the same isolated scope would risk a clash of scope binding configurations (multiple directives could try to bind to the same property on the isolated scope).
Why can't directives with lower priority use the isolated scope
A simple and compelling argument is that the {{interpolated.expressions}} on an element need to be evaluated against the same scope as plain expressions (supplied to directives that support them), otherwise the whole thing would be a total mess. (Interpolation of {{expressions}} is done separately, so a directive accepting a plain expression in one attribute and String in another could be configured with expressions evaluated against different scopes.)
If they really need to, they can access the isolated scope (but this needs Debug Data to be enabled). If they have lower priority than the directive creating the isolate scope, they can just use element.isolateScope() in their linking function (see demo).
This is likely because scopes are associated at an element level (AFAIK). Thus, at a given element, there is only one scope associated with it, which can be one of Parent, Child or Isolated. The Scope documentation on the AngularJS Guide also references this (https://docs.angularjs.org/guide/scope)
No element can have more than one scope associated with it (by design), because scopes represent the application structure as well as the context for any given element.
Because of this, when two directives on the same element ask for an isolated scope, AngularJS recognizes it would have to create two different scopes catering to the exact same element, which is not a supported behavior. The only way would then be to somehow merge the two scopes to allow for its basic assumption, which can cause collisions if the two isolated scopes both ask for binding to the same scope variable like
scope: { myData: '=' }
Now if both directives ask for this, or add certain functions to the scope, then you are into uncharted territory depending on which directive executes first.
One reason could be that the function isolateScope(), which is useful in unit testing directives, returns the isolated scope associated with an element. Allowing multiple isolated scopes would mean this function would be more complicated, having to return perhaps some sort of hash of directive-name to isolated scope pairs.
Whether this is enough to justify the design decision to not allow multiple isolated scopes, I have to admit I'm not sure...

AngularJS: Passing data from a transclude directive to the isolated scope of a sub-directive

Based on this Plunker: http://plnkr.co/edit/GufJjrn3OxYVSf2oLD5n?p=preview
I have two directives, for the sake of simplicity, let's name them directiveBlue and directiveRed.
directiveRed has to be a sub element of directiveBlue.
The MainCtrl of our mini app has a simple array under the variable $scope.elements.
This variable is passed to the isolate scope that directiveBlue creates via the data-elements attribute. Notice that the directiveBlue has to be a transclude directive.
Then my main problem is, how do I pass the array of elements to the directiveRed without having to get it doing it via $scope.$parent.elements which seems to me, is a bad practice and then it makes the code tightly coupled.
Any changes to the elements in the deepest directive should then be synched with the rest of the scopes.
Is there any good practice or valid solution for this?
Thanks!
EDIT:
To be more concrete on my use case:
I've created a plunker (http://plnkr.co/edit/i2Busz6E8ehlkG3uEllh?p=preview) with a more concrete situation, where I want to have directive for an action group, I've implemented an option as a simple directive and I want to place my logic in the directives controllers. The method selectAll is pretty simple, but I can imagine having more complex actions which would require the elements from the top scope.
there are plenty of solutions, but without knowing what your goal is, it is more or less guessing.
The following quote is from the angular docs for $compile and describes the use of controller
(...)The controller is instantiated before the pre-linking phase and it is shared with other directives (see require attribute). This allows the directives to communicate with each other and augment each other's behavior. The controller is injectable(...)
a fork of your plnkr to show how to access MainCtrl's $scope.elements in directiveBlue and directiveRed

When should I use an isolate scope in Angular?

In the AngularJS guide it says:
As the name suggests, the isolate scope of the directive isolates
everything except models that you've explicitly added to the scope: {}
hash object. This is helpful when building reusable components because
it prevents a component from changing your model state except for the
models that you explicitly pass in.
...which sounds great. It seems like the best practice would be to try to use an isolate scope in all your directives to keep them encapsulated.
However, I've found that if I try to add two directives with an isolate scope to the same element, Angular errors saying I can only have 1 directive with an isolate scope per element. That seems extremely limiting, and sort of defeats the purpose of an isolate scope.
To me, you shouldn't have to worry about whether or not your directive will be used with other directives when deciding whether or not your scope is isolated.
Am I missing something?
Should I be defaulting to non-isolate scopes? If so, is there a rule of thumb when I should be isolating my scope?
Generally, isolate scopes should be used on stand-alone directive - those directives that can't (and shouldn't) be modified by anything but themselves, no external influences, no add-ons (this includes modifying them by using another directive on their element, of course).
Why is this an issue?
Say you have two directives with isolate scope on your element. Each of them has a property called cdmckay:
<directive1 directive2 ng-show="cdmckay"></directive1>
Which one would have a higher priority, the one from directive1 or the one from directive2? Should the element be shown or not?
There's no way to know whether the used value will be from the directive one or the directive two. I guess it could be possible to combine multiple isolate scopes into a single one if they don't share property names (and only throw errors if they do), but I doubt Angular will ever take that path.
The way around this, in case your directives don't complement each other, is to use parent-child elements, so instead, say:
<directive1 directive2></directive1>
you do this:
<directive1>
<directive2></directive2>
</directive2>
I know it just doesn't seem right in many cases, but if you use isolate scope, this is one of the ways to combine directives. But again, if you expect directive1 to be extended with something, then build it that way (one of the ways would be to avoid using the isolate scope on directive2 then).
If you post your example, people will probably be able to give less generic answers. I hope this will get you moving in the right direction.
Brett's double-widget comment is spot-on:
If I have a calendar widget and a listview widget (that are implemented as directives with isolated scope), why on earth would I apply them to the same element?
I think you can get around this by putting them on separate elements.
<div my-custom-dir1 with-some-property="1"><span my-custom-dir2 with-some-property="2"></span></div>

AngularJS: What is the best practice to make communication between components?

I have a problem reasoning about components communication.
The main question I tried to reason about and failed: what should I use - $watch or $on ($broadcast/$emit) to establish the communication between components?
I see three basic cases:
Controller + Directive. They communicate naturally using the $scope bidibinding. I just put the service, which incapsulates some shared state, in the $scope using some object ($scope.filter = {}).
This approach seems very reasonable to me.
Controller + Controller. I use the DI to inject singleton services with incapsulated state to communicate between controllers. Those services are bounded to directives using the previous approach. This gives me the data binding out-of-the-box.
Directive + Directive. This is the blind spot in my reasoning. I have directives, that reside in different scopes, in the same scope, etc.
I have directives that must reflect all changes (think about slider + charts) and directives, that must trigger the http request (think about select input).
So, the questions are:
What should I use - $watch or $on ($broadcast/$emit) to establish the communication between components?
Should I tend to use $watch in directive-to-directive communication?
Or should I tend to use $broadcast in directive-to-directive case?
Is it better to share the state using injection+binding or injection+events?
I think this depends on the use case for your directives/components. If you expect to be able to re-use a component without having to modify the scope that the component lives in then using broadcast/emit/on would make more sense. IMO if a component internally has some information that I want to be able to retrieve and do different things with, then the broadcast/emit/on scheme makes the most sense.
If on the other hand I need to trigger some service calls in response to something in a directive or I want to share state between a couple of views I end up using a service.
As noted in the comments another alternative that exists is using the require property in the directive definition object:
require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent']
require - Require another directive and inject its controller as the
fourth argument to the linking function. The require takes a string
name (or array of strings) of the directive(s) to pass in. If an array
is used, the injected argument will be an array in corresponding
order. If no such directive can be found or if the directive does not
have a controller, then an error is raised. The name can be prefixed
with:
(no prefix) - Locate the required controller on the current element.
? - Attempt to locate the required controller, or return null if not found.
^ - Locate the required controller by searching the element's parents.
?^ - Attempt to locate the required controller by searching the element's parents, or return null if not found.
This can be useful in cases where you're creating a "compound component" where multiple directives make more sense than trying to encapsulate all of the functionality into one directive, but you still require some communication between the "main/wrapping directive" and it's siblings/children.
I know this isn't a clear cut answer but I'm not sure that there is one. Open to edits/comments to modify if I'm missing some points.

Categories

Resources