ngShowIf (Feature Request) #8433
Description
Abstract
If is often desirable to insert a directive's content upon first usage, and show/hide thereafter.
That is, have an ngIf
behaviour once, and ngShow
behaviour thereafter.
Current state of affairs
The two directives in question are:
ngIf
- the directive inserts/removes the contents from the DOM. The directive also creates a new child scope upon each insertion (unfavourable behaviour at times, as it forces outside/inside communication to be based on objects, rather than primitives).ngShow
- the directive simply shows or hides the contents. It does not affect the scope.
The request
It would be useful to have a directive that inserts the contents first time its value turns true, and following that simply hides and show it.
Example
Consider a typeahead directive on an input field:
<input typeahead="...">
The directive reveals a dropdown with options and search matches, like so:
The initiation of the dropdown may be time-consuming and its existence on the DOM performance-consuming. The dropdown may:
- Require an AJAX call to obtain the options (in case of remote search).
- Involve watches.
- Have to sort the returned options.
- Each item (and depending on the programmer, there may be hundreds of them):
- Require DOM listeners (eg, mousedown).
- Involve a watch (eg, selected).
On a single form, there may be 10 fields with such typeahead directives. And the user may or may not open the dropdowns. When the form represents a new entry, the dropdowns are likely to be opened; but when editing/reviewing an entry, very few (if any) dropdowns will be open.
And so if one chooses:
ngIf
- quite a bit of work will have to be done every time a dropdown opens (think AJAX requests); and due to the child scope, one has to take some extra step to ensure state preservation (eg, the query that was entered).ngShow
- we do a lot of work (10 typeaheds X 10 AJAX calls and many listeners and watches), which may not be needed at all.
A directive that inserts on the first show but show/hide thereafter would solve this.
Code
ngIfShow
Here's a code for and ngIfShow
directive, essentially a slimed-down marrige between ngIf
and ngShow
. As it uses transclude
the inside scope is a sibling scope (which quite a few people aren't happy with).
.directive( 'ngIfShow', ['$animate', function($animate) {
return {
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
link: function (scope, element, attr, ctrl, transclude) {
var rendered = false,
iClone;
scope.$watch(attr.ngIfShow, function ngIfShowWatchAction(value) {
if (!rendered){
if (value) {
transclude(function (clone, newScope) {
iClone = clone;
clone[clone.length++] = document.createComment(' end ngIfShow: ' + attr.ngIfShow + ' ');
$animate.enter(clone, element.parent(), element);
rendered = true;
});
}
} else {
$animate[value ? 'removeClass' : 'addClass'](iClone, 'ng-hide');
}
});
}
};
}])
ngShowIf
The following directive does the same thing, but behaves more like ngShow
in that it does not affect the scope:
.directive( 'ngShowIf', [ '$compile', '$animate', function( $compile, $animate ) {
return {
priority: 600,
terminal: true,
restrict: 'A',
compile: function compile( tElement, tAttributes ) {
var iContents = tElement.html();
tElement.empty();
return function postLink( scope, element, attrs, controller ) {
var iClone = false;
scope.$watch( attrs.ngShowIf, function ngShowIfWatchAction( doShow ) {
if ( !iClone ) {
if ( doShow ) {
iClone = $compile( iContents )( scope );
$animate.enter( iClone, element );
}
} else {
$animate[ doShow ? 'removeClass' : 'addClass']( iClone, 'ng-hide' );
}
});
};
}
};
}])