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

Commit

Permalink
feat(ngIf): add directive to remove and recreate DOM elements
Browse files Browse the repository at this point in the history
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
OrenAvissar authored and petebacondarwin committed Apr 19, 2013
1 parent 8a2bfd7 commit 2f96fbd
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 0 deletions.
1 change: 1 addition & 0 deletions angularFiles.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/AngularPublic.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function publishExternalAPI(angular){
ngController: ngControllerDirective,
ngForm: ngFormDirective,
ngHide: ngHideDirective,
ngIf: ngIfDirective,
ngInclude: ngIncludeDirective,
ngInit: ngInitDirective,
ngNonBindable: ngNonBindableDirective,
Expand Down
83 changes: 83 additions & 0 deletions src/ng/directive/ngIf.js
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);
});
}
});
}
}
}
}];
191 changes: 191 additions & 0 deletions test/ng/directive/ngIfSpec.js
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([]);
}
}));

});

0 comments on commit 2f96fbd

Please sign in to comment.