mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-01-12 22:45:52 +08:00
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.
This commit is contained in:
committed by
Pete Bacon Darwin
parent
8a2bfd7a78
commit
2f96fbd175
1
angularFiles.js
vendored
Normal file → Executable file
1
angularFiles.js
vendored
Normal file → Executable file
@@ -49,6 +49,7 @@ angularFiles = {
|
||||
'src/ng/directive/ngController.js',
|
||||
'src/ng/directive/ngCsp.js',
|
||||
'src/ng/directive/ngEventDirs.js',
|
||||
'src/ng/directive/ngIf.js',
|
||||
'src/ng/directive/ngInclude.js',
|
||||
'src/ng/directive/ngInit.js',
|
||||
'src/ng/directive/ngNonBindable.js',
|
||||
|
||||
1
src/AngularPublic.js
Normal file → Executable file
1
src/AngularPublic.js
Normal file → Executable file
@@ -82,6 +82,7 @@ function publishExternalAPI(angular){
|
||||
ngController: ngControllerDirective,
|
||||
ngForm: ngFormDirective,
|
||||
ngHide: ngHideDirective,
|
||||
ngIf: ngIfDirective,
|
||||
ngInclude: ngIncludeDirective,
|
||||
ngInit: ngInitDirective,
|
||||
ngNonBindable: ngNonBindableDirective,
|
||||
|
||||
83
src/ng/directive/ngIf.js
Executable file
83
src/ng/directive/ngIf.js
Executable file
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
191
test/ng/directive/ngIfSpec.js
Executable file
191
test/ng/directive/ngIfSpec.js
Executable file
@@ -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([]);
|
||||
}
|
||||
}));
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user