Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 2f96fbd

Browse files
OrenAvissarpetebacondarwin
authored andcommitted
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.
1 parent 8a2bfd7 commit 2f96fbd

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

angularFiles.js

100644100755
+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ angularFiles = {
4949
'src/ng/directive/ngController.js',
5050
'src/ng/directive/ngCsp.js',
5151
'src/ng/directive/ngEventDirs.js',
52+
'src/ng/directive/ngIf.js',
5253
'src/ng/directive/ngInclude.js',
5354
'src/ng/directive/ngInit.js',
5455
'src/ng/directive/ngNonBindable.js',

src/AngularPublic.js

100644100755
+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ function publishExternalAPI(angular){
8282
ngController: ngControllerDirective,
8383
ngForm: ngFormDirective,
8484
ngHide: ngHideDirective,
85+
ngIf: ngIfDirective,
8586
ngInclude: ngIncludeDirective,
8687
ngInit: ngInitDirective,
8788
ngNonBindable: ngNonBindableDirective,

src/ng/directive/ngIf.js

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use strict';
2+
3+
/**
4+
* @ngdoc directive
5+
* @name ng.directive:ngIf
6+
* @restrict A
7+
*
8+
* @description
9+
* The `ngIf` directive removes and recreates a portion of the DOM tree (HTML)
10+
* conditionally based on **"falsy"** and **"truthy"** values, respectively, evaluated within
11+
* an {expression}. In other words, if the expression assigned to **ngIf evaluates to a false
12+
* value** then **the element is removed from the DOM** and **if true** then **a clone of the
13+
* element is reinserted into the DOM**.
14+
*
15+
* `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the
16+
* element in the DOM rather than changing its visibility via the `display` css property. A common
17+
* case when this difference is significant is when using css selectors that rely on an element's
18+
* position within the DOM (HTML), such as the `:first-child` or `:last-child` pseudo-classes.
19+
*
20+
* Note that **when an element is removed using ngIf its scope is destroyed** and **a new scope
21+
* is created when the element is restored**. The scope created within `ngIf` inherits from
22+
* its parent scope using
23+
* {@link https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance}.
24+
* An important implication of this is if `ngModel` is used within `ngIf` to bind to
25+
* a javascript primitive defined in the parent scope. In this case any modifications made to the
26+
* variable within the child scope will override (hide) the value in the parent scope.
27+
*
28+
* Also, `ngIf` recreates elements using their compiled state. An example scenario of this behavior
29+
* is if an element's class attribute is directly modified after it's compiled, using something like
30+
* jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element
31+
* the added class will be lost because the original compiled state is used to regenerate the element.
32+
*
33+
* Additionally, you can provide animations via the ngAnimate attribute to animate the **enter**
34+
* and **leave** effects.
35+
*
36+
* @animations
37+
* enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container
38+
* leave - happens just before the ngIf contents are removed from the DOM
39+
*
40+
* @element ANY
41+
* @scope
42+
* @param {expression} ngIf If the {@link guide/expression expression} is falsy then
43+
* the element is removed from the DOM tree (HTML).
44+
*
45+
* @example
46+
<doc:example>
47+
<doc:source>
48+
Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/>
49+
Show when checked: <span ng-if="checked">I'm removed when the checkbox is unchecked</span>
50+
</doc:source>
51+
</doc:example>
52+
*/
53+
var ngIfDirective = ['$animator', function($animator) {
54+
return {
55+
transclude: 'element',
56+
priority: 1000,
57+
terminal: true,
58+
restrict: 'A',
59+
compile: function (element, attr, transclude) {
60+
return function ($scope, $element, $attr) {
61+
var animate = $animator($scope, $attr);
62+
var childElement, childScope;
63+
$scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
64+
if (childElement) {
65+
animate.leave(childElement);
66+
childElement = undefined;
67+
}
68+
if (childScope) {
69+
childScope.$destroy();
70+
childScope = undefined;
71+
}
72+
if (toBoolean(value)) {
73+
childScope = $scope.$new();
74+
transclude(childScope, function (clone) {
75+
childElement = clone;
76+
animate.enter(clone, $element.parent(), $element);
77+
});
78+
}
79+
});
80+
}
81+
}
82+
}
83+
}];

test/ng/directive/ngIfSpec.js

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
'use strict';
2+
3+
describe('ngIf', function () {
4+
var $scope, $compile, element;
5+
6+
beforeEach(inject(function ($rootScope, _$compile_) {
7+
$scope = $rootScope.$new();
8+
$compile = _$compile_;
9+
element = $compile('<div></div>')($scope);
10+
}));
11+
12+
afterEach(function () {
13+
dealoc(element);
14+
});
15+
16+
function makeIf(expr) {
17+
element.append($compile('<div class="my-class" ng-if="' + expr + '"><div>Hi</div></div>')($scope));
18+
$scope.$apply();
19+
}
20+
21+
it('should immediately remove element if condition is false', function () {
22+
makeIf('false');
23+
expect(element.children().length).toBe(0);
24+
});
25+
26+
it('should leave the element if condition is true', function () {
27+
makeIf('true');
28+
expect(element.children().length).toBe(1);
29+
});
30+
31+
it('should create then remove the element if condition changes', function () {
32+
$scope.hello = true;
33+
makeIf('hello');
34+
expect(element.children().length).toBe(1);
35+
$scope.$apply('hello = false');
36+
expect(element.children().length).toBe(0);
37+
});
38+
39+
it('should create a new scope', function () {
40+
$scope.$apply('value = true');
41+
element.append($compile(
42+
'<div ng-if="value"><span ng-init="value=false"></span></div>'
43+
)($scope));
44+
$scope.$apply();
45+
expect(element.children('div').length).toBe(1);
46+
});
47+
48+
it('should play nice with other elements beside it', function () {
49+
$scope.values = [1, 2, 3, 4];
50+
element.append($compile(
51+
'<div ng-repeat="i in values"></div>' +
52+
'<div ng-if="values.length==4"></div>' +
53+
'<div ng-repeat="i in values"></div>'
54+
)($scope));
55+
$scope.$apply();
56+
expect(element.children().length).toBe(9);
57+
$scope.$apply('values.splice(0,1)');
58+
expect(element.children().length).toBe(6);
59+
$scope.$apply('values.push(1)');
60+
expect(element.children().length).toBe(9);
61+
});
62+
63+
it('should restore the element to its compiled state', function() {
64+
$scope.value = true;
65+
makeIf('value');
66+
expect(element.children().length).toBe(1);
67+
jqLite(element.children()[0]).removeClass('my-class');
68+
expect(element.children()[0].className).not.toContain('my-class');
69+
$scope.$apply('value = false');
70+
expect(element.children().length).toBe(0);
71+
$scope.$apply('value = true');
72+
expect(element.children().length).toBe(1);
73+
expect(element.children()[0].className).toContain('my-class');
74+
});
75+
76+
});
77+
78+
describe('ngIf ngAnimate', function () {
79+
var vendorPrefix, window;
80+
var body, element;
81+
82+
function html(html) {
83+
body.html(html);
84+
element = body.children().eq(0);
85+
return element;
86+
}
87+
88+
beforeEach(function() {
89+
// we need to run animation on attached elements;
90+
body = jqLite(document.body);
91+
});
92+
93+
afterEach(function(){
94+
dealoc(body);
95+
dealoc(element);
96+
});
97+
98+
beforeEach(module(function($animationProvider, $provide) {
99+
$provide.value('$window', window = angular.mock.createMockWindow());
100+
return function($sniffer, $animator) {
101+
vendorPrefix = '-' + $sniffer.vendorPrefix + '-';
102+
$animator.enabled(true);
103+
};
104+
}));
105+
106+
it('should fire off the enter animation + add and remove the css classes',
107+
inject(function($compile, $rootScope, $sniffer) {
108+
var $scope = $rootScope.$new();
109+
var style = vendorPrefix + 'transition: 1s linear all';
110+
element = $compile(html(
111+
'<div>' +
112+
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
113+
'</div>'
114+
))($scope);
115+
116+
$rootScope.$digest();
117+
$scope.$apply('value = true');
118+
119+
120+
expect(element.children().length).toBe(1);
121+
var first = element.children()[0];
122+
123+
if ($sniffer.supportsTransitions) {
124+
expect(first.className).toContain('custom-enter-setup');
125+
window.setTimeout.expect(1).process();
126+
expect(first.className).toContain('custom-enter-start');
127+
window.setTimeout.expect(1000).process();
128+
} else {
129+
expect(window.setTimeout.queue).toEqual([]);
130+
}
131+
132+
expect(first.className).not.toContain('custom-enter-setup');
133+
expect(first.className).not.toContain('custom-enter-start');
134+
}));
135+
136+
it('should fire off the leave animation + add and remove the css classes',
137+
inject(function ($compile, $rootScope, $sniffer) {
138+
var $scope = $rootScope.$new();
139+
var style = vendorPrefix + 'transition: 1s linear all';
140+
element = $compile(html(
141+
'<div>' +
142+
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
143+
'</div>'
144+
))($scope);
145+
$scope.$apply('value = true');
146+
147+
expect(element.children().length).toBe(1);
148+
var first = element.children()[0];
149+
150+
if ($sniffer.supportsTransitions) {
151+
window.setTimeout.expect(1).process();
152+
window.setTimeout.expect(1000).process();
153+
} else {
154+
expect(window.setTimeout.queue).toEqual([]);
155+
}
156+
157+
$scope.$apply('value = false');
158+
expect(element.children().length).toBe($sniffer.supportsTransitions ? 1 : 0);
159+
160+
if ($sniffer.supportsTransitions) {
161+
expect(first.className).toContain('custom-leave-setup');
162+
window.setTimeout.expect(1).process();
163+
expect(first.className).toContain('custom-leave-start');
164+
window.setTimeout.expect(1000).process();
165+
} else {
166+
expect(window.setTimeout.queue).toEqual([]);
167+
}
168+
169+
expect(element.children().length).toBe(0);
170+
}));
171+
172+
it('should catch and use the correct duration for animation',
173+
inject(function ($compile, $rootScope, $sniffer) {
174+
var $scope = $rootScope.$new();
175+
var style = vendorPrefix + 'transition: 0.5s linear all';
176+
element = $compile(html(
177+
'<div>' +
178+
'<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
179+
'</div>'
180+
))($scope);
181+
$scope.$apply('value = true');
182+
183+
if ($sniffer.supportsTransitions) {
184+
window.setTimeout.expect(1).process();
185+
window.setTimeout.expect(500).process();
186+
} else {
187+
expect(window.setTimeout.queue).toEqual([]);
188+
}
189+
}));
190+
191+
});

0 commit comments

Comments
 (0)