aboutsummaryrefslogtreecommitdiffstats
path: root/src/ng/directive/ngIf.js
blob: 35c122ad12d97bbba3127c52d4624f56df0380fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
'use strict';

/**
 * @ngdoc directive
 * @name ng.directive:ngIf
 * @restrict A
 *
 * @description
 * The `ngIf` directive removes or recreates a portion of the DOM tree based on an
 * {expression}. If the expression assigned to `ngIf` evaluates to a false
 * value then the element is removed from the DOM, otherwise 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, 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 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` module 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
 * @priority 600
 * @param {expression} ngIf If the {@link guide/expression expression} is falsy then
 *     the element is removed from the DOM tree. If it is truthy a copy of the compiled
 *     element is added to the DOM tree.
 *
 * @example
  <example animations="true">
    <file name="index.html">
      Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/>
      Show when checked:
      <span ng-if="checked" class="animate-if">
        I'm removed when the checkbox is unchecked.
      </span>
    </file>
    <file name="animations.css">
      .animate-if {
        background:white;
        border:1px solid black;
        padding:10px;
      }

      .animate-if.ng-enter, .animate-if.ng-leave {
        -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
        -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
        -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
        transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
      }

      .animate-if.ng-enter,
      .animate-if.ng-leave.ng-leave-active {
        opacity:0;
      }

      .animate-if.ng-enter.ng-enter-active,
      .animate-if.ng-leave {
        opacity:1;
      }
    </file>
  </example>
 */
var ngIfDirective = ['$animate', function($animate) {
  return {
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    $$tlb: true,
    compile: function (element, attr, transclude) {
      return function ($scope, $element, $attr) {
        var block = {}, childScope;
        $scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
          if (block.startNode) {
            $animate.leave(getBlockElements(block));
            block = {};
          }
          if (block.startNode) {
            getBlockElements(block).$destroy();
            block = {};
          }
          if (toBoolean(value)) {
            childScope = $scope.$new();
            transclude(childScope, function (clone) {
              block.startNode = clone[0];
              block.endNode = clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ');
              $animate.enter(clone, $element.parent(), $element);
            });
          }
        });
      };
    }
  };
}];
<b>Hello</b> <i>World</i>!'; } function HTMLEditorWidget(element) { var self = this; var htmlFilter = angular.filter('html'); this.$parseModel = function() { // need to protect for script injection try { this.$viewValue = htmlFilter(this.$modelValue || '').get(); if (this.$error.HTML) { // we were invalid, but now we are OK. this.$emit('$valid', 'HTML'); } } catch (e) { // if HTML not parsable invalidate form. this.$emit('$invalid', 'HTML'); } } this.$render = function() { element.html(this.$viewValue); } element.bind('keyup', function() { self.$apply(function() { self.$emit('$viewChange', element.html()); }); }); } angular.directive('ng:contenteditable', function() { function linkFn($formFactory, element) { var exp = element.attr('ng:contenteditable'), form = $formFactory.forElement(element), widget; element.attr('contentEditable', true); widget = form.$createWidget({ scope: this, model: exp, controller: HTMLEditorWidget, controllerArgs: [element]}); // if the element is destroyed, then we need to notify the form. element.bind('$destroy', function() { widget.$destroy(); }); } linkFn.$inject = ['$formFactory']; return linkFn; }); </script> <form name='editorForm' ng:controller="EditorCntl"> <div ng:contenteditable="html"></div> <hr/> HTML: <br/> <textarea ng:model="html" cols=80></textarea> <hr/> <pre>editorForm = {{editorForm}}</pre> </form> </doc:source> <doc:scenario> it('should enter invalid HTML', function() { expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/); input('html').enter('<'); expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); }); </doc:scenario> </doc:example> */ angularServiceInject('$formFactory', function($rootScope) { /** * @ngdoc proprety * @name rootForm * @propertyOf angular.service.$formFactory * @description * Static property on `$formFactory` * * Each application ({@link guide/dev_guide.scopes.internals root scope}) gets a root form which * is the top-level parent of all forms. */ formFactory.rootForm = formFactory($rootScope); /** * @ngdoc method * @name forElement * @methodOf angular.service.$formFactory * @description * Static method on `$formFactory` service. * * Retrieve the closest form for a given element or defaults to the `root` form. Used by the * {@link angular.widget.form form} element. * @param {Element} element The element where the search for form should initiate. */ formFactory.forElement = function(element) { return element.inheritedData('$form') || formFactory.rootForm; }; return formFactory; function formFactory(parent) { return (parent || formFactory.rootForm).$new(FormController); } }, ['$rootScope']); function propertiesUpdate(widget) { widget.$valid = !(widget.$invalid = !(widget.$readonly || widget.$disabled || equals(widget.$error, {}))); } /** * @ngdoc property * @name $error * @propertyOf angular.service.$formFactory * @description * Property of the form and widget instance. * * Summary of all of the errors on the page. If a widget emits `$invalid` with `REQUIRED` key, * then the `$error` object will have a `REQUIRED` key with an array of widgets which have * emitted this key. `form.$error.REQUIRED == [ widget ]`. */ /** * @ngdoc property * @name $invalid * @propertyOf angular.service.$formFactory * @description * Property of the form and widget instance. * * True if any of the widgets of the form are invalid. */ /** * @ngdoc property * @name $valid * @propertyOf angular.service.$formFactory * @description * Property of the form and widget instance. * * True if all of the widgets of the form are valid. */ /** * @ngdoc event * @name angular.service.$formFactory#$valid * @eventOf angular.service.$formFactory * @eventType listen on form * @description * Upon receiving the `$valid` event from the widget update the `$error`, `$valid` and `$invalid` * properties of both the widget as well as the from. * * @param {String} validationKey The validation key to be used when updating the `$error` object. * The validation key is what will allow the template to bind to a specific validation error * such as `<div ng:show="form.$error.KEY">error for key</div>`. */ /** * @ngdoc event * @name angular.service.$formFactory#$invalid * @eventOf angular.service.$formFactory * @eventType listen on form * @description * Upon receiving the `$invalid` event from the widget update the `$error`, `$valid` and `$invalid` * properties of both the widget as well as the from. * * @param {String} validationKey The validation key to be used when updating the `$error` object. * The validation key is what will allow the template to bind to a specific validation error * such as `<div ng:show="form.$error.KEY">error for key</div>`. */ /** * @ngdoc event * @name angular.service.$formFactory#$validate * @eventOf angular.service.$formFactory * @eventType emit on widget * @description * Emit the `$validate` event on the widget, giving a widget a chance to emit a * `$valid` / `$invalid` event base on its state. The `$validate` event is triggered when the * model or the view changes. */ /** * @ngdoc event * @name angular.service.$formFactory#$viewChange * @eventOf angular.service.$formFactory * @eventType listen on widget * @description * A widget is responsible for emitting this event whenever the view changes do to user interaction. * The event takes a `$viewValue` parameter, which is the new value of the view. This * event triggers a call to `$parseView()` as well as `$validate` event on widget. * * @param {*} viewValue The new value for the view which will be assigned to `widget.$viewValue`. */ function FormController() { var form = this, $error = form.$error = {}; form.$on('$destroy', function(event){ var widget = event.targetScope; if (widget.$widgetId) { delete form[widget.$widgetId]; } forEach($error, removeWidget, widget); }); form.$on('$valid', function(event, error){ var widget = event.targetScope; delete widget.$error[error]; propertiesUpdate(widget); removeWidget($error[error], error, widget); }); form.$on('$invalid', function(event, error){ var widget = event.targetScope; addWidget(error, widget); widget.$error[error] = true; propertiesUpdate(widget); }); propertiesUpdate(form); function removeWidget(queue, errorKey, widget) { if (queue) { widget = widget || this; // so that we can be used in forEach; for (var i = 0, length = queue.length; i < length; i++) { if (queue[i] === widget) { queue.splice(i, 1); if (!queue.length) { delete $error[errorKey]; } } } propertiesUpdate(form); } } function addWidget(errorKey, widget) { var queue = $error[errorKey]; if (queue) { for (var i = 0, length = queue.length; i < length; i++) { if (queue[i] === widget) { return; } } } else { $error[errorKey] = queue = []; } queue.push(widget); propertiesUpdate(form); } } /** * @ngdoc method * @name $createWidget * @methodOf angular.service.$formFactory * @description * * Use form's `$createWidget` instance method to create new widgets. The widgets can be created * using an alias which makes the accessible from the form and available for data-binding, * useful for displaying validation error messages. * * The creation of a widget sets up: * * - `$watch` of `expression` on `model` scope. This code path syncs the model to the view. * The `$watch` listener will: * * - assign the new model value of `expression` to `widget.$modelValue`. * - call `widget.$parseModel` method if present. The `$parseModel` is responsible for copying * the `widget.$modelValue` to `widget.$viewValue` and optionally converting the data. * (For example to convert a number into string) * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid` * event. * - call `widget.$render()` method on widget. The `$render` method is responsible for * reading the `widget.$viewValue` and updating the DOM. * * - Listen on `$viewChange` event from the `widget`. This code path syncs the view to the model. * The `$viewChange` listener will: * * - assign the value to `widget.$viewValue`. * - call `widget.$parseView` method if present. The `$parseView` is responsible for copying * the `widget.$viewValue` to `widget.$modelValue` and optionally converting the data. * (For example to convert a string into number) * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid` * event. * - Assign the `widget.$modelValue` to the `expression` on the `model` scope. * * - Creates these set of properties on the `widget` which are updated as a response to the * `$valid` / `$invalid` events: * * - `$error` - object - validation errors will be published as keys on this object. * Data-binding to this property is useful for displaying the validation errors. * - `$valid` - boolean - true if there are no validation errors * - `$invalid` - boolean - opposite of `$valid`. * @param {Object} params Named parameters: * * - `scope` - `{Scope}` - The scope to which the model for this widget is attached. * - `model` - `{string}` - The name of the model property on model scope. * - `controller` - {WidgetController} - The controller constructor function. * The controller constructor should create these instance methods. * - `$parseView()`: optional method responsible for copying `$viewVale` to `$modelValue`. * The method may fire `$valid`/`$invalid` events. * - `$parseModel()`: optional method responsible for copying `$modelVale` to `$viewValue`. * The method may fire `$valid`/`$invalid` events. * - `$render()`: required method which needs to update the DOM of the widget to match the * `$viewValue`. * * - `controllerArgs` - `{Array}` (Optional) - Any extra arguments will be curried to the * WidgetController constructor. * - `onChange` - `{(string|function())}` (Optional) - Expression to execute when user changes the * value. * - `alias` - `{string}` (Optional) - The name of the form property under which the widget * instance should be published. The name should be unique for each form. * @returns {Widget} Instance of a widget scope. */ FormController.prototype.$createWidget = function(params) { var form = this, modelScope = params.scope, onChange = params.onChange, alias = params.alias, scopeGet = parser(params.model).assignable(), scopeSet = scopeGet.assign, widget = this.$new(params.controller, params.controllerArgs); widget.$error = {}; // Set the state to something we know will change to get the process going. widget.$modelValue = Number.NaN; // watch for scope changes and update the view appropriately modelScope.$watch(scopeGet, function(scope, value) { if (!equals(widget.$modelValue, value)) { widget.$modelValue = value; widget.$parseModel ? widget.$parseModel() : (widget.$viewValue = value); widget.$emit('$validate'); widget.$render && widget.$render(); } }); widget.$on('$viewChange', function(event, viewValue){ if (!equals(widget.$viewValue, viewValue)) { widget.$viewValue = viewValue; widget.$parseView ? widget.$parseView() : (widget.$modelValue = widget.$viewValue); scopeSet(modelScope, widget.$modelValue); if (onChange) modelScope.$eval(onChange); widget.$emit('$validate'); } }); propertiesUpdate(widget); // assign the widgetModel to the form if (alias && !form.hasOwnProperty(alias)) { form[alias] = widget; widget.$widgetId = alias; } else { alias = null; } return widget; };