aboutsummaryrefslogtreecommitdiffstats
path: root/src/directive
diff options
context:
space:
mode:
Diffstat (limited to 'src/directive')
-rw-r--r--src/directive/a.js29
-rw-r--r--src/directive/booleanAttrDirs.js282
-rw-r--r--src/directive/directives.js11
-rw-r--r--src/directive/form.js235
-rw-r--r--src/directive/input.js1171
-rw-r--r--src/directive/ngBind.js249
-rw-r--r--src/directive/ngClass.js143
-rw-r--r--src/directive/ngCloak.js61
-rw-r--r--src/directive/ngController.js103
-rw-r--r--src/directive/ngEventDirs.js222
-rw-r--r--src/directive/ngInclude.js119
-rw-r--r--src/directive/ngInit.js37
-rw-r--r--src/directive/ngNonBindable.js34
-rw-r--r--src/directive/ngPluralize.js204
-rw-r--r--src/directive/ngRepeat.js182
-rw-r--r--src/directive/ngShowHide.js80
-rw-r--r--src/directive/ngStyle.js42
-rw-r--r--src/directive/ngSwitch.js110
-rw-r--r--src/directive/ngTransclude.js58
-rw-r--r--src/directive/ngView.js169
-rw-r--r--src/directive/script.js42
-rw-r--r--src/directive/select.js444
-rw-r--r--src/directive/style.js6
23 files changed, 4033 insertions, 0 deletions
diff --git a/src/directive/a.js b/src/directive/a.js
new file mode 100644
index 00000000..bd56f3be
--- /dev/null
+++ b/src/directive/a.js
@@ -0,0 +1,29 @@
+'use strict';
+
+/*
+ * Modifies the default behavior of html A tag, so that the default action is prevented when href
+ * attribute is empty.
+ *
+ * The reasoning for this change is to allow easy creation of action links with ng:click without
+ * changing the location or causing page reloads, e.g.:
+ * <a href="" ng:click="model.$save()">Save</a>
+ */
+var htmlAnchorDirective = valueFn({
+ restrict: 'E',
+ compile: function(element, attr) {
+ // turn <a href ng:click="..">link</a> into a link in IE
+ // but only if it doesn't have name attribute, in which case it's an anchor
+ if (!attr.href) {
+ attr.$set('href', '');
+ }
+
+ return function(scope, element) {
+ element.bind('click', function(event){
+ // if we have no href url, then don't navigate anywhere.
+ if (!element.attr('href')) {
+ event.preventDefault();
+ }
+ });
+ }
+ }
+});
diff --git a/src/directive/booleanAttrDirs.js b/src/directive/booleanAttrDirs.js
new file mode 100644
index 00000000..06b85823
--- /dev/null
+++ b/src/directive/booleanAttrDirs.js
@@ -0,0 +1,282 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:href
+ *
+ * @description
+ * Using <angular/> markup like {{hash}} in an href attribute makes
+ * the page open to a wrong URL, if the user clicks that link before
+ * angular has a chance to replace the {{hash}} with actual URL, the
+ * link will be broken and will most likely return a 404 error.
+ * The `ng:href` solves this problem by placing the `href` in the
+ * `ng:` namespace.
+ *
+ * The buggy way to write it:
+ * <pre>
+ * <a href="http://www.gravatar.com/avatar/{{hash}}"/>
+ * </pre>
+ *
+ * The correct way to write it:
+ * <pre>
+ * <a ng:href="http://www.gravatar.com/avatar/{{hash}}"/>
+ * </pre>
+ *
+ * @element ANY
+ * @param {template} template any string which can contain `{{}}` markup.
+ *
+ * @example
+ * This example uses `link` variable inside `href` attribute:
+ <doc:example>
+ <doc:source>
+ <input ng:model="value" /><br />
+ <a id="link-1" href ng:click="value = 1">link 1</a> (link, don't reload)<br />
+ <a id="link-2" href="" ng:click="value = 2">link 2</a> (link, don't reload)<br />
+ <a id="link-3" ng:href="/{{'123'}}" ng:ext-link>link 3</a> (link, reload!)<br />
+ <a id="link-4" href="" name="xx" ng:click="value = 4">anchor</a> (link, don't reload)<br />
+ <a id="link-5" name="xxx" ng:click="value = 5">anchor</a> (no link)<br />
+ <a id="link-6" ng:href="/{{value}}" ng:ext-link>link</a> (link, change hash)
+ </doc:source>
+ <doc:scenario>
+ it('should execute ng:click but not reload when href without value', function() {
+ element('#link-1').click();
+ expect(input('value').val()).toEqual('1');
+ expect(element('#link-1').attr('href')).toBe("");
+ });
+
+ it('should execute ng:click but not reload when href empty string', function() {
+ element('#link-2').click();
+ expect(input('value').val()).toEqual('2');
+ expect(element('#link-2').attr('href')).toBe("");
+ });
+
+ it('should execute ng:click and change url when ng:href specified', function() {
+ expect(element('#link-3').attr('href')).toBe("/123");
+
+ element('#link-3').click();
+ expect(browser().window().path()).toEqual('/123');
+ });
+
+ it('should execute ng:click but not reload when href empty string and name specified', function() {
+ element('#link-4').click();
+ expect(input('value').val()).toEqual('4');
+ expect(element('#link-4').attr('href')).toBe("");
+ });
+
+ it('should execute ng:click but not reload when no href but name specified', function() {
+ element('#link-5').click();
+ expect(input('value').val()).toEqual('5');
+ expect(element('#link-5').attr('href')).toBe("");
+ });
+
+ it('should only change url when only ng:href', function() {
+ input('value').enter('6');
+ expect(element('#link-6').attr('href')).toBe("/6");
+
+ element('#link-6').click();
+ expect(browser().window().path()).toEqual('/6');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:src
+ *
+ * @description
+ * Using <angular/> markup like `{{hash}}` in a `src` attribute doesn't
+ * work right: The browser will fetch from the URL with the literal
+ * text `{{hash}}` until <angular/> replaces the expression inside
+ * `{{hash}}`. The `ng:src` attribute solves this problem by placing
+ * the `src` attribute in the `ng:` namespace.
+ *
+ * The buggy way to write it:
+ * <pre>
+ * <img src="http://www.gravatar.com/avatar/{{hash}}"/>
+ * </pre>
+ *
+ * The correct way to write it:
+ * <pre>
+ * <img ng:src="http://www.gravatar.com/avatar/{{hash}}"/>
+ * </pre>
+ *
+ * @element ANY
+ * @param {template} template any string which can contain `{{}}` markup.
+ */
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:disabled
+ *
+ * @description
+ *
+ * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs:
+ * <pre>
+ * <div ng:init="scope = { isDisabled: false }">
+ * <button disabled="{{scope.isDisabled}}">Disabled</button>
+ * </div>
+ * </pre>
+ *
+ * The HTML specs do not require browsers to preserve the special attributes such as disabled.
+ * (The presence of them means true and absence means false)
+ * This prevents the angular compiler from correctly retrieving the binding expression.
+ * To solve this problem, we introduce ng:disabled.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ Click me to toggle: <input type="checkbox" ng:model="checked"><br/>
+ <button ng:model="button" ng:disabled="{{checked}}">Button</button>
+ </doc:source>
+ <doc:scenario>
+ it('should toggle button', function() {
+ expect(element('.doc-example-live :button').prop('disabled')).toBeFalsy();
+ input('checked').check();
+ expect(element('.doc-example-live :button').prop('disabled')).toBeTruthy();
+ });
+ </doc:scenario>
+ </doc:example>
+ *
+ * @element ANY
+ * @param {template} template any string which can contain '{{}}' markup.
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:checked
+ *
+ * @description
+ * The HTML specs do not require browsers to preserve the special attributes such as checked.
+ * (The presence of them means true and absence means false)
+ * This prevents the angular compiler from correctly retrieving the binding expression.
+ * To solve this problem, we introduce ng:checked.
+ * @example
+ <doc:example>
+ <doc:source>
+ Check me to check both: <input type="checkbox" ng:model="master"><br/>
+ <input id="checkSlave" type="checkbox" ng:checked="{{master}}">
+ </doc:source>
+ <doc:scenario>
+ it('should check both checkBoxes', function() {
+ expect(element('.doc-example-live #checkSlave').prop('checked')).toBeFalsy();
+ input('master').check();
+ expect(element('.doc-example-live #checkSlave').prop('checked')).toBeTruthy();
+ });
+ </doc:scenario>
+ </doc:example>
+ *
+ * @element ANY
+ * @param {template} template any string which can contain '{{}}' markup.
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:multiple
+ *
+ * @description
+ * The HTML specs do not require browsers to preserve the special attributes such as multiple.
+ * (The presence of them means true and absence means false)
+ * This prevents the angular compiler from correctly retrieving the binding expression.
+ * To solve this problem, we introduce ng:multiple.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ Check me check multiple: <input type="checkbox" ng:model="checked"><br/>
+ <select id="select" ng:multiple="{{checked}}">
+ <option>Misko</option>
+ <option>Igor</option>
+ <option>Vojta</option>
+ <option>Di</option>
+ </select>
+ </doc:source>
+ <doc:scenario>
+ it('should toggle multiple', function() {
+ expect(element('.doc-example-live #select').prop('multiple')).toBeFalsy();
+ input('checked').check();
+ expect(element('.doc-example-live #select').prop('multiple')).toBeTruthy();
+ });
+ </doc:scenario>
+ </doc:example>
+ *
+ * @element ANY
+ * @param {template} template any string which can contain '{{}}' markup.
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:readonly
+ *
+ * @description
+ * The HTML specs do not require browsers to preserve the special attributes such as readonly.
+ * (The presence of them means true and absence means false)
+ * This prevents the angular compiler from correctly retrieving the binding expression.
+ * To solve this problem, we introduce ng:readonly.
+ * @example
+ <doc:example>
+ <doc:source>
+ Check me to make text readonly: <input type="checkbox" ng:model="checked"><br/>
+ <input type="text" ng:readonly="{{checked}}" value="I'm Angular"/>
+ </doc:source>
+ <doc:scenario>
+ it('should toggle readonly attr', function() {
+ expect(element('.doc-example-live :text').prop('readonly')).toBeFalsy();
+ input('checked').check();
+ expect(element('.doc-example-live :text').prop('readonly')).toBeTruthy();
+ });
+ </doc:scenario>
+ </doc:example>
+ *
+ * @element ANY
+ * @param {template} template any string which can contain '{{}}' markup.
+ */
+
+
+/**
+* @ngdoc directive
+* @name angular.module.ng.$compileProvider.directive.ng:selected
+*
+* @description
+* The HTML specs do not require browsers to preserve the special attributes such as selected.
+* (The presence of them means true and absence means false)
+* This prevents the angular compiler from correctly retrieving the binding expression.
+* To solve this problem, we introduce ng:selected.
+* @example
+ <doc:example>
+ <doc:source>
+ Check me to select: <input type="checkbox" ng:model="checked"><br/>
+ <select>
+ <option>Hello!</option>
+ <option id="greet" ng:selected="{{checked}}">Greetings!</option>
+ </select>
+ </doc:source>
+ <doc:scenario>
+ it('should select Greetings!', function() {
+ expect(element('.doc-example-live #greet').prop('selected')).toBeFalsy();
+ input('checked').check();
+ expect(element('.doc-example-live #greet').prop('selected')).toBeTruthy();
+ });
+ </doc:scenario>
+ </doc:example>
+* @element ANY
+* @param {template} template any string which can contain '{{}}' markup.
+*/
+
+
+function ngAttributeAliasDirective(propName, attrName) {
+ ngAttributeAliasDirectives[directiveNormalize('ng-' + attrName)] = valueFn(
+ function(scope, element, attr) {
+ attr.$observe(directiveNormalize('ng-' + attrName), function(value) {
+ attr.$set(attrName, value);
+ });
+ }
+ );
+}
+
+var ngAttributeAliasDirectives = {};
+forEach(BOOLEAN_ATTR, ngAttributeAliasDirective);
+ngAttributeAliasDirective(null, 'src');
diff --git a/src/directive/directives.js b/src/directive/directives.js
new file mode 100644
index 00000000..123645f9
--- /dev/null
+++ b/src/directive/directives.js
@@ -0,0 +1,11 @@
+'use strict';
+
+function ngDirective(directive) {
+ if (isFunction(directive)) {
+ directive = {
+ link: directive
+ }
+ }
+ directive.restrict = directive.restrict || 'AC';
+ return valueFn(directive);
+};
diff --git a/src/directive/form.js b/src/directive/form.js
new file mode 100644
index 00000000..c3e6b21d
--- /dev/null
+++ b/src/directive/form.js
@@ -0,0 +1,235 @@
+'use strict';
+
+
+/**
+ * @ngdoc object
+ * @name angular.module.ng.$compileProvider.directive.form.FormController
+ *
+ * @property {boolean} pristine True if user has not interacted with the form yet.
+ * @property {boolean} dirty True if user has already interacted with the form.
+ * @property {boolean} valid True if all of the containg widgets are valid.
+ * @property {boolean} invalid True if at least one containing widget is invalid.
+ *
+ * @property {Object} error Is an object hash, containing references to all invalid widgets, where
+ *
+ * - keys are error ids (such as `REQUIRED`, `URL` or `EMAIL`),
+ * - values are arrays of widgets that are invalid with given error.
+ *
+ * @description
+ * `FormController` keeps track of all its widgets as well as state of them form, such as being valid/invalid or dirty/pristine.
+ *
+ * Each {@link angular.module.ng.$compileProvider.directive.form form} directive creates an instance
+ * of `FormController`.
+ *
+ */
+FormController.$inject = ['$scope', 'name'];
+function FormController($scope, name) {
+ var form = this,
+ errors = form.error = {};
+
+ // publish the form into scope
+ name(this);
+
+ $scope.$on('$destroy', function(event, widget) {
+ if (!widget) return;
+
+ if (widget.widgetId) {
+ delete form[widget.widgetId];
+ }
+ forEach(errors, removeWidget, widget);
+ });
+
+ $scope.$on('$valid', function(event, error, widget) {
+ removeWidget(errors[error], error, widget);
+
+ if (equals(errors, {})) {
+ form.valid = true;
+ form.invalid = false;
+ }
+ });
+
+ $scope.$on('$invalid', function(event, error, widget) {
+ addWidget(error, widget);
+
+ form.valid = false;
+ form.invalid = true;
+ });
+
+ $scope.$on('$viewTouch', function() {
+ form.dirty = true;
+ form.pristine = false;
+ });
+
+ // init state
+ form.dirty = false;
+ form.pristine = true;
+ form.valid = true;
+ form.invalid = false;
+
+ 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 errors[errorKey];
+ }
+ }
+ }
+ }
+ }
+
+ function addWidget(errorKey, widget) {
+ var queue = errors[errorKey];
+ if (queue) {
+ for (var i = 0, length = queue.length; i < length; i++) {
+ if (queue[i] === widget) {
+ return;
+ }
+ }
+ } else {
+ errors[errorKey] = queue = [];
+ }
+ queue.push(widget);
+ }
+}
+
+/**
+ * @ngdoc function
+ * @name angular.module.ng.$compileProvider.directive.form.FormController#registerWidget
+ * @methodOf angular.module.ng.$compileProvider.directive.form.FormController
+ * @function
+ *
+ * @param {Object} widget Widget to register (controller of a widget)
+ * @param {string=} alias Name alias of the widget.
+ * (If specified, widget will be accesible as a form property)
+ *
+ * @description
+ *
+ */
+FormController.prototype.registerWidget = function(widget, alias) {
+ if (alias && !this.hasOwnProperty(alias)) {
+ widget.widgetId = alias;
+ this[alias] = widget;
+ }
+};
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.form
+ *
+ * @scope
+ * @description
+ * Directive that instantiates
+ * {@link angular.module.ng.$compileProvider.directive.form.FormController FormController}.
+ *
+ * If `name` attribute is specified, the controller is published to the scope as well.
+ *
+ * # Alias: `ng:form`
+ *
+ * In angular forms can be nested. This means that the outer form is valid when all of the child
+ * forms are valid as well. However browsers do not allow nesting of `<form>` elements, for this
+ * reason angular provides `<ng:form>` alias which behaves identical to `<form>` but allows
+ * element nesting.
+ *
+ *
+ * # CSS classes
+ * - `ng-valid` Is set if the form is valid.
+ * - `ng-invalid` Is set if the form is invalid.
+ * - `ng-pristine` Is set if the form is pristine.
+ * - `ng-dirty` Is set if the form is dirty.
+ *
+ *
+ * # Submitting a form and preventing default action
+ *
+ * Since the role of forms in client-side Angular applications is different than in classical
+ * roundtrip apps, it is desirable for the browser not to translate the form submission into a full
+ * page reload that sends the data to the server. Instead some javascript logic should be triggered
+ * to handle the form submission in application specific way.
+ *
+ * For this reason, Angular prevents the default action (form submission to the server) unless the
+ * `<form>` element has an `action` attribute specified.
+ *
+ * You can use one of the following two ways to specify what javascript method should be called when
+ * a form is submitted:
+ *
+ * - ng:submit on the form element (add link to ng:submit)
+ * - ng:click on the first button or input field of type submit (input[type=submit])
+ *
+ * To prevent double execution of the handler, use only one of ng:submit or ng:click. This is
+ * because of the following form submission rules coming from the html spec:
+ *
+ * - If a form has only one input field then hitting enter in this field triggers form submit
+ * (`ng:submit`)
+ * - if a form has has 2+ input fields and no buttons or input[type=submit] then hitting enter
+ * doesn't trigger submit
+ * - if a form has one or more input fields and one or more buttons or input[type=submit] then
+ * hitting enter in any of the input fields will trigger the click handler on the *first* button or
+ * input[type=submit] (`ng:click`) *and* a submit handler on the enclosing form (`ng:submit`)
+ *
+ * @param {string=} name Name of the form. If specified, the form controller will be published into
+ * related scope, under this name.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.text = 'guest';
+ }
+ </script>
+ <form name="myForm" ng:controller="Ctrl">
+ text: <input type="text" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.error.REQUIRED">Required!</span>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/>
+ <tt>myForm.input.error = {{myForm.input.error}}</tt><br/>
+ <tt>myForm.valid = {{myForm.valid}}</tt><br/>
+ <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var formDirective = [function() {
+ return {
+ name: 'form',
+ restrict: 'E',
+ scope: true,
+ inject: {
+ name: 'accessor'
+ },
+ controller: FormController,
+ compile: function() {
+ return {
+ pre: function(scope, formElement, attr, controller) {
+ formElement.data('$form', controller);
+ formElement.bind('submit', function(event) {
+ if (!attr.action) event.preventDefault();
+ });
+
+ forEach(['valid', 'invalid', 'dirty', 'pristine'], function(name) {
+ scope.$watch(function() {
+ return controller[name];
+ }, function(value) {
+ formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ });
+ }
+ };
+ }
+ };
+}];
diff --git a/src/directive/input.js b/src/directive/input.js
new file mode 100644
index 00000000..af446c6b
--- /dev/null
+++ b/src/directive/input.js
@@ -0,0 +1,1171 @@
+'use strict';
+
+var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
+var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
+var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
+
+var inputType = {
+
+ /**
+ * @ngdoc inputType
+ * @name angular.module.ng.$compileProvider.directive.input.text
+ *
+ * @description
+ * Standard HTML text input with angular data binding.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than
+ * minlength.
+ * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than
+ * maxlength.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ * @param {string=} ng:change Angular expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.text = 'guest';
+ $scope.word = /^\w*$/;
+ }
+ </script>
+ <form name="myForm" ng:controller="Ctrl">
+ Single word: <input type="text" name="input" ng:model="text"
+ ng:pattern="word" required>
+ <span class="error" ng:show="myForm.input.error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.error.PATTERN">
+ Single word only!</span>
+
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/>
+ <tt>myForm.input.error = {{myForm.input.error}}</tt><br/>
+ <tt>myForm.valid = {{myForm.valid}}</tt><br/>
+ <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+
+ it('should be invalid if multi word', function() {
+ input('text').enter('hello world');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+ 'text': textInputType,
+
+
+ /**
+ * @ngdoc inputType
+ * @name angular.module.ng.$compileProvider.directive.input.number
+ *
+ * @description
+ * Text input with number validation and transformation. Sets the `NUMBER` validation
+ * error if not a valid number.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`.
+ * @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than
+ * minlength.
+ * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than
+ * maxlength.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ * @param {string=} ng:change Angular expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.value = 12;
+ }
+ </script>
+ <form name="myForm" ng:controller="Ctrl">
+ Number: <input type="number" name="input" ng:model="value"
+ min="0" max="99" required>
+ <span class="error" ng:show="myForm.list.error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.list.error.NUMBER">
+ Not valid number!</span>
+ <tt>value = {{value}}</tt><br/>
+ <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/>
+ <tt>myForm.input.error = {{myForm.input.error}}</tt><br/>
+ <tt>myForm.valid = {{myForm.valid}}</tt><br/>
+ <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('value').enter('');
+ expect(binding('value')).toEqual('');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+
+ it('should be invalid if over max', function() {
+ input('value').enter('123');
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+ 'number': numberInputType,
+
+
+ /**
+ * @ngdoc inputType
+ * @name angular.module.ng.$compileProvider.directive.input.url
+ *
+ * @description
+ * Text input with URL validation. Sets the `URL` validation error key if the content is not a
+ * valid URL.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than
+ * minlength.
+ * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than
+ * maxlength.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ * @param {string=} ng:change Angular expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.text = 'http://google.com';
+ }
+ </script>
+ <form name="myForm" ng:controller="Ctrl">
+ URL: <input type="url" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.error.url">
+ Not valid url!</span>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/>
+ <tt>myForm.input.error = {{myForm.input.error}}</tt><br/>
+ <tt>myForm.valid = {{myForm.valid}}</tt><br/>
+ <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/>
+ <tt>myForm.error.url = {{!!myForm.error.url}}</tt><br/>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('http://google.com');
+ expect(binding('myForm.input.valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+
+ it('should be invalid if not url', function() {
+ input('text').enter('xxx');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+ 'url': urlInputType,
+
+
+ /**
+ * @ngdoc inputType
+ * @name angular.module.ng.$compileProvider.directive.input.email
+ *
+ * @description
+ * Text input with email validation. Sets the `EMAIL` validation error key if not a valid email
+ * address.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than
+ * minlength.
+ * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than
+ * maxlength.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.text = 'me@example.com';
+ }
+ </script>
+ <form name="myForm" ng:controller="Ctrl">
+ Email: <input type="email" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.error.EMAIL">
+ Not valid email!</span>
+ <tt>text = {{text}}</tt><br/>
+ <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/>
+ <tt>myForm.input.error = {{myForm.input.error}}</tt><br/>
+ <tt>myForm.valid = {{myForm.valid}}</tt><br/>
+ <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/>
+ <tt>myForm.error.EMAIL = {{!!myForm.error.EMAIL}}</tt><br/>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('me@example.com');
+ expect(binding('myForm.input.valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+
+ it('should be invalid if not email', function() {
+ input('text').enter('xxx');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+ 'email': emailInputType,
+
+
+ /**
+ * @ngdoc inputType
+ * @name angular.module.ng.$compileProvider.directive.input.radio
+ *
+ * @description
+ * HTML radio button.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string} value The value to which the expression should be set when selected.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} ng:change Angular expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.color = 'blue';
+ }
+ </script>
+ <form name="myForm" ng:controller="Ctrl">
+ <input type="radio" ng:model="color" value="red"> Red <br/>
+ <input type="radio" ng:model="color" value="green"> Green <br/>
+ <input type="radio" ng:model="color" value="blue"> Blue <br/>
+ <tt>color = {{color}}</tt><br/>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should change state', function() {
+ expect(binding('color')).toEqual('blue');
+
+ input('color').select('red');
+ expect(binding('color')).toEqual('red');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+ 'radio': radioInputType,
+
+
+ /**
+ * @ngdoc inputType
+ * @name angular.module.ng.$compileProvider.directive.input.checkbox
+ *
+ * @description
+ * HTML checkbox.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} ng:true-value The value to which the expression should be set when selected.
+ * @param {string=} ng:false-value The value to which the expression should be set when not selected.
+ * @param {string=} ng:change Angular expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.value1 = true;
+ $scope.value2 = 'YES'
+ }
+ </script>
+ <form name="myForm" ng:controller="Ctrl">
+ Value1: <input type="checkbox" ng:model="value1"> <br/>
+ Value2: <input type="checkbox" ng:model="value2"
+ ng:true-value="YES" ng:false-value="NO"> <br/>
+ <tt>value1 = {{value1}}</tt><br/>
+ <tt>value2 = {{value2}}</tt><br/>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should change state', function() {
+ expect(binding('value1')).toEqual('true');
+ expect(binding('value2')).toEqual('YES');
+
+ input('value1').check();
+ input('value2').check();
+ expect(binding('value1')).toEqual('false');
+ expect(binding('value2')).toEqual('NO');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+ 'checkbox': checkboxInputType,
+
+ 'hidden': noop,
+ 'button': noop,
+ 'submit': noop,
+ 'reset': noop
+};
+
+
+function isEmpty(value) {
+ return isUndefined(value) || value === '' || value === null || value !== value;
+}
+
+
+function textInputType(scope, element, attr, ctrl) {
+ element.bind('blur', function() {
+ var touched = ctrl.touch(),
+ value = trim(element.val());
+
+ if (ctrl.viewValue !== value) {
+ scope.$apply(function() {
+ ctrl.read(value);
+ });
+ } else if (touched) {
+ scope.$apply();
+ }
+ });
+
+ ctrl.render = function() {
+ element.val(isEmpty(ctrl.viewValue) ? '' : ctrl.viewValue);
+ };
+
+ // pattern validator
+ var pattern = attr.ngPattern,
+ patternValidator;
+
+ var emit = function(regexp, value) {
+ if (isEmpty(value) || regexp.test(value)) {
+ ctrl.setValidity('PATTERN', true);
+ return value;
+ } else {
+ ctrl.setValidity('PATTERN', false);
+ return undefined;
+ }
+ };
+
+ if (pattern) {
+ if (pattern.match(/^\/(.*)\/$/)) {
+ pattern = new RegExp(pattern.substr(1, pattern.length - 2));
+ patternValidator = function(value) {
+ return emit(pattern, value)
+ };
+ } else {
+ patternValidator = function(value) {
+ var patternObj = scope.$eval(pattern);
+
+ if (!patternObj || !patternObj.test) {
+ throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj);
+ }
+ return emit(patternObj, value);
+ };
+ }
+
+ ctrl.formatters.push(patternValidator);
+ ctrl.parsers.push(patternValidator);
+ }
+
+ // min length validator
+ if (attr.ngMinlength) {
+ var minlength = parseInt(attr.ngMinlength, 10);
+ var minLengthValidator = function(value) {
+ if (!isEmpty(value) && value.length < minlength) {
+ ctrl.setValidity('MINLENGTH', false);
+ return undefined;
+ } else {
+ ctrl.setValidity('MINLENGTH', true);
+ return value;
+ }
+ };
+
+ ctrl.parsers.push(minLengthValidator);
+ ctrl.formatters.push(minLengthValidator);
+ }
+
+ // max length validator
+ if (attr.ngMaxlength) {
+ var maxlength = parseInt(attr.ngMaxlength, 10);
+ var maxLengthValidator = function(value) {
+ if (!isEmpty(value) && value.length > maxlength) {
+ ctrl.setValidity('MAXLENGTH', false);
+ return undefined;
+ } else {
+ ctrl.setValidity('MAXLENGTH', true);
+ return value;
+ }
+ };
+
+ ctrl.parsers.push(maxLengthValidator);
+ ctrl.formatters.push(maxLengthValidator);
+ }
+};
+
+function numberInputType(scope, element, attr, ctrl) {
+ textInputType(scope, element, attr, ctrl);
+
+ ctrl.parsers.push(function(value) {
+ var empty = isEmpty(value);
+ if (empty || NUMBER_REGEXP.test(value)) {
+ ctrl.setValidity('NUMBER', true);
+ return value === '' ? null : (empty ? value : parseFloat(value));
+ } else {
+ ctrl.setValidity('NUMBER', false);
+ return undefined;
+ }
+ });
+
+ ctrl.formatters.push(function(value) {
+ return isEmpty(value) ? '' : '' + value;
+ });
+
+ if (attr.min) {
+ var min = parseFloat(attr.min);
+ var minValidator = function(value) {
+ if (!isEmpty(value) && value < min) {
+ ctrl.setValidity('MIN', false);
+ return undefined;
+ } else {
+ ctrl.setValidity('MIN', true);
+ return value;
+ }
+ };
+
+ ctrl.parsers.push(minValidator);
+ ctrl.formatters.push(minValidator);
+ }
+
+ if (attr.max) {
+ var max = parseFloat(attr.max);
+ var maxValidator = function(value) {
+ if (!isEmpty(value) && value > max) {
+ ctrl.setValidity('MAX', false);
+ return undefined;
+ } else {
+ ctrl.setValidity('MAX', true);
+ return value;
+ }
+ };
+
+ ctrl.parsers.push(maxValidator);
+ ctrl.formatters.push(maxValidator);
+ }
+
+ ctrl.formatters.push(function(value) {
+
+ if (isEmpty(value) || isNumber(value)) {
+ ctrl.setValidity('NUMBER', true);
+ return value;
+ } else {
+ ctrl.setValidity('NUMBER', false);
+ return undefined;
+ }
+ });
+}
+
+function urlInputType(scope, element, attr, ctrl) {
+ textInputType(scope, element, attr, ctrl);
+
+ var urlValidator = function(value) {
+ if (isEmpty(value) || URL_REGEXP.test(value)) {
+ ctrl.setValidity('URL', true);
+ return value;
+ } else {
+ ctrl.setValidity('URL', false);
+ return undefined;
+ }
+ };
+
+ ctrl.formatters.push(urlValidator);
+ ctrl.parsers.push(urlValidator);
+}
+
+function emailInputType(scope, element, attr, ctrl) {
+ textInputType(scope, element, attr, ctrl);
+
+ var emailValidator = function(value) {
+ if (isEmpty(value) || EMAIL_REGEXP.test(value)) {
+ ctrl.setValidity('EMAIL', true);
+ return value;
+ } else {
+ ctrl.setValidity('EMAIL', false);
+ return undefined;
+ }
+ };
+
+ ctrl.formatters.push(emailValidator);
+ ctrl.parsers.push(emailValidator);
+}
+
+function radioInputType(scope, element, attr, ctrl) {
+ // correct the name
+ element.attr('name', attr.id + '@' + attr.name);
+
+ element.bind('click', function() {
+ if (element[0].checked) {
+ scope.$apply(function() {
+ ctrl.touch();
+ ctrl.read(attr.value);
+ });
+ };
+ });
+
+ ctrl.render = function() {
+ var value = attr.value;
+ element[0].checked = isDefined(value) && (value == ctrl.viewValue);
+ };
+}
+
+function checkboxInputType(scope, element, attr, ctrl) {
+ var trueValue = attr.ngTrueValue,
+ falseValue = attr.ngFalseValue;
+
+ if (!isString(trueValue)) trueValue = true;
+ if (!isString(falseValue)) falseValue = false;
+
+ element.bind('click', function() {
+ scope.$apply(function() {
+ ctrl.touch();
+ ctrl.read(element[0].checked);
+ });
+ });
+
+ ctrl.render = function() {
+ element[0].checked = ctrl.viewValue;
+ };
+
+ ctrl.formatters.push(function(value) {
+ return value === trueValue;
+ });
+
+ ctrl.parsers.push(function(value) {
+ return value ? trueValue : falseValue;
+ });
+}
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.textarea
+ *
+ * @description
+ * HTML textarea element widget with angular data-binding. The data-binding and validation
+ * properties of this element are exactly the same as those of the
+ * {@link angular.module.ng.$compileProvider.directive.input input element}.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than
+ * minlength.
+ * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than
+ * maxlength.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ * @param {string=} ng:change Angular expression to be executed when input changes due to user
+ * interaction with the input element.
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.input
+ *
+ * @description
+ * HTML input element widget with angular data-binding. Input widget follows HTML5 input types
+ * and polyfills the HTML5 validation behavior for older browsers.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than
+ * minlength.
+ * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than
+ * maxlength.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ * @param {string=} ng:change Angular expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.user = {name: 'guest', last: 'visitor'};
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ User name: <input type="text" name="userName" ng:model="user.name" required>
+ <span class="error" ng:show="myForm.userName.error.REQUIRED">
+ Required!</span><br>
+ Last name: <input type="text" name="lastName" ng:model="user.last"
+ ng:minlength="3" ng:maxlength="10">
+ <span class="error" ng:show="myForm.lastName.error.MINLENGTH">
+ Too short!</span>
+ <span class="error" ng:show="myForm.lastName.error.MAXLENGTH">
+ Too long!</span><br>
+ </form>
+ <hr>
+ <tt>user = {{user}}</tt><br/>
+ <tt>myForm.userName.valid = {{myForm.userName.valid}}</tt><br>
+ <tt>myForm.userName.error = {{myForm.userName.error}}</tt><br>
+ <tt>myForm.lastName.valid = {{myForm.lastName.valid}}</tt><br>
+ <tt>myForm.userName.error = {{myForm.lastName.error}}</tt><br>
+ <tt>myForm.valid = {{myForm.valid}}</tt><br>
+ <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br>
+ <tt>myForm.error.MINLENGTH = {{!!myForm.error.MINLENGTH}}</tt><br>
+ <tt>myForm.error.MAXLENGTH = {{!!myForm.error.MAXLENGTH}}</tt><br>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('user')).toEqual('{"last":"visitor","name":"guest"}');
+ expect(binding('myForm.userName.valid')).toEqual('true');
+ expect(binding('myForm.valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty when required', function() {
+ input('user.name').enter('');
+ expect(binding('user')).toEqual('{"last":"visitor","name":null}');
+ expect(binding('myForm.userName.valid')).toEqual('false');
+ expect(binding('myForm.valid')).toEqual('false');
+ });
+
+ it('should be valid if empty when min length is set', function() {
+ input('user.last').enter('');
+ expect(binding('user')).toEqual('{"last":"","name":"guest"}');
+ expect(binding('myForm.lastName.valid')).toEqual('true');
+ expect(binding('myForm.valid')).toEqual('true');
+ });
+
+ it('should be invalid if less than required min length', function() {
+ input('user.last').enter('xx');
+ expect(binding('user')).toEqual('{"last":"visitor","name":"guest"}');
+ expect(binding('myForm.lastName.valid')).toEqual('false');
+ expect(binding('myForm.lastName.error')).toMatch(/MINLENGTH/);
+ expect(binding('myForm.valid')).toEqual('false');
+ });
+
+ it('should be valid if longer than max length', function() {
+ input('user.last').enter('some ridiculously long name');
+ expect(binding('user'))
+ .toEqual('{"last":"visitor","name":"guest"}');
+ expect(binding('myForm.lastName.valid')).toEqual('false');
+ expect(binding('myForm.lastName.error')).toMatch(/MAXLENGTH/);
+ expect(binding('myForm.valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var inputDirective = [function() {
+ return {
+ restrict: 'E',
+ require: '?ngModel',
+ link: function(scope, element, attr, ctrl) {
+ if (ctrl) {
+ (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl);
+ }
+ }
+ };
+}];
+
+
+/**
+ * @ngdoc object
+ * @name angular.module.ng.$compileProvider.directive.ng:model.NgModelController
+ *
+ * @property {string} viewValue Actual string value in the view.
+ * @property {*} modelValue The value in the model, that the widget is bound to.
+ * @property {Array.<Function>} parsers Whenever the widget reads value from the DOM, it executes
+ * all of these functions to sanitize / convert the value as well as validate.
+ *
+ * @property {Array.<Function>} formatters Wheneveer the model value changes, it executes all of
+ * these functions to convert the value as well as validate.
+ *
+ * @property {Object} error An bject hash with all errors as keys.
+ *
+ * @property {boolean} pristine True if user has not interacted with the widget yet.
+ * @property {boolean} dirty True if user has already interacted with the widget.
+ * @property {boolean} valid True if there is no error.
+ * @property {boolean} invalid True if at least one error on the widget.
+ *
+ * @description
+ *
+ */
+var NgModelController = ['$scope', '$exceptionHandler', 'ngModel',
+ function($scope, $exceptionHandler, ngModel) {
+ this.viewValue = Number.NaN;
+ this.modelValue = Number.NaN;
+ this.parsers = [];
+ this.formatters = [];
+ this.error = {};
+ this.pristine = true;
+ this.dirty = false;
+ this.valid = true;
+ this.invalid = false;
+ this.render = noop;
+
+
+ /**
+ * @ngdoc function
+ * @name angular.module.ng.$compileProvider.directive.ng:model.NgModelController#touch
+ * @methodOf angular.module.ng.$compileProvider.directive.ng:model.NgModelController
+ *
+ * @return {boolean} Whether it did change state.
+ *
+ * @description
+ * This method should be called from within a DOM event handler.
+ * For example {@link angular.module.ng.$compileProvider.directive.input input} or
+ * {@link angular.module.ng.$compileProvider.directive.select select} directives call it.
+ *
+ * It changes state to `dirty` and emits `$viewTouch` event if the state was `pristine` before.
+ */
+ this.touch = function() {
+ if (this.dirty) return false;
+
+ this.dirty = true;
+ this.pristine = false;
+ try {
+ $scope.$emit('$viewTouch');
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ return true;
+ };
+
+
+ /**
+ * @ngdoc function
+ * @name angular.module.ng.$compileProvider.directive.ng:model.NgModelController#setValidity
+ * @methodOf angular.module.ng.$compileProvider.directive.ng:model.NgModelController
+ *
+ * @description
+ * Change the validity state, and notifies the form when the widget changes validity. (i.e. does
+ * not emit `$invalid` if given validator is already marked as invalid).
+ *
+ * This method should be called by validators - ie the parser or formatter method.
+ *
+ * @param {string} name Name of the validator.
+ * @param {boolean} isValid Whether it should $emit `$valid` (true) or `$invalid` (false) event.
+ */
+ this.setValidity = function(name, isValid) {
+
+ if (!isValid && this.error[name]) return;
+ if (isValid && !this.error[name]) return;
+
+ if (isValid) {
+ delete this.error[name];
+ if (equals(this.error, {})) {
+ this.valid = true;
+ this.invalid = false;
+ }
+ } else {
+ this.error[name] = true;
+ this.invalid = true;
+ this.valid = false;
+ }
+
+ return $scope.$emit(isValid ? '$valid' : '$invalid', name, this);
+ };
+
+
+ /**
+ * @ngdoc function
+ * @name angular.module.ng.$compileProvider.directive.ng:model.NgModelController#read
+ * @methodOf angular.module.ng.$compileProvider.directive.ng:model.NgModelController
+ *
+ * @description
+ * Read a value from view.
+ *
+ * This method should be called from within a DOM event handler.
+ * For example {@link angular.module.ng.$compileProvider.directive.input input} or
+ * {@link angular.module.ng.$compileProvider.directive.select select} directives call it.
+ *
+ * It internally calls all `formatters` and if resulted value is valid, update the model and emits
+ * `$viewChange` event afterwards.
+ *
+ * @param {string} value Value from the view
+ */
+ this.read = function(value) {
+ this.viewValue = value;
+
+ forEach(this.parsers, function(fn) {
+ value = fn(value);
+ });
+
+ if (isDefined(value) && this.model !== value) {
+ this.modelValue = value;
+ ngModel(value);
+ $scope.$emit('$viewChange', value, this);
+ }
+ };
+
+ // model -> value
+ var ctrl = this;
+ $scope.$watch(function() {
+ return ngModel();
+ }, function(value) {
+
+ // ignore change from view
+ if (ctrl.modelValue === value) return;
+
+ var formatters = ctrl.formatters,
+ idx = formatters.length;
+
+ ctrl.modelValue = value;
+ while(idx--) {
+ value = formatters[idx](value);
+ }
+
+ if (isDefined(value) && ctrl.viewValue !== value) {
+ ctrl.viewValue = value;
+ ctrl.render();
+ }
+ });
+}];
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:model
+ *
+ * @element input
+ *
+ * @description
+ * Is directive that tells Angular to do two-way data binding. It works together with `input`,
+ * `select`, `textarea`. You can easily write your own directives to use `ng:model` as well.
+ *
+ * `ng:model` is responsible for:
+ *
+ * - binding the view into the model, which other directives such as `input`, `textarea` or `select`
+ * require,
+ * - providing validation behavior (i.e. required, number, email, url),
+ * - keeping state of the widget (valid/invalid, dirty/pristine, validation errors),
+ * - setting related css class onto the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`),
+ * - register the widget with parent {@link angular.module.ng.$compileProvider.directive.form form}.
+ *
+ * For basic examples, how to use `ng:model`, see:
+ *
+ * - {@link angular.module.ng.$compileProvider.directive.input input}
+ * - {@link angular.module.ng.$compileProvider.directive.input.text text}
+ * - {@link angular.module.ng.$compileProvider.directive.input.checkbox checkbox}
+ * - {@link angular.module.ng.$compileProvider.directive.input.radio radio}
+ * - {@link angular.module.ng.$compileProvider.directive.input.number number}
+ * - {@link angular.module.ng.$compileProvider.directive.input.email email}
+ * - {@link angular.module.ng.$compileProvider.directive.input.url url}
+ * - {@link angular.module.ng.$compileProvider.directive.select select}
+ * - {@link angular.module.ng.$compileProvider.directive.textarea textarea}
+ *
+ */
+var ngModelDirective = [function() {
+ return {
+ inject: {
+ ngModel: 'accessor'
+ },
+ require: ['ngModel', '^?form'],
+ controller: NgModelController,
+ link: function(scope, element, attr, controllers) {
+ var modelController = controllers[0],
+ formController = controllers[1];
+
+ if (formController) {
+ formController.registerWidget(modelController, attr.name);
+ }
+
+ forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) {
+ scope.$watch(function() {
+ return modelController[name];
+ }, function(value) {
+ element[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ });
+
+ element.bind('$destroy', function() {
+ scope.$emit('$destroy', modelController);
+ });
+ }
+ };
+}];
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:change
+ *
+ * @description
+ * Evaluate given expression when user changes the input.
+ * The expression is not evaluated when the value change is coming from the model.
+ *
+ * Note, this directive requires `ng:model` to be present.
+ *
+ * @element input
+ *
+ * @example
+ * <doc:example>
+ * <doc:source>
+ * <script>
+ * function Controller($scope) {
+ * $scope.counter = 0;
+ * $scope.change = function() {
+ * $scope.counter++;
+ * };
+ * }
+ * </script>
+ * <div ng:controller="Controller">
+ * <input type="checkbox" ng:model="confirmed" ng:change="change()" id="ng-change-example1" />
+ * <input type="checkbox" ng:model="confirmed" id="ng-change-example2" />
+ * <label for="ng-change-example2">Confirmed</label><br />
+ * debug = {{confirmed}}<br />
+ * counter = {{counter}}
+ * </div>
+ * </doc:source>
+ * <doc:scenario>
+ * it('should evaluate the expression if changing from view', function() {
+ * expect(binding('counter')).toEqual('0');
+ * element('#ng-change-example1').click();
+ * expect(binding('counter')).toEqual('1');
+ * expect(binding('confirmed')).toEqual('true');
+ * });
+ *
+ * it('should not evaluate the expression if changing from model', function() {
+ * element('#ng-change-example2').click();
+ * expect(binding('counter')).toEqual('0');
+ * expect(binding('confirmed')).toEqual('true');
+ * });
+ * </doc:scenario>
+ * </doc:example>
+ */
+var ngChangeDirective = valueFn({
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ scope.$on('$viewChange', function(event, value, widget) {
+ if (ctrl === widget) scope.$eval(attr.ngChange);
+ });
+ }
+});
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:model-instant
+ *
+ * @element input
+ *
+ * @description
+ * By default, Angular udpates the model only on `blur` event - when the input looses focus.
+ * If you want to update after every key stroke, use `ng:model-instant`.
+ *
+ * @example
+ * <doc:example>
+ * <doc:source>
+ * First name: <input type="text" ng:model="firstName" /><br />
+ * Last name: <input type="text" ng:model="lastName" ng:model-instant /><br />
+ *
+ * First name ({{firstName}}) is only updated on `blur` event, but the last name ({{lastName}})
+ * is updated immediately, because of using `ng:model-instant`.
+ * </doc:source>
+ * <doc:scenario>
+ * it('should update first name on blur', function() {
+ * input('firstName').enter('santa', 'blur');
+ * expect(binding('firstName')).toEqual('santa');
+ * });
+ *
+ * it('should update last name immediately', function() {
+ * input('lastName').enter('santa', 'keydown');
+ * expect(binding('lastName')).toEqual('santa');
+ * });
+ * </doc:scenario>
+ * </doc:example>
+ */
+var ngModelInstantDirective = ['$browser', function($browser) {
+ return {
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ var handler = function() {
+ var touched = ctrl.touch(),
+ value = trim(element.val());
+
+ if (ctrl.viewValue !== value) {
+ scope.$apply(function() {
+ ctrl.read(value);
+ });
+ } else if (touched) {
+ scope.$apply();
+ }
+ };
+
+ var timeout;
+ element.bind('keydown', function(event) {
+ var key = event.keyCode;
+
+ // command modifiers arrows
+ if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
+
+ if (!timeout) {
+ timeout = $browser.defer(function() {
+ handler();
+ timeout = null;
+ });
+ }
+ });
+
+ element.bind('change input', handler);
+ }
+ };
+}];
+
+
+var requiredDirective = [function() {
+ return {
+ require: '?ngModel',
+ link: function(scope, elm, attr, ctrl) {
+ if (!ctrl) return;
+
+ var validator = function(value) {
+ if (attr.required && isEmpty(value)) {
+ ctrl.setValidity('REQUIRED', false);
+ return null;
+ } else {
+ ctrl.setValidity('REQUIRED', true);
+ return value;
+ }
+ };
+
+ ctrl.formatters.push(validator);
+ ctrl.parsers.unshift(validator);
+
+ attr.$observe('required', function() {
+ validator(ctrl.viewValue);
+ });
+ }
+ };
+}];
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:list
+ *
+ * @description
+ * Text input that converts between comma-seperated string into an array of strings.
+ *
+ * @element input
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.names = ['igor', 'misko', 'vojta'];
+ }
+ </script>
+ <form name="myForm" ng:controller="Ctrl">
+ List: <input name="input" ng:model="names" ng:list required>
+ <span class="error" ng:show="myForm.list.error.REQUIRED">
+ Required!</span>
+ <tt>names = {{names}}</tt><br/>
+ <tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/>
+ <tt>myForm.input.error = {{myForm.input.error}}</tt><br/>
+ <tt>myForm.valid = {{myForm.valid}}</tt><br/>
+ <tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('names')).toEqual('["igor","misko","vojta"]');
+ expect(binding('myForm.input.valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('names').enter('');
+ expect(binding('names')).toEqual('[]');
+ expect(binding('myForm.input.valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngListDirective = function() {
+ return {
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ var parse = function(viewValue) {
+ var list = [];
+
+ if (viewValue) {
+ forEach(viewValue.split(/\s*,\s*/), function(value) {
+ if (value) list.push(value);
+ });
+ }
+
+ return list;
+ };
+
+ ctrl.parsers.push(parse);
+ ctrl.formatters.push(function(value) {
+ if (isArray(value) && !equals(parse(ctrl.viewValue), value)) {
+ return value.join(', ');
+ }
+
+ return undefined;
+ });
+ }
+ };
+};
diff --git a/src/directive/ngBind.js b/src/directive/ngBind.js
new file mode 100644
index 00000000..37fabb94
--- /dev/null
+++ b/src/directive/ngBind.js
@@ -0,0 +1,249 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:bind
+ *
+ * @description
+ * The `ng:bind` attribute tells Angular to replace the text content of the specified HTML element
+ * with the value of a given expression, and to update the text content when the value of that
+ * expression changes.
+ *
+ * Typically, you don't use `ng:bind` directly, but instead you use the double curly markup like
+ * `{{ expression }}` and let the Angular compiler transform it to
+ * `<span ng:bind="expression"></span>` when the template is compiled.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate.
+ *
+ * @example
+ * Enter a name in the Live Preview text box; the greeting below the text box changes instantly.
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.name = 'Whirled';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Enter name: <input type="text" ng:model="name"> <br/>
+ Hello <span ng:bind="name"></span>!
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:bind', function() {
+ expect(using('.doc-example-live').binding('name')).toBe('Whirled');
+ using('.doc-example-live').input('name').enter('world');
+ expect(using('.doc-example-live').binding('name')).toBe('world');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngBindDirective = ngDirective(function(scope, element, attr) {
+ element.addClass('ng-binding').data('$binding', attr.ngBind);
+ scope.$watch(attr.ngBind, function(value) {
+ element.text(value == undefined ? '' : value);
+ });
+});
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:bind-html-unsafe
+ *
+ * @description
+ * Creates a binding that will innerHTML the result of evaluating the `expression` into the current
+ * element. *The innerHTML-ed content will not be sanitized!* You should use this directive only if
+ * {@link angular.module.ng.$compileProvider.directive.ng:bind-html ng:bind-html} directive is too
+ * restrictive and when you absolutely trust the source of the content you are binding to.
+ *
+ * See {@link angular.module.ng.$sanitize $sanitize} docs for examples.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate.
+ */
+var ngBindHtmlUnsafeDirective = ngDirective(function(scope, element, attr) {
+ element.addClass('ng-binding').data('$binding', attr.ngBindHtmlUnsafe);
+ scope.$watch(attr.ngBindHtmlUnsafe, function(value) {
+ element.html(value == undefined ? '' : value);
+ });
+});
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:bind-html
+ *
+ * @description
+ * Creates a binding that will sanitize the result of evaluating the `expression` with the
+ * {@link angular.module.ng.$sanitize $sanitize} service and innerHTML the result into the current
+ * element.
+ *
+ * See {@link angular.module.ng.$sanitize $sanitize} docs for examples.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate.
+ */
+var ngBindHtmlDirective = ['$sanitize', function($sanitize) {
+ return function(scope, element, attr) {
+ element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
+ scope.$watch(attr.ngBindHtml, function(value) {
+ if (value = $sanitize(value)) {
+ element.html(value);
+ }
+ });
+ }
+}];
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:bind-template
+ *
+ * @description
+ * The `ng:bind-template` attribute specifies that the element
+ * text should be replaced with the template in ng:bind-template.
+ * Unlike ng:bind the ng:bind-template can contain multiple `{{` `}}`
+ * expressions. (This is required since some HTML elements
+ * can not have SPAN elements such as TITLE, or OPTION to name a few.)
+ *
+ * @element ANY
+ * @param {string} template of form
+ * <tt>{{</tt> <tt>expression</tt> <tt>}}</tt> to eval.
+ *
+ * @example
+ * Try it here: enter text in text box and watch the greeting change.
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.salutation = 'Hello';
+ $scope.name = 'World';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Salutation: <input type="text" ng:model="salutation"><br/>
+ Name: <input type="text" ng:model="name"><br/>
+ <pre ng:bind-template="{{salutation}} {{name}}!"></pre>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:bind', function() {
+ expect(using('.doc-example-live').binding('salutation')).
+ toBe('Hello');
+ expect(using('.doc-example-live').binding('name')).
+ toBe('World');
+ using('.doc-example-live').input('salutation').enter('Greetings');
+ using('.doc-example-live').input('name').enter('user');
+ expect(using('.doc-example-live').binding('salutation')).
+ toBe('Greetings');
+ expect(using('.doc-example-live').binding('name')).
+ toBe('user');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngBindTemplateDirective = ['$interpolate', function($interpolate) {
+ return function(scope, element, attr) {
+ // TODO: move this to scenario runner
+ var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate));
+ element.addClass('ng-binding').data('$binding', interpolateFn);
+ attr.$observe('ngBindTemplate', function(value) {
+ element.text(value);
+ });
+ }
+}];
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:bind-attr
+ *
+ * @description
+ * The `ng:bind-attr` attribute specifies that a
+ * {@link guide/dev_guide.templates.databinding databinding} should be created between a particular
+ * element attribute and a given expression. Unlike `ng:bind`, the `ng:bind-attr` contains one or
+ * more JSON key value pairs; each pair specifies an attribute and the
+ * {@link guide/dev_guide.expressions expression} to which it will be mapped.
+ *
+ * Instead of writing `ng:bind-attr` statements in your HTML, you can use double-curly markup to
+ * specify an <tt ng:non-bindable>{{expression}}</tt> for the value of an attribute.
+ * At compile time, the attribute is translated into an
+ * `<span ng:bind-attr="{attr:expression}"></span>`.
+ *
+ * The following HTML snippet shows how to specify `ng:bind-attr`:
+ * <pre>
+ * <a ng:bind-attr='{"href":"http://www.google.com/search?q={{query}}"}'>Google</a>
+ * </pre>
+ *
+ * This is cumbersome, so as we mentioned using double-curly markup is a prefered way of creating
+ * this binding:
+ * <pre>
+ * <a href="http://www.google.com/search?q={{query}}">Google</a>
+ * </pre>
+ *
+ * During compilation, the template with attribute markup gets translated to the ng:bind-attr form
+ * mentioned above.
+ *
+ * _Note_: You might want to consider using {@link angular.module.ng.$compileProvider.directive.ng:href ng:href} instead of
+ * `href` if the binding is present in the main application template (`index.html`) and you want to
+ * make sure that a user is not capable of clicking on raw/uncompiled link.
+ *
+ *
+ * @element ANY
+ * @param {string} attribute_json one or more JSON key-value pairs representing
+ * the attributes to replace with expressions. Each key matches an attribute
+ * which needs to be replaced. Each value is a text template of
+ * the attribute with the embedded
+ * <tt ng:non-bindable>{{expression}}</tt>s. Any number of
+ * key-value pairs can be specified.
+ *
+ * @example
+ * Enter a search string in the Live Preview text box and then click "Google". The search executes instantly.
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.query = 'AngularJS';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Google for:
+ <input type="text" ng:model="query"/>
+ <a ng:bind-attr='{"href":"http://www.google.com/search?q={{query}}"}'>
+ Google
+ </a> (ng:bind-attr) |
+ <a href="http://www.google.com/search?q={{query}}">Google</a>
+ (curly binding in attribute val)
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:bind-attr', function() {
+ expect(using('.doc-example-live').element('a').attr('href')).
+ toBe('http://www.google.com/search?q=AngularJS');
+ using('.doc-example-live').input('query').enter('google');
+ expect(using('.doc-example-live').element('a').attr('href')).
+ toBe('http://www.google.com/search?q=google');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+var ngBindAttrDirective = ['$interpolate', function($interpolate) {
+ return function(scope, element, attr) {
+ var lastValue = {};
+ var interpolateFns = {};
+ scope.$watch(function() {
+ var values = scope.$eval(attr.ngBindAttr);
+ for(var key in values) {
+ var exp = values[key],
+ fn = (interpolateFns[exp] ||
+ (interpolateFns[values[key]] = $interpolate(exp))),
+ value = fn(scope);
+ if (lastValue[key] !== value) {
+ attr.$set(key, lastValue[key] = value);
+ }
+ }
+ });
+ }
+}];
diff --git a/src/directive/ngClass.js b/src/directive/ngClass.js
new file mode 100644
index 00000000..2016d04f
--- /dev/null
+++ b/src/directive/ngClass.js
@@ -0,0 +1,143 @@
+'use strict';
+
+function classDirective(name, selector) {
+ name = 'ngClass' + name;
+ return ngDirective(function(scope, element, attr) {
+ scope.$watch(attr[name], function(newVal, oldVal) {
+ if (selector === true || scope.$index % 2 === selector) {
+ if (oldVal && (newVal !== oldVal)) {
+ if (isObject(oldVal) && !isArray(oldVal))
+ oldVal = map(oldVal, function(v, k) { if (v) return k });
+ element.removeClass(isArray(oldVal) ? oldVal.join(' ') : oldVal);
+ }
+ if (isObject(newVal) && !isArray(newVal))
+ newVal = map(newVal, function(v, k) { if (v) return k });
+ if (newVal) element.addClass(isArray(newVal) ? newVal.join(' ') : newVal); }
+ }, true);
+ });
+}
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:class
+ *
+ * @description
+ * The `ng:class` allows you to set CSS class on HTML element dynamically by databinding an
+ * expression that represents all classes to be added.
+ *
+ * The directive won't add duplicate classes if a particular class was already set.
+ *
+ * When the expression changes, the previously added classes are removed and only then the classes
+ * new classes are added.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval. The result
+ * of the evaluation can be a string representing space delimited class
+ * names, an array, or a map of class names to boolean values.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <input type="button" value="set" ng:click="myVar='ng-invalid'">
+ <input type="button" value="clear" ng:click="myVar=''">
+ <br>
+ <span ng:class="myVar">Sample Text &nbsp;&nbsp;&nbsp;&nbsp;</span>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:class', function() {
+ expect(element('.doc-example-live span').prop('className')).not().
+ toMatch(/ng-invalid/);
+
+ using('.doc-example-live').element(':button:first').click();
+
+ expect(element('.doc-example-live span').prop('className')).
+ toMatch(/ng-invalid/);
+
+ using('.doc-example-live').element(':button:last').click();
+
+ expect(element('.doc-example-live span').prop('className')).not().
+ toMatch(/ng-invalid/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngClassDirective = classDirective('', true);
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:class-odd
+ *
+ * @description
+ * The `ng:class-odd` and `ng:class-even` works exactly as
+ * {@link angular.module.ng.$compileProvider.directive.ng:class ng:class}, except it works in conjunction with `ng:repeat` and
+ * takes affect only on odd (even) rows.
+ *
+ * This directive can be applied only within a scope of an
+ * {@link angular.module.ng.$compileProvider.directive.ng:repeat ng:repeat}.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval. The result
+ * of the evaluation can be a string representing space delimited class names or an array.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <ol ng:init="names=['John', 'Mary', 'Cate', 'Suz']">
+ <li ng:repeat="name in names">
+ <span ng:class-odd="'ng-format-negative'"
+ ng:class-even="'ng-invalid'">
+ {{name}} &nbsp; &nbsp; &nbsp;
+ </span>
+ </li>
+ </ol>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:class-odd and ng:class-even', function() {
+ expect(element('.doc-example-live li:first span').prop('className')).
+ toMatch(/ng-format-negative/);
+ expect(element('.doc-example-live li:last span').prop('className')).
+ toMatch(/ng-invalid/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngClassOddDirective = classDirective('Odd', 0);
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:class-even
+ *
+ * @description
+ * The `ng:class-odd` and `ng:class-even` works exactly as
+ * {@link angular.module.ng.$compileProvider.directive.ng:class ng:class}, except it works in conjunction with `ng:repeat` and
+ * takes affect only on odd (even) rows.
+ *
+ * This directive can be applied only within a scope of an
+ * {@link angular.module.ng.$compileProvider.directive.ng:repeat ng:repeat}.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval. The result
+ * of the evaluation can be a string representing space delimited class names or an array.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <ol ng:init="names=['John', 'Mary', 'Cate', 'Suz']">
+ <li ng:repeat="name in names">
+ <span ng:class-odd="'odd'" ng:class-even="'even'">
+ {{name}} &nbsp; &nbsp; &nbsp;
+ </span>
+ </li>
+ </ol>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:class-odd and ng:class-even', function() {
+ expect(element('.doc-example-live li:first span').prop('className')).
+ toMatch(/odd/);
+ expect(element('.doc-example-live li:last span').prop('className')).
+ toMatch(/even/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngClassEvenDirective = classDirective('Even', 1);
diff --git a/src/directive/ngCloak.js b/src/directive/ngCloak.js
new file mode 100644
index 00000000..b4fe6708
--- /dev/null
+++ b/src/directive/ngCloak.js
@@ -0,0 +1,61 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:cloak
+ *
+ * @description
+ * The `ng:cloak` directive is used to prevent the Angular html template from being briefly
+ * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this
+ * directive to avoid the undesirable flicker effect caused by the html template display.
+ *
+ * The directive can be applied to the `<body>` element, but typically a fine-grained application is
+ * prefered in order to benefit from progressive rendering of the browser view.
+ *
+ * `ng:cloak` works in cooperation with a css rule that is embedded within `angular.js` and
+ * `angular.min.js` files. Following is the css rule:
+ *
+ * <pre>
+ * [ng\:cloak], .ng-cloak {
+ * display: none;
+ * }
+ * </pre>
+ *
+ * When this css rule is loaded by the browser, all html elements (including their children) that
+ * are tagged with the `ng:cloak` directive are hidden. When Angular comes across this directive
+ * during the compilation of the template it deletes the `ng:cloak` element attribute, which
+ * makes the compiled element visible.
+ *
+ * For the best result, `angular.js` script must be loaded in the head section of the html file;
+ * alternatively, the css rule (above) must be included in the external stylesheet of the
+ * application.
+ *
+ * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they
+ * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css
+ * class `ng-cloak` in addition to `ng:cloak` directive as shown in the example below.
+ *
+ * @element ANY
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <div id="template1" ng:cloak>{{ 'hello' }}</div>
+ <div id="template2" ng:cloak class="ng-cloak">{{ 'hello IE7' }}</div>
+ </doc:source>
+ <doc:scenario>
+ it('should remove the template directive and css class', function() {
+ expect(element('.doc-example-live #template1').attr('ng:cloak')).
+ not().toBeDefined();
+ expect(element('.doc-example-live #template2').attr('ng:cloak')).
+ not().toBeDefined();
+ });
+ </doc:scenario>
+ </doc:example>
+ *
+ */
+var ngCloakDirective = ngDirective({
+ compile: function(element, attr) {
+ attr.$set('ngCloak', undefined);
+ element.removeClass('ng-cloak');
+ }
+});
diff --git a/src/directive/ngController.js b/src/directive/ngController.js
new file mode 100644
index 00000000..e17a97cd
--- /dev/null
+++ b/src/directive/ngController.js
@@ -0,0 +1,103 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:controller
+ *
+ * @description
+ * The `ng:controller` directive assigns behavior to a scope. This is a key aspect of how angular
+ * supports the principles behind the Model-View-Controller design pattern.
+ *
+ * MVC components in angular:
+ *
+ * * Model — The Model is data in scope properties; scopes are attached to the DOM.
+ * * View — The template (HTML with data bindings) is rendered into the View.
+ * * Controller — The `ng:controller` directive specifies a Controller class; the class has
+ * methods that typically express the business logic behind the application.
+ *
+ * Note that an alternative way to define controllers is via the `{@link angular.module.ng.$route}`
+ * service.
+ *
+ * @element ANY
+ * @scope
+ * @param {expression} expression Name of a globally accessible constructor function or an
+ * {@link guide/dev_guide.expressions expression} that on the current scope evaluates to a
+ * constructor function.
+ *
+ * @example
+ * Here is a simple form for editing user contact information. Adding, removing, clearing, and
+ * greeting are methods declared on the controller (see source tab). These methods can
+ * easily be called from the angular markup. Notice that the scope becomes the `this` for the
+ * controller's instance. This allows for easy access to the view data from the controller. Also
+ * notice that any changes to the data are automatically reflected in the View without the need
+ * for a manual update.
+ <doc:example>
+ <doc:source>
+ <script type="text/javascript">
+ function SettingsController($scope) {
+ $scope.name = "John Smith";
+ $scope.contacts = [
+ {type:'phone', value:'408 555 1212'},
+ {type:'email', value:'john.smith@example.org'} ];
+
+ $scope.greet = function() {
+ alert(this.name);
+ };
+
+ $scope.addContact = function() {
+ this.contacts.push({type:'email', value:'yourname@example.org'});
+ };
+
+ $scope.removeContact = function(contactToRemove) {
+ var index = this.contacts.indexOf(contactToRemove);
+ this.contacts.splice(index, 1);
+ };
+
+ $scope.clearContact = function(contact) {
+ contact.type = 'phone';
+ contact.value = '';
+ };
+ }
+ </script>
+ <div ng:controller="SettingsController">
+ Name: <input type="text" ng:model="name"/>
+ [ <a href="" ng:click="greet()">greet</a> ]<br/>
+ Contact:
+ <ul>
+ <li ng:repeat="contact in contacts">
+ <select ng:model="contact.type">
+ <option>phone</option>
+ <option>email</option>
+ </select>
+ <input type="text" ng:model="contact.value"/>
+ [ <a href="" ng:click="clearContact(contact)">clear</a>
+ | <a href="" ng:click="removeContact(contact)">X</a> ]
+ </li>
+ <li>[ <a href="" ng:click="addContact()">add</a> ]</li>
+ </ul>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check controller', function() {
+ expect(element('.doc-example-live div>:input').val()).toBe('John Smith');
+ expect(element('.doc-example-live li:nth-child(1) input').val())
+ .toBe('408 555 1212');
+ expect(element('.doc-example-live li:nth-child(2) input').val())
+ .toBe('john.smith@example.org');
+
+ element('.doc-example-live li:first a:contains("clear")').click();
+ expect(element('.doc-example-live li:first input').val()).toBe('');
+
+ element('.doc-example-live li:last a:contains("add")').click();
+ expect(element('.doc-example-live li:nth-child(3) input').val())
+ .toBe('yourname@example.org');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngControllerDirective = ['$controller', '$window', function($controller, $window) {
+ return {
+ scope: true,
+ controller: '@'
+ }
+}];
diff --git a/src/directive/ngEventDirs.js b/src/directive/ngEventDirs.js
new file mode 100644
index 00000000..0815229d
--- /dev/null
+++ b/src/directive/ngEventDirs.js
@@ -0,0 +1,222 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:click
+ *
+ * @description
+ * The ng:click allows you to specify custom behavior when
+ * element is clicked.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * click. (Event object is available as `$event`)
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <button ng:click="count = count + 1" ng:init="count=0">
+ Increment
+ </button>
+ count: {{count}}
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:click', function() {
+ expect(binding('count')).toBe('0');
+ element('.doc-example-live :button').click();
+ expect(binding('count')).toBe('1');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+/*
+ * A directive that allows creation of custom onclick handlers that are defined as angular
+ * expressions and are compiled and executed within the current scope.
+ *
+ * Events that are handled via these handler are always configured not to propagate further.
+ */
+var ngEventDirectives = {};
+forEach(
+ 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave'.split(' '),
+ function(name) {
+ var directiveName = directiveNormalize('ng-' + name);
+ ngEventDirectives[directiveName] = ['$parse', function($parse) {
+ return function(scope, element, attr) {
+ var fn = $parse(attr[directiveName]);
+ element.bind(lowercase(name), function(event) {
+ scope.$apply(function() {
+ fn(scope, {$event:event});
+ });
+ });
+ };
+ }];
+ }
+);
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:dblclick
+ *
+ * @description
+ * The ng:dblclick allows you to specify custom behavior on dblclick event.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * dblclick. (Event object is available as `$event`)
+ *
+ * @example
+ * See {@link angular.module.ng.$compileProvider.directive.ng:click ng:click}
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:mousedown
+ *
+ * @description
+ * The ng:mousedown allows you to specify custom behavior on mousedown event.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * mousedown. (Event object is available as `$event`)
+ *
+ * @example
+ * See {@link angular.module.ng.$compileProvider.directive.ng:click ng:click}
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:mouseup
+ *
+ * @description
+ * Specify custom behavior on mouseup event.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * mouseup. (Event object is available as `$event`)
+ *
+ * @example
+ * See {@link angular.module.ng.$compileProvider.directive.ng:click ng:click}
+ */
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:mouseover
+ *
+ * @description
+ * Specify custom behavior on mouseover event.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * mouseover. (Event object is available as `$event`)
+ *
+ * @example
+ * See {@link angular.module.ng.$compileProvider.directive.ng:click ng:click}
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:mouseenter
+ *
+ * @description
+ * Specify custom behavior on mouseenter event.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * mouseenter. (Event object is available as `$event`)
+ *
+ * @example
+ * See {@link angular.module.ng.$compileProvider.directive.ng:click ng:click}
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:mouseleave
+ *
+ * @description
+ * Specify custom behavior on mouseleave event.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * mouseleave. (Event object is available as `$event`)
+ *
+ * @example
+ * See {@link angular.module.ng.$compileProvider.directive.ng:click ng:click}
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:mousemove
+ *
+ * @description
+ * Specify custom behavior on mousemove event.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to evaluate upon
+ * mousemove. (Event object is available as `$event`)
+ *
+ * @example
+ * See {@link angular.module.ng.$compileProvider.directive.ng:click ng:click}
+ */
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:submit
+ *
+ * @description
+ * Enables binding angular expressions to onsubmit events.
+ *
+ * Additionally it prevents the default action (which for form means sending the request to the
+ * server and reloading the current page).
+ *
+ * @element form
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.list = [];
+ $scope.text = 'hello';
+ $scope.submit = function() {
+ if (this.text) {
+ this.list.push(this.text);
+ this.text = '';
+ }
+ };
+ }
+ </script>
+ <form ng:submit="submit()" ng:controller="Ctrl">
+ Enter text and hit enter:
+ <input type="text" ng:model="text" name="text" />
+ <input type="submit" id="submit" value="Submit" />
+ <pre>list={{list}}</pre>
+ </form>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:submit', function() {
+ expect(binding('list')).toBe('[]');
+ element('.doc-example-live #submit').click();
+ expect(binding('list')).toBe('["hello"]');
+ expect(input('text').val()).toBe('');
+ });
+ it('should ignore empty strings', function() {
+ expect(binding('list')).toBe('[]');
+ element('.doc-example-live #submit').click();
+ element('.doc-example-live #submit').click();
+ expect(binding('list')).toBe('["hello"]');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngSubmitDirective = ngDirective(function(scope, element, attrs) {
+ element.bind('submit', function() {
+ scope.$apply(attrs.ngSubmit);
+ });
+});
diff --git a/src/directive/ngInclude.js b/src/directive/ngInclude.js
new file mode 100644
index 00000000..08b14488
--- /dev/null
+++ b/src/directive/ngInclude.js
@@ -0,0 +1,119 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:include
+ * @restrict EA
+ *
+ * @description
+ * Fetches, compiles and includes an external HTML fragment.
+ *
+ * Keep in mind that Same Origin Policy applies to included resources
+ * (e.g. ng:include won't work for file:// access).
+ *
+ * @scope
+ *
+ * @param {string} src angular expression evaluating to URL. If the source is a string constant,
+ * make sure you wrap it in quotes, e.g. `src="'myPartialTemplate.html'"`.
+ * @param {Scope=} [scope=new_child_scope] optional expression which evaluates to an
+ * instance of angular.module.ng.$rootScope.Scope to set the HTML fragment to.
+ * @param {string=} onload Expression to evaluate when a new partial is loaded.
+ *
+ * @param {string=} autoscroll Whether `ng:include` should call {@link angular.module.ng.$anchorScroll
+ * $anchorScroll} to scroll the viewport after the content is loaded.
+ *
+ * - If the attribute is not set, disable scrolling.
+ * - If the attribute is set without value, enable scrolling.
+ * - Otherwise enable scrolling only if the expression evaluates to truthy value.
+ *
+ * @example
+ <doc:example>
+ <doc:source jsfiddle="false">
+ <script>
+ function Ctrl($scope) {
+ $scope.templates =
+ [ { name: 'template1.html', url: 'examples/ng-include/template1.html'}
+ , { name: 'template2.html', url: 'examples/ng-include/template2.html'} ];
+ $scope.template = $scope.templates[0];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <select ng:model="template" ng:options="t.name for t in templates">
+ <option value="">(blank)</option>
+ </select>
+ url of the template: <tt><a href="{{template.url}}">{{template.url}}</a></tt>
+ <hr/>
+ <div ng-include src="template.url"></div>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should load template1.html', function() {
+ expect(element('.doc-example-live [ng-include]').text()).
+ toBe('Content of template1.html\n');
+ });
+ it('should load template2.html', function() {
+ select('template').option('1');
+ expect(element('.doc-example-live [ng-include]').text()).
+ toBe('Content of template2.html\n');
+ });
+ it('should change to blank', function() {
+ select('template').option('');
+ expect(element('.doc-example-live [ng-include]').text()).toEqual('');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile',
+ function($http, $templateCache, $anchorScroll, $compile) {
+ return {
+ restrict: 'EA',
+ compile: function(element, attr) {
+ var srcExp = attr.src,
+ scopeExp = attr.scope || '',
+ autoScrollExp = attr.autoscroll;
+
+ return function(scope, element, attr) {
+ var changeCounter = 0,
+ childScope;
+
+ function incrementChange() { changeCounter++;}
+ scope.$watch(srcExp, incrementChange);
+ scope.$watch(function() {
+ var includeScope = scope.$eval(scopeExp);
+ if (includeScope) return includeScope.$id;
+ }, incrementChange);
+ scope.$watch(function() {return changeCounter;}, function(newChangeCounter) {
+ var src = scope.$eval(srcExp),
+ useScope = scope.$eval(scopeExp);
+
+ function clearContent() {
+ // if this callback is still desired
+ if (newChangeCounter === changeCounter) {
+ if (childScope) childScope.$destroy();
+ childScope = null;
+ element.html('');
+ }
+ }
+
+ if (src) {
+ $http.get(src, {cache: $templateCache}).success(function(response) {
+ // if this callback is still desired
+ if (newChangeCounter === changeCounter) {
+ element.html(response);
+ if (childScope) childScope.$destroy();
+ childScope = useScope ? useScope : scope.$new();
+ $compile(element.contents())(childScope);
+ if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
+ $anchorScroll();
+ }
+ scope.$emit('$contentLoaded');
+ }
+ }).error(clearContent);
+ } else {
+ clearContent();
+ }
+ });
+ };
+ }
+ }
+}];
diff --git a/src/directive/ngInit.js b/src/directive/ngInit.js
new file mode 100644
index 00000000..cdab1cdb
--- /dev/null
+++ b/src/directive/ngInit.js
@@ -0,0 +1,37 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:init
+ *
+ * @description
+ * The `ng:init` attribute specifies initialization tasks to be executed
+ * before the template enters execution mode during bootstrap.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} to eval.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <div ng:init="greeting='Hello'; person='World'">
+ {{greeting}} {{person}}!
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check greeting', function() {
+ expect(binding('greeting')).toBe('Hello');
+ expect(binding('person')).toBe('World');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngInitDirective = ngDirective({
+ compile: function() {
+ return {
+ pre: function(scope, element, attrs) {
+ scope.$eval(attrs.ngInit);
+ }
+ }
+ }
+});
diff --git a/src/directive/ngNonBindable.js b/src/directive/ngNonBindable.js
new file mode 100644
index 00000000..b9857afa
--- /dev/null
+++ b/src/directive/ngNonBindable.js
@@ -0,0 +1,34 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:non-bindable
+ *
+ * @description
+ * Sometimes it is necessary to write code which looks like bindings but which should be left alone
+ * by angular. Use `ng:non-bindable` to make angular ignore a chunk of HTML.
+ *
+ * Note: `ng:non-bindable` looks like a directive, but is actually an attribute widget.
+ *
+ * @element ANY
+ *
+ * @example
+ * In this example there are two location where a simple binding (`{{}}`) is present, but the one
+ * wrapped in `ng:non-bindable` is left alone.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <div>Normal: {{1 + 2}}</div>
+ <div ng:non-bindable>Ignored: {{1 + 2}}</div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:non-bindable', function() {
+ expect(using('.doc-example-live').binding('1 + 2')).toBe('3');
+ expect(using('.doc-example-live').element('div:last').text()).
+ toMatch(/1 \+ 2/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngNonBindableDirective = ngDirective({ terminal: true });
diff --git a/src/directive/ngPluralize.js b/src/directive/ngPluralize.js
new file mode 100644
index 00000000..4a6f4a7e
--- /dev/null
+++ b/src/directive/ngPluralize.js
@@ -0,0 +1,204 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:pluralize
+ * @restrict EA
+ *
+ * @description
+ * # Overview
+ * ng:pluralize is a widget that displays messages according to en-US localization rules.
+ * These rules are bundled with angular.js and the rules can be overridden
+ * (see {@link guide/dev_guide.i18n Angular i18n} dev guide). You configure ng:pluralize by
+ * specifying the mappings between
+ * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
+ * plural categories} and the strings to be displayed.
+ *
+ * # Plural categories and explicit number rules
+ * There are two
+ * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
+ * plural categories} in Angular's default en-US locale: "one" and "other".
+ *
+ * While a pural category may match many numbers (for example, in en-US locale, "other" can match
+ * any number that is not 1), an explicit number rule can only match one number. For example, the
+ * explicit number rule for "3" matches the number 3. You will see the use of plural categories
+ * and explicit number rules throughout later parts of this documentation.
+ *
+ * # Configuring ng:pluralize
+ * You configure ng:pluralize by providing 2 attributes: `count` and `when`.
+ * You can also provide an optional attribute, `offset`.
+ *
+ * The value of the `count` attribute can be either a string or an {@link guide/dev_guide.expressions
+ * Angular expression}; these are evaluated on the current scope for its binded value.
+ *
+ * The `when` attribute specifies the mappings between plural categories and the actual
+ * string to be displayed. The value of the attribute should be a JSON object so that Angular
+ * can interpret it correctly.
+ *
+ * The following example shows how to configure ng:pluralize:
+ *
+ * <pre>
+ * <ng:pluralize count="personCount"
+ when="{'0': 'Nobody is viewing.',
+ * 'one': '1 person is viewing.',
+ * 'other': '{} people are viewing.'}">
+ * </ng:pluralize>
+ *</pre>
+ *
+ * In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not
+ * specify this rule, 0 would be matched to the "other" category and "0 people are viewing"
+ * would be shown instead of "Nobody is viewing". You can specify an explicit number rule for
+ * other numbers, for example 12, so that instead of showing "12 people are viewing", you can
+ * show "a dozen people are viewing".
+ *
+ * You can use a set of closed braces(`{}`) as a placeholder for the number that you want substituted
+ * into pluralized strings. In the previous example, Angular will replace `{}` with
+ * <span ng:non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder
+ * for <span ng:non-bindable>{{numberExpression}}</span>.
+ *
+ * # Configuring ng:pluralize with offset
+ * The `offset` attribute allows further customization of pluralized text, which can result in
+ * a better user experience. For example, instead of the message "4 people are viewing this document",
+ * you might display "John, Kate and 2 others are viewing this document".
+ * The offset attribute allows you to offset a number by any desired value.
+ * Let's take a look at an example:
+ *
+ * <pre>
+ * <ng:pluralize count="personCount" offset=2
+ * when="{'0': 'Nobody is viewing.',
+ * '1': '{{person1}} is viewing.',
+ * '2': '{{person1}} and {{person2}} are viewing.',
+ * 'one': '{{person1}}, {{person2}} and one other person are viewing.',
+ * 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
+ * </ng:pluralize>
+ * </pre>
+ *
+ * Notice that we are still using two plural categories(one, other), but we added
+ * three explicit number rules 0, 1 and 2.
+ * When one person, perhaps John, views the document, "John is viewing" will be shown.
+ * When three people view the document, no explicit number rule is found, so
+ * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category.
+ * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing"
+ * is shown.
+ *
+ * Note that when you specify offsets, you must provide explicit number rules for
+ * numbers from 0 up to and including the offset. If you use an offset of 3, for example,
+ * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for
+ * plural categories "one" and "other".
+ *
+ * @param {string|expression} count The variable to be bounded to.
+ * @param {string} when The mapping between plural category to its correspoding strings.
+ * @param {number=} offset Offset to deduct from the total number.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.person1 = 'Igor';
+ $scope.person2 = 'Misko';
+ $scope.personCount = 1;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Person 1:<input type="text" ng:model="person1" value="Igor" /><br/>
+ Person 2:<input type="text" ng:model="person2" value="Misko" /><br/>
+ Number of People:<input type="text" ng:model="personCount" value="1" /><br/>
+
+ <!--- Example with simple pluralization rules for en locale --->
+ Without Offset:
+ <ng-pluralize count="personCount"
+ when="{'0': 'Nobody is viewing.',
+ 'one': '1 person is viewing.',
+ 'other': '{} people are viewing.'}">
+ </ng-pluralize><br>
+
+ <!--- Example with offset --->
+ With Offset(2):
+ <ng-pluralize count="personCount" offset=2
+ when="{'0': 'Nobody is viewing.',
+ '1': '{{person1}} is viewing.',
+ '2': '{{person1}} and {{person2}} are viewing.',
+ 'one': '{{person1}}, {{person2}} and one other person are viewing.',
+ 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
+ </ng-pluralize>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should show correct pluralized string', function() {
+ expect(element('.doc-example-live ng-pluralize:first').text()).
+ toBe('1 person is viewing.');
+ expect(element('.doc-example-live ng-pluralize:last').text()).
+ toBe('Igor is viewing.');
+
+ using('.doc-example-live').input('personCount').enter('0');
+ expect(element('.doc-example-live ng-pluralize:first').text()).
+ toBe('Nobody is viewing.');
+ expect(element('.doc-example-live ng-pluralize:last').text()).
+ toBe('Nobody is viewing.');
+
+ using('.doc-example-live').input('personCount').enter('2');
+ expect(element('.doc-example-live ng-pluralize:first').text()).
+ toBe('2 people are viewing.');
+ expect(element('.doc-example-live ng-pluralize:last').text()).
+ toBe('Igor and Misko are viewing.');
+
+ using('.doc-example-live').input('personCount').enter('3');
+ expect(element('.doc-example-live ng-pluralize:first').text()).
+ toBe('3 people are viewing.');
+ expect(element('.doc-example-live ng-pluralize:last').text()).
+ toBe('Igor, Misko and one other person are viewing.');
+
+ using('.doc-example-live').input('personCount').enter('4');
+ expect(element('.doc-example-live ng-pluralize:first').text()).
+ toBe('4 people are viewing.');
+ expect(element('.doc-example-live ng-pluralize:last').text()).
+ toBe('Igor, Misko and 2 other people are viewing.');
+ });
+
+ it('should show data-binded names', function() {
+ using('.doc-example-live').input('personCount').enter('4');
+ expect(element('.doc-example-live ng-pluralize:last').text()).
+ toBe('Igor, Misko and 2 other people are viewing.');
+
+ using('.doc-example-live').input('person1').enter('Di');
+ using('.doc-example-live').input('person2').enter('Vojta');
+ expect(element('.doc-example-live ng-pluralize:last').text()).
+ toBe('Di, Vojta and 2 other people are viewing.');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) {
+ var BRACE = /{}/g;
+ return {
+ restrict: 'EA',
+ link: function(scope, element, attr) {
+ var numberExp = attr.count,
+ whenExp = element.attr(attr.$attr.when), // this is because we have {{}} in attrs
+ offset = attr.offset || 0,
+ whens = scope.$eval(whenExp),
+ whensExpFns = {};
+
+ forEach(whens, function(expression, key) {
+ whensExpFns[key] =
+ $interpolate(expression.replace(BRACE, '{{' + numberExp + '-' + offset + '}}'));
+ });
+
+ scope.$watch(function() {
+ var value = parseFloat(scope.$eval(numberExp));
+
+ if (!isNaN(value)) {
+ //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise,
+ //check it against pluralization rules in $locale service
+ if (!whens[value]) value = $locale.pluralCat(value - offset);
+ return whensExpFns[value](scope, element, true);
+ } else {
+ return '';
+ }
+ }, function(newVal) {
+ element.text(newVal);
+ });
+ }
+ };
+}];
diff --git a/src/directive/ngRepeat.js b/src/directive/ngRepeat.js
new file mode 100644
index 00000000..7cab7c40
--- /dev/null
+++ b/src/directive/ngRepeat.js
@@ -0,0 +1,182 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:repeat
+ *
+ * @description
+ * The `ng:repeat` widget instantiates a template once per item from a collection. Each template
+ * instance gets its own scope, where the given loop variable is set to the current collection item,
+ * and `$index` is set to the item index or key.
+ *
+ * Special properties are exposed on the local scope of each template instance, including:
+ *
+ * * `$index` – `{number}` – iterator offset of the repeated element (0..length-1)
+ * * `$position` – `{string}` – position of the repeated element in the iterator. One of:
+ * * `'first'`,
+ * * `'middle'`
+ * * `'last'`
+ *
+ * Note: Although `ng:repeat` looks like a directive, it is actually an attribute widget.
+ *
+ * @element ANY
+ * @scope
+ * @priority 1000
+ * @param {string} repeat_expression The expression indicating how to enumerate a collection. Two
+ * formats are currently supported:
+ *
+ * * `variable in expression` – where variable is the user defined loop variable and `expression`
+ * is a scope expression giving the collection to enumerate.
+ *
+ * For example: `track in cd.tracks`.
+ *
+ * * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
+ * and `expression` is the scope expression giving the collection to enumerate.
+ *
+ * For example: `(name, age) in {'adam':10, 'amalie':12}`.
+ *
+ * @example
+ * This example initializes the scope to a list of names and
+ * then uses `ng:repeat` to display every person:
+ <doc:example>
+ <doc:source>
+ <div ng:init="friends = [{name:'John', age:25}, {name:'Mary', age:28}]">
+ I have {{friends.length}} friends. They are:
+ <ul>
+ <li ng:repeat="friend in friends">
+ [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
+ </li>
+ </ul>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:repeat', function() {
+ var r = using('.doc-example-live').repeater('ul li');
+ expect(r.count()).toBe(2);
+ expect(r.row(0)).toEqual(["1","John","25"]);
+ expect(r.row(1)).toEqual(["2","Mary","28"]);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngRepeatDirective = ngDirective({
+ transclude: 'element',
+ priority: 1000,
+ terminal: true,
+ compile: function(element, attr, linker) {
+ return function(scope, iterStartElement, attr){
+ var expression = attr.ngRepeat;
+ var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
+ lhs, rhs, valueIdent, keyIdent;
+ if (! match) {
+ throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" +
+ expression + "'.");
+ }
+ lhs = match[1];
+ rhs = match[2];
+ match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/);
+ if (!match) {
+ throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
+ keyValue + "'.");
+ }
+ valueIdent = match[3] || match[1];
+ keyIdent = match[2];
+
+ // Store a list of elements from previous run. This is a hash where key is the item from the
+ // iterator, and the value is an array of objects with following properties.
+ // - scope: bound scope
+ // - element: previous element.
+ // - index: position
+ // We need an array of these objects since the same object can be returned from the iterator.
+ // We expect this to be a rare case.
+ var lastOrder = new HashQueueMap();
+ scope.$watch(function(scope){
+ var index, length,
+ collection = scope.$eval(rhs),
+ collectionLength = size(collection, true),
+ childScope,
+ // Same as lastOrder but it has the current state. It will become the
+ // lastOrder on the next iteration.
+ nextOrder = new HashQueueMap(),
+ key, value, // key/value of iteration
+ array, last, // last object information {scope, element, index}
+ cursor = iterStartElement; // current position of the node
+
+ if (!isArray(collection)) {
+ // if object, extract keys, sort them and use to determine order of iteration over obj props
+ array = [];
+ for(key in collection) {
+ if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
+ array.push(key);
+ }
+ }
+ array.sort();
+ } else {
+ array = collection || [];
+ }
+
+ // we are not using forEach for perf reasons (trying to avoid #call)
+ for (index = 0, length = array.length; index < length; index++) {
+ key = (collection === array) ? index : array[index];
+ value = collection[key];
+ last = lastOrder.shift(value);
+ if (last) {
+ // if we have already seen this object, then we need to reuse the
+ // associated scope/element
+ childScope = last.scope;
+ nextOrder.push(value, last);
+
+ if (index === last.index) {
+ // do nothing
+ cursor = last.element;
+ } else {
+ // existing item which got moved
+ last.index = index;
+ // This may be a noop, if the element is next, but I don't know of a good way to
+ // figure this out, since it would require extra DOM access, so let's just hope that
+ // the browsers realizes that it is noop, and treats it as such.
+ cursor.after(last.element);
+ cursor = last.element;
+ }
+ } else {
+ // new item which we don't know about
+ childScope = scope.$new();
+ }
+
+ childScope[valueIdent] = value;
+ if (keyIdent) childScope[keyIdent] = key;
+ childScope.$index = index;
+ childScope.$position = index === 0 ?
+ 'first' :
+ (index == collectionLength - 1 ? 'last' : 'middle');
+
+ if (!last) {
+ linker(childScope, function(clone){
+ cursor.after(clone);
+ last = {
+ scope: childScope,
+ element: (cursor = clone),
+ index: index
+ };
+ nextOrder.push(value, last);
+ });
+ }
+ }
+
+ //shrink children
+ for (key in lastOrder) {
+ if (lastOrder.hasOwnProperty(key)) {
+ array = lastOrder[key];
+ while(array.length) {
+ value = array.pop();
+ value.element.remove();
+ value.scope.$destroy();
+ }
+ }
+ }
+
+ lastOrder = nextOrder;
+ });
+ };
+ }
+});
diff --git a/src/directive/ngShowHide.js b/src/directive/ngShowHide.js
new file mode 100644
index 00000000..0060ec80
--- /dev/null
+++ b/src/directive/ngShowHide.js
@@ -0,0 +1,80 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:show
+ *
+ * @description
+ * The `ng:show` and `ng:hide` directives show or hide a portion of the DOM tree (HTML)
+ * conditionally.
+ *
+ * @element ANY
+ * @param {expression} expression If the {@link guide/dev_guide.expressions expression} is truthy
+ * then the element is shown or hidden respectively.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ Click me: <input type="checkbox" ng:model="checked"><br/>
+ Show: <span ng:show="checked">I show up when your checkbox is checked.</span> <br/>
+ Hide: <span ng:hide="checked">I hide when your checkbox is checked.</span>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:show / ng:hide', function() {
+ expect(element('.doc-example-live span:first:hidden').count()).toEqual(1);
+ expect(element('.doc-example-live span:last:visible').count()).toEqual(1);
+
+ input('checked').check();
+
+ expect(element('.doc-example-live span:first:visible').count()).toEqual(1);
+ expect(element('.doc-example-live span:last:hidden').count()).toEqual(1);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+//TODO(misko): refactor to remove element from the DOM
+var ngShowDirective = ngDirective(function(scope, element, attr){
+ scope.$watch(attr.ngShow, function(value){
+ element.css('display', toBoolean(value) ? '' : 'none');
+ });
+});
+
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:hide
+ *
+ * @description
+ * The `ng:hide` and `ng:show` directives hide or show a portion
+ * of the HTML conditionally.
+ *
+ * @element ANY
+ * @param {expression} expression If the {@link guide/dev_guide.expressions expression} truthy then
+ * the element is shown or hidden respectively.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ Click me: <input type="checkbox" ng:model="checked"><br/>
+ Show: <span ng:show="checked">I show up when you checkbox is checked?</span> <br/>
+ Hide: <span ng:hide="checked">I hide when you checkbox is checked?</span>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:show / ng:hide', function() {
+ expect(element('.doc-example-live span:first:hidden').count()).toEqual(1);
+ expect(element('.doc-example-live span:last:visible').count()).toEqual(1);
+
+ input('checked').check();
+
+ expect(element('.doc-example-live span:first:visible').count()).toEqual(1);
+ expect(element('.doc-example-live span:last:hidden').count()).toEqual(1);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+//TODO(misko): refactor to remove element from the DOM
+var ngHideDirective = ngDirective(function(scope, element, attr){
+ scope.$watch(attr.ngHide, function(value){
+ element.css('display', toBoolean(value) ? 'none' : '');
+ });
+});
diff --git a/src/directive/ngStyle.js b/src/directive/ngStyle.js
new file mode 100644
index 00000000..cd2c1e3c
--- /dev/null
+++ b/src/directive/ngStyle.js
@@ -0,0 +1,42 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:style
+ *
+ * @description
+ * The ng:style allows you to set CSS style on an HTML element conditionally.
+ *
+ * @element ANY
+ * @param {expression} expression {@link guide/dev_guide.expressions Expression} which evals to an
+ * object whose keys are CSS style names and values are corresponding values for those CSS
+ * keys.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <input type="button" value="set" ng:click="myStyle={color:'red'}">
+ <input type="button" value="clear" ng:click="myStyle={}">
+ <br/>
+ <span ng:style="myStyle">Sample Text</span>
+ <pre>myStyle={{myStyle}}</pre>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:style', function() {
+ expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)');
+ element('.doc-example-live :button[value=set]').click();
+ expect(element('.doc-example-live span').css('color')).toBe('rgb(255, 0, 0)');
+ element('.doc-example-live :button[value=clear]').click();
+ expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngStyleDirective = ngDirective(function(scope, element, attr) {
+ scope.$watch(attr.ngStyle, function(newStyles, oldStyles) {
+ if (oldStyles && (newStyles !== oldStyles)) {
+ forEach(oldStyles, function(val, style) { element.css(style, '');});
+ }
+ if (newStyles) element.css(newStyles);
+ }, true);
+});
diff --git a/src/directive/ngSwitch.js b/src/directive/ngSwitch.js
new file mode 100644
index 00000000..0b2475f3
--- /dev/null
+++ b/src/directive/ngSwitch.js
@@ -0,0 +1,110 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:switch
+ * @restrict EA
+ *
+ * @description
+ * Conditionally change the DOM structure.
+ *
+ * @usageContent
+ * <any ng:switch-when="matchValue1">...</any>
+ * <any ng:switch-when="matchValue2">...</any>
+ * ...
+ * <any ng:switch-default>...</any>
+ *
+ * @scope
+ * @param {*} on expression to match against <tt>ng:switch-when</tt>.
+ * @paramDescription
+ * On child elments add:
+ *
+ * * `ng:switch-when`: the case statement to match against. If match then this
+ * case will be displayed.
+ * * `ng:switch-default`: the default case when no other casses match.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.items = ['settings', 'home', 'other'];
+ $scope.selection = $scope.items[0];
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <select ng:model="selection" ng:options="item for item in items">
+ </select>
+ <tt>selection={{selection}}</tt>
+ <hr/>
+ <ng:switch on="selection" >
+ <div ng:switch-when="settings">Settings Div</div>
+ <span ng:switch-when="home">Home Span</span>
+ <span ng:switch-default>default</span>
+ </ng:switch>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should start in settings', function() {
+ expect(element('.doc-example-live ng\\:switch').text()).toMatch(/Settings Div/);
+ });
+ it('should change to home', function() {
+ select('selection').option('home');
+ expect(element('.doc-example-live ng\\:switch').text()).toMatch(/Home Span/);
+ });
+ it('should select deafault', function() {
+ select('selection').option('other');
+ expect(element('.doc-example-live ng\\:switch').text()).toMatch(/default/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var NG_SWITCH = 'ng-switch';
+var ngSwitchDirective = valueFn({
+ restrict: 'EA',
+ compile: function(element, attr) {
+ var watchExpr = attr.ngSwitch || attr.on,
+ cases = {};
+
+ element.data(NG_SWITCH, cases);
+ return function(scope, element){
+ var selectedTransclude,
+ selectedElement;
+
+ scope.$watch(watchExpr, function(value) {
+ if (selectedElement) {
+ selectedElement.remove();
+ selectedElement = null;
+ }
+ if ((selectedTransclude = cases['!' + value] || cases['?'])) {
+ scope.$eval(attr.change);
+ selectedTransclude(scope.$new(), function(caseElement, scope) {
+ selectedElement = caseElement;
+ element.append(caseElement);
+ element.bind('$destroy', bind(scope, scope.$destroy));
+ });
+ }
+ });
+ };
+ }
+});
+
+var ngSwitchWhenDirective = ngDirective({
+ transclude: 'element',
+ priority: 500,
+ compile: function(element, attrs, transclude) {
+ var cases = element.inheritedData(NG_SWITCH);
+ assertArg(cases);
+ cases['!' + attrs.ngSwitchWhen] = transclude;
+ }
+});
+
+var ngSwitchDefaultDirective = ngDirective({
+ transclude: 'element',
+ priority: 500,
+ compile: function(element, attrs, transclude) {
+ var cases = element.inheritedData(NG_SWITCH);
+ assertArg(cases);
+ cases['?'] = transclude;
+ }
+});
diff --git a/src/directive/ngTransclude.js b/src/directive/ngTransclude.js
new file mode 100644
index 00000000..16587f30
--- /dev/null
+++ b/src/directive/ngTransclude.js
@@ -0,0 +1,58 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:transclude
+ *
+ * @description
+ * Insert the transcluded DOM here.
+ *
+ * @element ANY
+ *
+ * @example
+ <doc:example module="transclude">
+ <doc:source>
+ <script>
+ function Ctrl($scope) {
+ $scope.title = 'Lorem Ipsum';
+ $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
+ }
+
+ angular.module('transclude', [])
+ .directive('pane', function(){
+ return {
+ restrict: 'E',
+ transclude: true,
+ scope: 'isolate',
+ locals: { title:'bind' },
+ template: '<div style="border: 1px solid black;">' +
+ '<div style="background-color: gray">{{title}}</div>' +
+ '<div ng-transclude></div>' +
+ '</div>'
+ };
+ });
+ </script>
+ <div ng:controller="Ctrl">
+ <input ng:model="title"><br>
+ <textarea ng:model="text"></textarea> <br/>
+ <pane title="{{title}}">{{text}}</pane>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should have transcluded', function() {
+ input('title').enter('TITLE');
+ input('text').enter('TEXT');
+ expect(binding('title')).toEqual('TITLE');
+ expect(binding('text')).toEqual('TEXT');
+ });
+ </doc:scenario>
+ </doc:example>
+ *
+ */
+var ngTranscludeDirective = ngDirective({
+ controller: ['$transclude', '$element', function($transclude, $element) {
+ $transclude(function(clone) {
+ $element.append(clone);
+ });
+ }]
+});
diff --git a/src/directive/ngView.js b/src/directive/ngView.js
new file mode 100644
index 00000000..3c589354
--- /dev/null
+++ b/src/directive/ngView.js
@@ -0,0 +1,169 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.ng:view
+ * @restrict ECA
+ *
+ * @description
+ * # Overview
+ * `ng:view` is a directive that complements the {@link angular.module.ng.$route $route} service by
+ * including the rendered template of the current route into the main layout (`index.html`) file.
+ * Every time the current route changes, the included view changes with it according to the
+ * configuration of the `$route` service.
+ *
+ * @scope
+ * @example
+ <doc:example module="ngView">
+ <doc:source>
+ <script type="text/ng-template" id="examples/book.html">
+ controller: {{name}}<br />
+ Book Id: {{params.bookId}}<br />
+ </script>
+
+ <script type="text/ng-template" id="examples/chapter.html">
+ controller: {{name}}<br />
+ Book Id: {{params.bookId}}<br />
+ Chapter Id: {{params.chapterId}}
+ </script>
+
+ <script>
+ angular.module('ngView', [], function($routeProvider, $locationProvider) {
+ $routeProvider.when('/Book/:bookId', {
+ template: 'examples/book.html',
+ controller: BookCntl
+ });
+ $routeProvider.when('/Book/:bookId/ch/:chapterId', {
+ template: 'examples/chapter.html',
+ controller: ChapterCntl
+ });
+
+ // configure html5 to get links working on jsfiddle
+ $locationProvider.html5Mode(true);
+ });
+
+ function MainCntl($scope, $route, $routeParams, $location) {
+ $scope.$route = $route;
+ $scope.$location = $location;
+ $scope.$routeParams = $routeParams;
+ }
+
+ function BookCntl($scope, $routeParams) {
+ $scope.name = "BookCntl";
+ $scope.params = $routeParams;
+ }
+
+ function ChapterCntl($scope, $routeParams) {
+ $scope.name = "ChapterCntl";
+ $scope.params = $routeParams;
+ }
+ </script>
+
+ <div ng:controller="MainCntl">
+ Choose:
+ <a href="/Book/Moby">Moby</a> |
+ <a href="/Book/Moby/ch/1">Moby: Ch1</a> |
+ <a href="/Book/Gatsby">Gatsby</a> |
+ <a href="/Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
+ <a href="/Book/Scarlet">Scarlet Letter</a><br/>
+
+ <ng:view></ng:view>
+ <hr />
+
+ <pre>$location.path() = {{$location.path()}}</pre>
+ <pre>$route.current.template = {{$route.current.template}}</pre>
+ <pre>$route.current.params = {{$route.current.params}}</pre>
+ <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
+ <pre>$routeParams = {{$routeParams}}</pre>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should load and compile correct template', function() {
+ element('a:contains("Moby: Ch1")').click();
+ var content = element('.doc-example-live ng\\:view').text();
+ expect(content).toMatch(/controller\: ChapterCntl/);
+ expect(content).toMatch(/Book Id\: Moby/);
+ expect(content).toMatch(/Chapter Id\: 1/);
+
+ element('a:contains("Scarlet")').click();
+ content = element('.doc-example-live ng\\:view').text();
+ expect(content).toMatch(/controller\: BookCntl/);
+ expect(content).toMatch(/Book Id\: Scarlet/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile',
+ '$controller',
+ function($http, $templateCache, $route, $anchorScroll, $compile,
+ $controller) {
+ return {
+ restrict: 'ECA',
+ terminal: true,
+ link: function(scope, element) {
+ var changeCounter = 0,
+ lastScope;
+
+ scope.$on('$afterRouteChange', function(event, next, previous) {
+ changeCounter++;
+ });
+
+ scope.$watch(function() {return changeCounter;}, function(newChangeCounter) {
+ var template = $route.current && $route.current.template;
+
+ function destroyLastScope() {
+ if (lastScope) {
+ lastScope.$destroy();
+ lastScope = null;
+ }
+ }
+
+ function clearContent() {
+ // ignore callback if another route change occured since
+ if (newChangeCounter == changeCounter) {
+ element.html('');
+ }
+ destroyLastScope();
+ }
+
+ if (template) {
+ $http.get(template, {cache: $templateCache}).success(function(response) {
+ // ignore callback if another route change occured since
+ if (newChangeCounter == changeCounter) {
+ element.html(response);
+ destroyLastScope();
+
+ var link = $compile(element.contents()),
+ current = $route.current;
+
+ lastScope = current.scope = scope.$new();
+ if (current.controller) {
+ $controller(current.controller, {$scope: lastScope});
+ }
+
+ link(lastScope);
+ lastScope.$emit('$contentLoaded');
+
+ // $anchorScroll might listen on event...
+ $anchorScroll();
+ }
+ }).error(clearContent);
+ } else {
+ clearContent();
+ }
+ });
+ }
+ };
+}];
+
+
+var onloadDirective = valueFn({
+ restrict: 'AC',
+ link: function(scope, elm, attr) {
+ var onloadExp = attr.onload || ''; //workaround for jquery bug #7537)
+
+ scope.$on('$contentLoaded', function(event) {
+ scope.$eval(onloadExp);
+ });
+ }
+});
diff --git a/src/directive/script.js b/src/directive/script.js
new file mode 100644
index 00000000..f0affaf9
--- /dev/null
+++ b/src/directive/script.js
@@ -0,0 +1,42 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.script
+ *
+ * @description
+ * Load content of a script tag, with type `text/ng-template`, into `$templateCache`, so that the
+ * template can be used by `ng:include`, `ng:view` or directive templates.
+ *
+ * @restrict E
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script type="text/ng-template" id="/tpl.html">
+ Content of the template.
+ </script>
+
+ <a ng:click="currentTpl='/tpl.html'" id="tpl-link">Load inlined template</a>
+ <div id="tpl-content" ng-include src="currentTpl"></div>
+ </doc:source>
+ <doc:scenario>
+ it('should load template defined inside script tag', function() {
+ element('#tpl-link').click();
+ expect(element('#tpl-content').text()).toMatch(/Content of the template/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+var scriptDirective = ['$templateCache', function($templateCache) {
+ return {
+ restrict: 'E',
+ terminal: true,
+ compile: function(element, attr) {
+ if (attr.type == 'text/ng-template') {
+ var templateUrl = attr.id;
+ $templateCache.put(templateUrl, element.text());
+ }
+ }
+ };
+}];
diff --git a/src/directive/select.js b/src/directive/select.js
new file mode 100644
index 00000000..5ed1367f
--- /dev/null
+++ b/src/directive/select.js
@@ -0,0 +1,444 @@
+'use strict';
+
+/**
+ * @ngdoc directive
+ * @name angular.module.ng.$compileProvider.directive.select
+ *
+ * @description
+ * HTML `SELECT` element with angular data-binding.
+ *
+ * # `ng:options`
+ *
+ * Optionally `ng:options` attribute can be used to dynamically generate a list of `<option>`
+ * elements for a `<select>` element using an array or an object obtained by evaluating the
+ * `ng:options` expression.
+ *˝˝
+ * When an item in the select menu is select, the value of array element or object property
+ * represented by the selected option will be bound to the model identified by the `ng:model` attribute
+ * of the parent select element.
+ *
+ * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
+ * be nested into the `<select>` element. This element will then represent `null` or "not selected"
+ * option. See example below for demonstration.
+ *
+ * Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
+ * of {@link angular.module.ng.$compileProvider.directive.ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
+ * `<option>` element because of the following reasons:
+ *
+ * * value attribute of the option element that we need to bind to requires a string, but the
+ * source of data for the iteration might be in a form of array containing objects instead of
+ * strings
+ * * {@link angular.module.ng.$compileProvider.directive.ng:repeat ng:repeat} unrolls after the select binds causing
+ * incorect rendering on most browsers.
+ * * binding to a value not in list confuses most browsers.
+ *
+ * @param {string} name assignable expression to data-bind to.
+ * @param {string=} required The widget is considered valid only if value is entered.
+ * @param {comprehension_expression=} ng:options in one of the following forms:
+ *
+ * * for array data sources:
+ * * `label` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * for object data sources:
+ * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`group by`** `group`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ *
+ * Where:
+ *
+ * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
+ * * `value`: local variable which will refer to each item in the `array` or each property value
+ * of `object` during iteration.
+ * * `key`: local variable which will refer to a property name in `object` during iteration.
+ * * `label`: The result of this expression will be the label for `<option>` element. The
+ * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
+ * * `select`: The result of this expression will be bound to the model of the parent `<select>`
+ * element. If not specified, `select` expression will default to `value`.
+ * * `group`: The result of this expression will be used to group options using the `<optgroup>`
+ * DOM element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function MyCntrl($scope) {
+ $scope.colors = [
+ {name:'black', shade:'dark'},
+ {name:'white', shade:'light'},
+ {name:'red', shade:'dark'},
+ {name:'blue', shade:'dark'},
+ {name:'yellow', shade:'light'}
+ ];
+ $scope.color = $scope.colors[2]; // red
+ }
+ </script>
+ <div ng:controller="MyCntrl">
+ <ul>
+ <li ng:repeat="color in colors">
+ Name: <input ng:model="color.name">
+ [<a href ng:click="colors.$remove(color)">X</a>]
+ </li>
+ <li>
+ [<a href ng:click="colors.push({})">add</a>]
+ </li>
+ </ul>
+ <hr/>
+ Color (null not allowed):
+ <select ng:model="color" ng:options="c.name for c in colors"></select><br>
+
+ Color (null allowed):
+ <div class="nullable">
+ <select ng:model="color" ng:options="c.name for c in colors">
+ <option value="">-- chose color --</option>
+ </select>
+ </div><br/>
+
+ Color grouped by shade:
+ <select ng:model="color" ng:options="c.name group by c.shade for c in colors">
+ </select><br/>
+
+
+ Select <a href ng:click="color={name:'not in list'}">bogus</a>.<br>
+ <hr/>
+ Currently selected: {{ {selected_color:color} }}
+ <div style="border:solid 1px black; height:20px"
+ ng:style="{'background-color':color.name}">
+ </div>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:options', function() {
+ expect(binding('{selected_color:color}')).toMatch('red');
+ select('color').option('0');
+ expect(binding('{selected_color:color}')).toMatch('black');
+ using('.nullable').select('color').option('');
+ expect(binding('{selected_color:color}')).toMatch('null');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+var ngOptionsDirective = valueFn({ terminal: true });
+var selectDirective = ['$compile', '$parse', function($compile, $parse) {
+ //00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
+ var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
+
+ return {
+ restrict: 'E',
+ require: '?ngModel',
+ link: function(scope, element, attr, ctrl) {
+ if (!ctrl) return;
+
+ var multiple = attr.multiple,
+ optionsExp = attr.ngOptions;
+
+ // required validator
+ if (multiple && (attr.required || attr.ngRequired)) {
+ var requiredValidator = function(value) {
+ ctrl.setValidity('REQUIRED', !attr.required || (value && value.length));
+ return value;
+ };
+
+ ctrl.parsers.push(requiredValidator);
+ ctrl.formatters.unshift(requiredValidator);
+
+ attr.$observe('required', function() {
+ requiredValidator(ctrl.viewValue);
+ });
+ }
+
+ if (optionsExp) Options(scope, element, ctrl);
+ else if (multiple) Multiple(scope, element, ctrl);
+ else Single(scope, element, ctrl);
+
+
+ ////////////////////////////
+
+
+
+ function Single(scope, selectElement, ctrl) {
+ ctrl.render = function() {
+ selectElement.val(ctrl.viewValue);
+ };
+
+ selectElement.bind('change', function() {
+ scope.$apply(function() {
+ ctrl.touch();
+ ctrl.read(selectElement.val());
+ });
+ });
+ }
+
+ function Multiple(scope, selectElement, ctrl) {
+ ctrl.render = function() {
+ var items = new HashMap(ctrl.viewValue);
+ forEach(selectElement.children(), function(option) {
+ option.selected = isDefined(items.get(option.value));
+ });
+ };
+
+ selectElement.bind('change', function() {
+ scope.$apply(function() {
+ var array = [];
+ forEach(selectElement.children(), function(option) {
+ if (option.selected) {
+ array.push(option.value);
+ }
+ });
+ ctrl.touch();
+ ctrl.read(array);
+ });
+ });
+ }
+
+ function Options(scope, selectElement, ctrl) {
+ var match;
+
+ if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
+ throw Error(
+ "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
+ " but got '" + optionsExp + "'.");
+ }
+
+ var displayFn = $parse(match[2] || match[1]),
+ valueName = match[4] || match[6],
+ keyName = match[5],
+ groupByFn = $parse(match[3] || ''),
+ valueFn = $parse(match[2] ? match[1] : valueName),
+ valuesFn = $parse(match[7]),
+ // we can't just jqLite('<option>') since jqLite is not smart enough
+ // to create it in <select> and IE barfs otherwise.
+ optionTemplate = jqLite(document.createElement('option')),
+ optGroupTemplate = jqLite(document.createElement('optgroup')),
+ nullOption = false, // if false then user will not be able to select it
+ // This is an array of array of existing option groups in DOM. We try to reuse these if possible
+ // optionGroupsCache[0] is the options with no option group
+ // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
+ optionGroupsCache = [[{element: selectElement, label:''}]];
+
+ // find existing special options
+ forEach(selectElement.children(), function(option) {
+ if (option.value == '') {
+ // developer declared null option, so user should be able to select it
+ nullOption = jqLite(option).remove();
+ // compile the element since there might be bindings in it
+ $compile(nullOption)(scope);
+ }
+ });
+ selectElement.html(''); // clear contents
+
+ selectElement.bind('change', function() {
+ scope.$apply(function() {
+ var optionGroup,
+ collection = valuesFn(scope) || [],
+ locals = {},
+ key, value, optionElement, index, groupIndex, length, groupLength;
+
+ if (multiple) {
+ value = [];
+ for (groupIndex = 0, groupLength = optionGroupsCache.length;
+ groupIndex < groupLength;
+ groupIndex++) {
+ // list of options for that group. (first item has the parent)
+ optionGroup = optionGroupsCache[groupIndex];
+
+ for(index = 1, length = optionGroup.length; index < length; index++) {
+ if ((optionElement = optionGroup[index].element)[0].selected) {
+ key = optionElement.val();
+ if (keyName) locals[keyName] = key;
+ locals[valueName] = collection[key];
+ value.push(valueFn(scope, locals));
+ }
+ }
+ }
+ } else {
+ key = selectElement.val();
+ if (key == '?') {
+ value = undefined;
+ } else if (key == ''){
+ value = null;
+ } else {
+ locals[valueName] = collection[key];
+ if (keyName) locals[keyName] = key;
+ value = valueFn(scope, locals);
+ }
+ }
+ ctrl.touch();
+
+ if (ctrl.viewValue !== value) {
+ ctrl.read(value);
+ }
+ });
+ });
+
+ ctrl.render = render;
+
+ // TODO(vojta): can't we optimize this ?
+ scope.$watch(render);
+
+ function render() {
+ var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
+ optionGroupNames = [''],
+ optionGroupName,
+ optionGroup,
+ option,
+ existingParent, existingOptions, existingOption,
+ modelValue = ctrl.modelValue,
+ values = valuesFn(scope) || [],
+ keys = keyName ? sortedKeys(values) : values,
+ groupLength, length,
+ groupIndex, index,
+ locals = {},
+ selected,
+ selectedSet = false, // nothing is selected yet
+ lastElement,
+ element;
+
+ if (multiple) {
+ selectedSet = new HashMap(modelValue);
+ } else if (modelValue === null || nullOption) {
+ // if we are not multiselect, and we are null then we have to add the nullOption
+ optionGroups[''].push({selected:modelValue === null, id:'', label:''});
+ selectedSet = true;
+ }
+
+ // We now build up the list of options we need (we merge later)
+ for (index = 0; length = keys.length, index < length; index++) {
+ locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index];
+ optionGroupName = groupByFn(scope, locals) || '';
+ if (!(optionGroup = optionGroups[optionGroupName])) {
+ optionGroup = optionGroups[optionGroupName] = [];
+ optionGroupNames.push(optionGroupName);
+ }
+ if (multiple) {
+ selected = selectedSet.remove(valueFn(scope, locals)) != undefined;
+ } else {
+ selected = modelValue === valueFn(scope, locals);
+ selectedSet = selectedSet || selected; // see if at least one item is selected
+ }
+ optionGroup.push({
+ id: keyName ? keys[index] : index, // either the index into array or key from object
+ label: displayFn(scope, locals) || '', // what will be seen by the user
+ selected: selected // determine if we should be selected
+ });
+ }
+ if (!multiple && !selectedSet) {
+ // nothing was selected, we have to insert the undefined item
+ optionGroups[''].unshift({id:'?', label:'', selected:true});
+ }
+
+ // Now we need to update the list of DOM nodes to match the optionGroups we computed above
+ for (groupIndex = 0, groupLength = optionGroupNames.length;
+ groupIndex < groupLength;
+ groupIndex++) {
+ // current option group name or '' if no group
+ optionGroupName = optionGroupNames[groupIndex];
+
+ // list of options for that group. (first item has the parent)
+ optionGroup = optionGroups[optionGroupName];
+
+ if (optionGroupsCache.length <= groupIndex) {
+ // we need to grow the optionGroups
+ existingParent = {
+ element: optGroupTemplate.clone().attr('label', optionGroupName),
+ label: optionGroup.label
+ };
+ existingOptions = [existingParent];
+ optionGroupsCache.push(existingOptions);
+ selectElement.append(existingParent.element);
+ } else {
+ existingOptions = optionGroupsCache[groupIndex];
+ existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
+
+ // update the OPTGROUP label if not the same.
+ if (existingParent.label != optionGroupName) {
+ existingParent.element.attr('label', existingParent.label = optionGroupName);
+ }
+ }
+
+ lastElement = null; // start at the begining
+ for(index = 0, length = optionGroup.length; index < length; index++) {
+ option = optionGroup[index];
+ if ((existingOption = existingOptions[index+1])) {
+ // reuse elements
+ lastElement = existingOption.element;
+ if (existingOption.label !== option.label) {
+ lastElement.text(existingOption.label = option.label);
+ }
+ if (existingOption.id !== option.id) {
+ lastElement.val(existingOption.id = option.id);
+ }
+ if (existingOption.element.selected !== option.selected) {
+ lastElement.prop('selected', (existingOption.selected = option.selected));
+ }
+ } else {
+ // grow elements
+
+ // if it's a null option
+ if (option.id === '' && nullOption) {
+ // put back the pre-compiled element
+ element = nullOption;
+ } else {
+ // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
+ // in this version of jQuery on some browser the .text() returns a string
+ // rather then the element.
+ (element = optionTemplate.clone())
+ .val(option.id)
+ .attr('selected', option.selected)
+ .text(option.label);
+ }
+
+ existingOptions.push(existingOption = {
+ element: element,
+ label: option.label,
+ id: option.id,
+ selected: option.selected
+ });
+ if (lastElement) {
+ lastElement.after(element);
+ } else {
+ existingParent.element.append(element);
+ }
+ lastElement = element;
+ }
+ }
+ // remove any excessive OPTIONs in a group
+ index++; // increment since the existingOptions[0] is parent element not OPTION
+ while(existingOptions.length > index) {
+ existingOptions.pop().element.remove();
+ }
+ }
+ // remove any excessive OPTGROUPs from select
+ while(optionGroupsCache.length > groupIndex) {
+ optionGroupsCache.pop()[0].element.remove();
+ }
+ };
+ }
+ }
+ }
+}];
+
+var optionDirective = ['$interpolate', function($interpolate) {
+ return {
+ restrict: 'E',
+ priority: 100,
+ compile: function(element, attr) {
+ if (isUndefined(attr.value)) {
+ var interpolateFn = $interpolate(element.text(), true);
+ if (interpolateFn) {
+ return function (scope, element, attr) {
+ scope.$watch(interpolateFn, function(value) {
+ attr.$set('value', value);
+ });
+ }
+ } else {
+ attr.$set('value', element.text());
+ }
+ }
+ }
+ }
+}];
diff --git a/src/directive/style.js b/src/directive/style.js
new file mode 100644
index 00000000..68ea1465
--- /dev/null
+++ b/src/directive/style.js
@@ -0,0 +1,6 @@
+'use strict';
+
+var styleDirective = valueFn({
+ restrict: 'E',
+ terminal: true
+});