I am wondering if is possible to unbind/destroy ng-if once its value is true?
I created tree structure with recurrence directive and each of branches has <div ng-if="visible"> which keeps tracking if element needs to be rendered. The problem is that the solution increase number of watchers because every ng-if creates new one. Once the ng-if expression became true it won't change so watch can be removed, is there any way to "destroy" ng-if in that case?
if using angular 1.3+ you can do
<div ng-if="::visible">
Which will remove the expression from angular watchlist and essentially no watches on it. Use this for any single binded expressions in your app. Keeps your watch count low and digest cycles faster.
You could use bindonce library, which happens to have a bo-if = "condition" directive attribute, which is described as:
equivalent to ng-if but doesn't use watchers
So is somewhat similar of what you want to achieve.
Related
As I've recently found out, ngSwitch acts a bit like ngIf, in that it outright removes elements from the DOM rather than just hide them. Now in my application this strikes me as both needlessly expensive and potentially problematic down the line (if I need to access properties of my hidden DOM elements). At the same time I like the cleanliness of the ngSwitch syntax as opposed to a bunch of different ngShow directives (which would not allow me to include a 'default' behaviour either). Is there any way I can modify the way this directive works to have it merely hide elements, not remove them altogether? Thanks.
OK, I understood better your need and I have the same need before.
I advice you to use ngSwitch with custom directives like this :
<div ng-switch="mode">
<directive-a ng-switch-when="a"></directive-a>
<directive-b ng-switch-when="b">...</directive-b>
<directive-c ng-switch-default>...</directive-c>
</div>
and in the definitions of your directives directiveA, directiveB and directiveC you use the templateUrl wich will use the cache from the second call of the directive.
The second approach is to use ng-show on every directive but I prefer the ng-switch because it let the dom lighter.
I know this is a bit old but I faced the same issue today and I managed to cache segments views by not using ngSwitch but directly the [hidden] attributes with the segment condition this way :
<ion-segment [(ngModel)]="segmentName">
<ion-segment-button value="profile">
Profile
</ion-segment-button>
<ion-segment-button value="friends">
Friends
</ion-segment-button>
</ion-segment>
<page-profile [hidden]="segmentName != 'profile'" ></page-profile>
<page-friends [hidden]="segmentName != 'friends'" ></page-friends>
Just replace ngSwitchCase by the [hidden] conditional
No, there isn't any way to keep nodes hidden as ngShow and ngHide do if you use ngSwitch. Only the ngSwitchWhen node will be created if its condition is true, all others are commented.
The advantage of ngSwitch is that the dom will be lighter.
One of my mates in work show me a really weird behavior of one-time binding in angular.
UseCase:
When you have an element which text is binding by that one-time binding inside block which is conditional by ng-if, then if we change that value, for example adding some letters, and later change the condition of ng-if, and after that the value from one-time binding has been refreshed.
HTML:
<div ng-if="a" class="blue">{{ ::text }}</div>
It is a kind of bug, or expected behaviour?
Here is an example of what I'm doing: http://codepen.io/samot/pen/rLJAdN
If the condition of ng-if is made false and then true, it will recreate its contents, causing the one-time ng-bind directive to be evaluated again.
The only thing one time binding does is to avoid adding a watch on your expression, but it doesn't "cache" or "store" the result for the case the content of a directive is compiled again.
So it's expected behavior.
Works as expected because using ngIf directive the code is completely recompiled. If you use ngShow the behavior changes and code behaves as you are expecting from ngIf. This because code is only hidden and not recompiled, so not causing the re-(first)-evaluation of the variable.
I'm creating a custom Angular directive for a slide in menu which needs to watch a couple of attributes and one of those attributes needs to be two way bound to the main controller scope (sometimes). However, sometimes the attribute will not be added by the developer so it needs to be added automatically and set to the default (false). So, the directive can be used like this.
<slide-menu position="right" is-open="menuIsOpen"></slide-menu>
or like this:
<slide-menu></slide-menu>
When used the first way the main controller will be able to open and close the menu by changing the value of the boolean $scope.menuIsOpen.
When used without supplying the is-open attribute it should default to false and is obviously used internally and by a child toggle directive.
An additional complication is that whether the attribute is supplied by the developer or not it should exist in the DOM. so in the second example above the directive would set itself to false by default and add the attribute is-open="false" to the DOM?
The reason for requiring is-open="false/true" in the DOM at all times is that the menu is actually operated using CSS tansitions which use the following selector:
slide-menu[is-active="true"]{
// Slide the menu in using transforms/transitions
}
There is a jsfiddle here which shows how far I have got.
http://jsfiddle.net/jonhobbs/gEPvE/
Obviously it doesn't work, but it shows how I have tried to set a default and how I have tried to use # and & on the isolated scope for a one time binding (the menu position) and a 2 way bound expression for the is-open variable.
I'm clearly a long way from achieving what I need but any advice would really be appreciated.
Have a look at this fiddle http://jsfiddle.net/gEPvE/38/
I took the one you started and updated it to act like you specified.
You can make a two way binding value optional by adding a ? on the scope definition.
Like this
{
scope: {
'isOpen':'=?'
}
}
Now the is-open attribute is optional.
Then you can set the default value in the directive controller, like you had started to do.
Next, in order to synchronize the DOM attribute with the scope value you can use $watch.
$scope.$watch('isOpen', function(val) {
$element.attr('is-open', val);
});
Finally, I changed the second 'slideMenuToggle' directive to wrap/transclude its element in order to add an ng-click handler. This is mainly to avoid any nastiness with calling $scope.$apply yourself.
Let me know if that works for you.
EDIT
Answering your question in the comment, you can pass a value directly without having it be bound to the scope, you just need to wrap the value in quotes.
For example
<div ng-controller='ctrl'>
<hello world='imOnScope'></hello>
</div>
Assuming 'hello' is a directive with a scope of 'world': '=?' then angular will assign a reference to the parent scope's 'imOnScope' object to the directive's $scope.world member, allowing a two way binding scenario.
To just provide a value directly you may do something like this
<div ng-controller="ctrl">
<hello world="'directValue'"></hello>
</div>
In this scenario angular will just assign 'directValue' to the directive's $scope.world member.
You need to add ngTouch to your module.
var app = angular.module('app', ['ngTouch']);
And add this script:
http://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-touch.js
The reason for requiring is-open="false/true" in the DOM at all times
is that the menu is actually operated using CSS tansitions which use
the following selector
Forcing directive attributes to be appropriate for css selectors is terrible idea. As you correctly stated, they are for developers. So add a class to the element dynamically.
It seems that you're misusing &, it would be ok to set up a callback, but since you don't do this, in its current state you can end up with one-way # with confidence.
I guess it can be something like this (just added ngTouch and ng-controller for parent scope).
You could replace
$scope.watch('isOpen', function () {
$element.toggleClass('opened', $scope.isOpen);
});
with
$scope.watch('isOpen', function () {
$attrs.isOpen = !!$scope.isOpen;
});
and get the behaviour you're asking for, easy as that. Ok, it is boolean now, and it reflects the scope, and you can use [is-open=true] selector. But guess what will happen with your binding? Broken. Fortunately, you can do
$scope.watch('isOpen', function () {
$element.attr('is-open', !!$scope.isOpen);
});
instead. Voila, we tricked Angular because it doesn't look after jqlite. But what will will happen with the binding when the directive will be re-compiled for any reason? Again, isOpen's binding is non-existing 'true' or 'false' scope variable. Broken.
It seems that getting an element in AngularJS is a bad idea, i.e. doing something like:
$('.myElement')
in say, a controller is not an angular way of doing things.
Now my question is, how should I get something in angular?
Right now, what I'm doing (and is an accepted way of doing it) is by watching a variable, and my directive does something based on it.
scope.$watch('varToWatch', function (varToWatch) {
if(attrs.id == varToWatch)
{
//Run my Directive specific code
}
});
However, while this particular design works for most cases, watch is an expensive operation, and having lots of directives watching can really slow down your application.
TL:DR - What is an angular way of getting a directive based on a variable on the directive? (like the one above)?
If you want to get/set values you don't need to fetch the element using jQuery. Angular data binding is the way to do it.
directives is the way to go if you want to do animations or any kind of element attributes and DOM manipulation.
Your code is basically right; the directive should watch something in the $scope and perform it's logic when that thing changes. Yes, watch statements are expensive, and that is a problem once your number of watches start to approach ~2000.
Looking at your code though, I see one problem:
The variable $scope.varToWatch references an id in the template.
When this variable changes, you want something to happen to the element which has this id.
The problem here is in the first point: The controller should know nothing about the DOM, including the id of any element. You should find another way to handle this, for example:
<div my-directive="one"> ... </div>
<div my-directive="two"> ... </div>
<div my-directive="three"> ... </div>
...etc
And in your directive:
scope.$watch('varToWatch', function (varToWatch) {
if(attrs.myDirective == varToWatch)
{
// Run my Directive specific code
}
});
You are very vague as to what you're trying to achieve, but I'll try to answer in context of your last comment.
I have a lot of the same directives (therefore the code will run on all of them), but I need to get only one directive from the lot.
You talk a lot about getting the right element. The directive element is passed to the link function in the directive. If you are not using this element (or children of it) directly, but rather trying to search for the element you want somehow, you are most likely approaching the problem the wrong way.
There are several ways to solve this, I'm sure. If you're thinking about animations, there is already support for that in Angular, so please don't try reinvent the wheel yourself. For other logic, here are two suggestions:
Secondary directive
If the logic you want to apply to this directive is generic, i.e. it could be applied to other directives in your application, you could create a new directive which works together with directives. You can set prioritization in directive in order to control which directive is executed first.
<main-directive ... helper-directive="{{condition_for_applying_logic}}"></main-directive>
jsFiddle example
Expanding main directive
If the logic is tightly coupled to this directive, you can just create a new attribute, either dynamic or static, and bind to it in the directive. Instead of checking 'attrs.id == varToWatch', you check if $scope.apply-logic === 'true' and apply the logic then.
<main-directive ...></main-directive> <!-- Not applied here -->
<main-directive apply-logic="true" ...></main-directive> <!-- Applied here -->
<main-directive apply-logic="{{some.varOnScope}}"...></main-directive> <!-- Conditional -->
Please comment if something is unclear.
What is the proper way to dynamically add or remove directive from compiled and linked element?
I have a page that has bunch of inputs there (the list is pretty long, so i want to come up with a general solution). What i want to do is to disable all the inputs if specific flag set. I can do this by using jQuery's element.prop('disabled', true).
The problem of such approach is that if any of inputs have ng-disabled or ng-enabled directives attached, then on any their expression modification they will override previously set 'disabled' property. But I want them to not override my global flag.
I came up with the solution to add another bunch of watchers for ng-disabled or ng-enabled expression, but it seems to be not the best approach.
What I want to do, is to remove most of directives attached to the element and set appropriate attributes myself. But if I recompile and relink the element, and then replace it in the document, then I will get a memory leak, as the old element will be de-attached from the DOM document tree, and will remain in memory. I cannot destroy element's scope either, because those elements basically use whole page's main scope.
You can try something like
<div ng-show="someBoolean" >Some text or nested element</div>
or instead of "someBoolean" you can attach a function that resolves to a boolean. To set your boolean you could attach a ng-click to your input that updates your model/boolean value
<button type="button" ng-click="setBoolean()">Some text or nested element </button>
Because of angulars two way data binding the ng-show will be updated upon completion of the next digest cycle