This repository has been archived by the owner on Apr 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ngIf): add directive to remove and recreate DOM elements
This directive is adapted from ui-if in the AngularUI project and provides a complement to the ngShow/ngHide directives that only change the visibility of the DOM element and ngSwitch which does change the DOM but is more verbose.
- Loading branch information
1 parent
8a2bfd7
commit 2f96fbd
Showing
4 changed files
with
276 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
'use strict'; | ||
|
||
/** | ||
* @ngdoc directive | ||
* @name ng.directive:ngIf | ||
* @restrict A | ||
* | ||
* @description | ||
* The `ngIf` directive removes and recreates a portion of the DOM tree (HTML) | ||
* conditionally based on **"falsy"** and **"truthy"** values, respectively, evaluated within | ||
* an {expression}. In other words, if the expression assigned to **ngIf evaluates to a false | ||
* value** then **the element is removed from the DOM** and **if true** then **a clone of the | ||
* element is reinserted into the DOM**. | ||
* | ||
* `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the | ||
* element in the DOM rather than changing its visibility via the `display` css property. A common | ||
* case when this difference is significant is when using css selectors that rely on an element's | ||
* position within the DOM (HTML), such as the `:first-child` or `:last-child` pseudo-classes. | ||
* | ||
* Note that **when an element is removed using ngIf its scope is destroyed** and **a new scope | ||
* is created when the element is restored**. The scope created within `ngIf` inherits from | ||
* its parent scope using | ||
* {@link https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance}. | ||
* An important implication of this is if `ngModel` is used within `ngIf` to bind to | ||
* a javascript primitive defined in the parent scope. In this case any modifications made to the | ||
* variable within the child scope will override (hide) the value in the parent scope. | ||
* | ||
* Also, `ngIf` recreates elements using their compiled state. An example scenario of this behavior | ||
* is if an element's class attribute is directly modified after it's compiled, using something like | ||
* jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element | ||
* the added class will be lost because the original compiled state is used to regenerate the element. | ||
* | ||
* Additionally, you can provide animations via the ngAnimate attribute to animate the **enter** | ||
* and **leave** effects. | ||
* | ||
* @animations | ||
* enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container | ||
* leave - happens just before the ngIf contents are removed from the DOM | ||
* | ||
* @element ANY | ||
* @scope | ||
* @param {expression} ngIf If the {@link guide/expression expression} is falsy then | ||
* the element is removed from the DOM tree (HTML). | ||
* | ||
* @example | ||
<doc:example> | ||
<doc:source> | ||
Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/> | ||
Show when checked: <span ng-if="checked">I'm removed when the checkbox is unchecked</span> | ||
</doc:source> | ||
</doc:example> | ||
*/ | ||
var ngIfDirective = ['$animator', function($animator) { | ||
return { | ||
transclude: 'element', | ||
priority: 1000, | ||
terminal: true, | ||
restrict: 'A', | ||
compile: function (element, attr, transclude) { | ||
return function ($scope, $element, $attr) { | ||
var animate = $animator($scope, $attr); | ||
var childElement, childScope; | ||
$scope.$watch($attr.ngIf, function ngIfWatchAction(value) { | ||
if (childElement) { | ||
animate.leave(childElement); | ||
childElement = undefined; | ||
} | ||
if (childScope) { | ||
childScope.$destroy(); | ||
childScope = undefined; | ||
} | ||
if (toBoolean(value)) { | ||
childScope = $scope.$new(); | ||
transclude(childScope, function (clone) { | ||
childElement = clone; | ||
animate.enter(clone, $element.parent(), $element); | ||
}); | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
}]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
'use strict'; | ||
|
||
describe('ngIf', function () { | ||
var $scope, $compile, element; | ||
|
||
beforeEach(inject(function ($rootScope, _$compile_) { | ||
$scope = $rootScope.$new(); | ||
$compile = _$compile_; | ||
element = $compile('<div></div>')($scope); | ||
})); | ||
|
||
afterEach(function () { | ||
dealoc(element); | ||
}); | ||
|
||
function makeIf(expr) { | ||
element.append($compile('<div class="my-class" ng-if="' + expr + '"><div>Hi</div></div>')($scope)); | ||
$scope.$apply(); | ||
} | ||
|
||
it('should immediately remove element if condition is false', function () { | ||
makeIf('false'); | ||
expect(element.children().length).toBe(0); | ||
}); | ||
|
||
it('should leave the element if condition is true', function () { | ||
makeIf('true'); | ||
expect(element.children().length).toBe(1); | ||
}); | ||
|
||
it('should create then remove the element if condition changes', function () { | ||
$scope.hello = true; | ||
makeIf('hello'); | ||
expect(element.children().length).toBe(1); | ||
$scope.$apply('hello = false'); | ||
expect(element.children().length).toBe(0); | ||
}); | ||
|
||
it('should create a new scope', function () { | ||
$scope.$apply('value = true'); | ||
element.append($compile( | ||
'<div ng-if="value"><span ng-init="value=false"></span></div>' | ||
)($scope)); | ||
$scope.$apply(); | ||
expect(element.children('div').length).toBe(1); | ||
}); | ||
|
||
it('should play nice with other elements beside it', function () { | ||
$scope.values = [1, 2, 3, 4]; | ||
element.append($compile( | ||
'<div ng-repeat="i in values"></div>' + | ||
'<div ng-if="values.length==4"></div>' + | ||
'<div ng-repeat="i in values"></div>' | ||
)($scope)); | ||
$scope.$apply(); | ||
expect(element.children().length).toBe(9); | ||
$scope.$apply('values.splice(0,1)'); | ||
expect(element.children().length).toBe(6); | ||
$scope.$apply('values.push(1)'); | ||
expect(element.children().length).toBe(9); | ||
}); | ||
|
||
it('should restore the element to its compiled state', function() { | ||
$scope.value = true; | ||
makeIf('value'); | ||
expect(element.children().length).toBe(1); | ||
jqLite(element.children()[0]).removeClass('my-class'); | ||
expect(element.children()[0].className).not.toContain('my-class'); | ||
$scope.$apply('value = false'); | ||
expect(element.children().length).toBe(0); | ||
$scope.$apply('value = true'); | ||
expect(element.children().length).toBe(1); | ||
expect(element.children()[0].className).toContain('my-class'); | ||
}); | ||
|
||
}); | ||
|
||
describe('ngIf ngAnimate', function () { | ||
var vendorPrefix, window; | ||
var body, element; | ||
|
||
function html(html) { | ||
body.html(html); | ||
element = body.children().eq(0); | ||
return element; | ||
} | ||
|
||
beforeEach(function() { | ||
// we need to run animation on attached elements; | ||
body = jqLite(document.body); | ||
}); | ||
|
||
afterEach(function(){ | ||
dealoc(body); | ||
dealoc(element); | ||
}); | ||
|
||
beforeEach(module(function($animationProvider, $provide) { | ||
$provide.value('$window', window = angular.mock.createMockWindow()); | ||
return function($sniffer, $animator) { | ||
vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; | ||
$animator.enabled(true); | ||
}; | ||
})); | ||
|
||
it('should fire off the enter animation + add and remove the css classes', | ||
inject(function($compile, $rootScope, $sniffer) { | ||
var $scope = $rootScope.$new(); | ||
var style = vendorPrefix + 'transition: 1s linear all'; | ||
element = $compile(html( | ||
'<div>' + | ||
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' + | ||
'</div>' | ||
))($scope); | ||
|
||
$rootScope.$digest(); | ||
$scope.$apply('value = true'); | ||
|
||
|
||
expect(element.children().length).toBe(1); | ||
var first = element.children()[0]; | ||
|
||
if ($sniffer.supportsTransitions) { | ||
expect(first.className).toContain('custom-enter-setup'); | ||
window.setTimeout.expect(1).process(); | ||
expect(first.className).toContain('custom-enter-start'); | ||
window.setTimeout.expect(1000).process(); | ||
} else { | ||
expect(window.setTimeout.queue).toEqual([]); | ||
} | ||
|
||
expect(first.className).not.toContain('custom-enter-setup'); | ||
expect(first.className).not.toContain('custom-enter-start'); | ||
})); | ||
|
||
it('should fire off the leave animation + add and remove the css classes', | ||
inject(function ($compile, $rootScope, $sniffer) { | ||
var $scope = $rootScope.$new(); | ||
var style = vendorPrefix + 'transition: 1s linear all'; | ||
element = $compile(html( | ||
'<div>' + | ||
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' + | ||
'</div>' | ||
))($scope); | ||
$scope.$apply('value = true'); | ||
|
||
expect(element.children().length).toBe(1); | ||
var first = element.children()[0]; | ||
|
||
if ($sniffer.supportsTransitions) { | ||
window.setTimeout.expect(1).process(); | ||
window.setTimeout.expect(1000).process(); | ||
} else { | ||
expect(window.setTimeout.queue).toEqual([]); | ||
} | ||
|
||
$scope.$apply('value = false'); | ||
expect(element.children().length).toBe($sniffer.supportsTransitions ? 1 : 0); | ||
|
||
if ($sniffer.supportsTransitions) { | ||
expect(first.className).toContain('custom-leave-setup'); | ||
window.setTimeout.expect(1).process(); | ||
expect(first.className).toContain('custom-leave-start'); | ||
window.setTimeout.expect(1000).process(); | ||
} else { | ||
expect(window.setTimeout.queue).toEqual([]); | ||
} | ||
|
||
expect(element.children().length).toBe(0); | ||
})); | ||
|
||
it('should catch and use the correct duration for animation', | ||
inject(function ($compile, $rootScope, $sniffer) { | ||
var $scope = $rootScope.$new(); | ||
var style = vendorPrefix + 'transition: 0.5s linear all'; | ||
element = $compile(html( | ||
'<div>' + | ||
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' + | ||
'</div>' | ||
))($scope); | ||
$scope.$apply('value = true'); | ||
|
||
if ($sniffer.supportsTransitions) { | ||
window.setTimeout.expect(1).process(); | ||
window.setTimeout.expect(500).process(); | ||
} else { | ||
expect(window.setTimeout.queue).toEqual([]); | ||
} | ||
})); | ||
|
||
}); |