Angular directive disposal - javascript

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.

Related

Isolate scopes angularjs, ditching dependency injection, reusable components

I haven't really been fiddling for angularjs's directive for a while and I do not still have a good grasp on it. Before I dive into it, I am thinking react, on how they do their components.
So I have search on how to create reusable components using directives, and found this article:
http://michalostruszka.pl/blog/2015/01/18/angular-directives-di/
But the implementation on his final solution is quite blurry, as I cannot figure out on how to use it correctly.
Let's say I create a title directive:
<epw-title store="epwEventStore">{{vm.title}}</epw-title>
And another directive that uses the same service epwEventStore so that it can update the state
<epw-event-list store="epwEventStore"></epw-event-list>
Where the epw-event-list renders a list and when clicked should change the value of vm.title of the epw-title.
How is this possible?
Update
Q: Are they nested?
A: No, they are siblings.
Don't put Services inside Views
Just to avoid any misunderstanding, if epwEventStore is an AngularJS Service (such as provider, factory, service), it is not meant to be put as attribute value inside your template like that:
<epw-title store="epwEventStore">{{vm.title}}</epw-title>
In a well-designed decoupled architecture, your Views (template) should not "know" anything about your Services. They should only refer to controllers, directives and filters.
Different directives can use the same service
That means you can perfectly have
...
.directive("first", function(myService) {
...
})
.directive("two", function(myService) {
...
})
...
where, e.g., both directive can access and manipulate the same data.
Angular way is declarative
Following Angular's philosophy, if your directive depends on a Service, you declare it in the directive's definition. So whoever reads the code - knows immediately about all dependencies. This makes the code readable.
What the author of the article seems to be suggesting is to use events instead. Which is the publish-subscribe pattern. However, using it inside your directive, especially with $rootScope, makes the directive "leaky" as it is no more encapsulated and can both affect the external state and be affected. And worse - the reader has now manually search all your codebase for whoever is affected by the events emitted from the directive. This pattern has its uses but should be enjoyed with care.

$emit, $broadcast, prototypical inheritance

Still on the basics of AngularJS, i understand the concepts, however, just looking at custom events,
$emit
and
$broadcast
for nested controller..
According to the docs, $emit bubbles the event, ie. passes it up the chain, for nested controllers,
My question, is, how is this different to just simply calling a function from the parent controller (prototypical inheritance). Or have i miss understood something?
The thing is, you can't always be certain that your direct parent, is the one you want to call. It's easy to break your code with that kind of anti-pattern.
And you must agree that
$scope.$parent.$parent.$parent.doSomething();
Is pretty ugly.
Instead you can $emit an event upwards, now it doesn't matter how far up the chain your parent controller is, as long as it is listening and reacting.
This gives you nice loose coupling between controllers, and just acts like a message pump.
The same goes for $broadcast, just downwards instead, and here I would argue that it is even more important.
Take the example of a child controller with many parents or a parent controller with many children. Should the developer need to maintain a list of children within the parent to invoke a function on each of them. $emit and $broadcast are utilities to allow a loose-coupling messaging along the lines of an Observer pattern. If all parents in the hierarchy need to know that a child controller has done some task or needs some task dome on its behalf then is can just generate an event and interested parties can listen.
Data can also be passed removing the need for controllers to share data on the inherited scope.
$emit helps you to pass event to the parent controllers.
You can't use $controller('ParentController', {scope: scope}) in all your child controllers to inherit the properties. To make the code clean and loosely coupled, $emit will help you to achieve that.
Assume you have three levels of hierarchy and you want the child controller to update the value of a particular parent controller. If you are going to do that via prototype chain, you need to inherit that particular controller using $controller('ParentController', {scope: scope}) but $emit will avoid that. You need not know which parent controller. instead just emit the event.
In the appropriate parent controller where you want to read the data, use
$scope.$on("eventname", function(event, data) {
// update value here
});

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 $broadcast and isolate scope

So I have two directives splitter and pane that may be used like so:
<splitter>
<pane></pane>
<pane></pane>
</splitter>
<splitter>
</splitter>
I want them all to have isolate or inherited scope. However I also want to be able to $broadcast (or equivalent) between them so that if I were to $broadcast on one directive's scope, the same event would be triggered on all the nested directives that are listening but not it's parent or siblings (no $rootScope here).
How would one go about doing this? My solution must be future friendly as I will be adding more directives in to the mix which also listen for this event.
To do inter-directive communication, the best is to use the parent directive controller and expose methods in this.
Then you just have to require it in your children directive (require: '^splitter') and the parent controller will be injected as the fourth argument of your link function.
For more information, you can see the official documentation about Creating Directives that Communicate.

Categories

Resources