When should I use an isolate scope in Angular? - javascript

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>

Related

ng-init vs function in controller, which is better practice when using $parent?

I have a Bootstrap modal which corresponds to a ng-repeat list. I would like to access the ng-repeat scope data from its parent, which contains the modal. I do this so that when the modal button is clicked on the list, the corresponding data from the JSON appears in the modal.
I have found 2 ways of doing this, and I wonder which is the best alternative?
Method 1
View:
<li ng-init="myFunction(item,$parent)" ng-repeat="item in data.webapps_1.items>
Controller:
$scope.myFunction = function(item,parent){
parent.selected=item.counter-1;
};
Method 2 View:
<li ng-init="$parent.selected=item.counter-1" ng-repeat="item in data.webapps_1.items>
With nothing in the controller.
I have read in the Angular ngInit docs that
The only appropriate use of ngInit is for aliasing special properties
of ngRepeat, as seen in the demo below. Besides this case, you should
use controllers rather than ngInit to initialize values on a scope.
But the list of special properties of ngRepeat does not include $parent.
So, which is the better practice? Including the expression $parent.selected=item.counter-1in the controller or in ngInit directive?
Thanks in advance
Either of the two fine really, so long as you're consistent. Depends on the scale of the app though.
IMO if the app is going to be large you'll want to go the function way, to better adhere to the whole MVC philosophy of decoupling and separation of concerns (http://victorblog.com/2013/03/18/angularjs-separation-of-concerns/).
ng-init="myFunction(item,$parent)"
It's a better structure because you want to keep most of your business logic in the javascript controllers, not in the view.
Personally, I prefer the ng-init approach. Mainly for consistency of the timing of the call (i.e. more like an "onLoad" event). I understand the concerns regarding SoC and that makes sense as well. However, I have seen some specific scenarios (particularly pages with a lot of directives), when changing scope variables with the = binding in the (non ng-init) function, caused extra (premature) digest loops. Obviously it depends on what you're actually doing in the function as well as your scope bindings, but in my opinion its best to just avoid the situation and use the ng-init directive across the board. Your experience may obviously vary wildly. Like others said just pick an approach and stick with it and be aware of the impact of every call that you're making in the function itself. :)
EDIT: Regarding the accepted answer, the example was used passing values to the init function in ng-init. And hes correct that is a violation of the pattern. I typically use parameterless init functions and call it like:
ng-init="model.init()"
Any reference to $scope or what have you would happen inside the function itself. I rarely pass anything to init, unless its a static value. For example in several cases on an ASP.NET WebApi project, I wanted minimize round trips to the server so I would resolve a value from the MVC View Model, which is just rendered and passed as a string like so:
ng-init="model.init(#model.myValue)"
In those cases its usually best to keep it to simple value types (i.e. string, int, etc), but I have occasionally for small directives such as one that does nothing but display a dropdown, I have passed arrays to pre-populate the dropdown binding. Those cases are extremely situational and this one in particular would obviously only work when using server side rendering of the template.

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

AngularJS GlobalCtrl vs $rootScope vs Service

I am confused on a couple of things with Globals in Angular. Below is my pseudo code.
1) With the way I have my GlobalCtrl placed, I am able to reference my $scope.modalOptions from all of my controllers. That being the case, I'm confused as to why I see people adding global properties to $rootScope instead of just adding them like I am doing here. Is that just in case they want to inject it into a service or something?
2) Should I be using a service instead of adding properties and methods to my GlobalCtrl? If so, why?
<div ng-app="app" ng-controller="GlobalCtrl">
<div ng-view></div>
</div>
function GlobalCtrl($scope, $location) {
$scope.modalOptions = {
backdropFade: true,
dialogFade: true
};
}
The 'Main Controller' approach is definitely preferable to using $rootScope.
Scope inheritance is there, so why not leverage it. In my opinion, that solution works well for most cases, i.e. unless you need to have a parallel controller somewhere (that wouldn't be a child of Main). In that case, the best way to go is to use a service and inject it where needed. Services (or rather factories, because that's what you'll probably be using -- read more about them here) are singletons and work well for sharing data across controllers.
Important to know about scopes
Scope inheritance is pretty much regular JavaScript inheritance at play. You should tend to use objects for your data, because they are passed by reference.
If you have a primitive like $scope.myString = 'is of a primitive data type'; in your parent controller and try to overwrite the value in a child controller, the result won't be what you'd expect -- it will create a new string on the child controller instead of writing to the parent.
Suggested reading here
Final thoughts
If you are using the nested controllers approach, do not forget to still inject $scope (and other dependencies) in the child controller. It might work without, but it's slower and hard to test, and last but not least -- the wrong way to do it.
Finally, if you have a lot of state variables to keep track of and/or a lot of usage points, it's definitely a good idea to extract them into a service.
Generally speaking global variables are considered bad practice, as they don't encourage encapsulation, make debugging difficult, and promote bloated code. Here's a good discussion of global variables: http://c2.com/cgi/wiki?GlobalVariablesAreBad.
A good rule of thumb is to add properties and methods to the most local scope possible and use services to share data between modules.

Directives callbacks and scope

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.

Categories

Resources