aboutsummaryrefslogtreecommitdiffstats
path: root/src/widget
diff options
context:
space:
mode:
Diffstat (limited to 'src/widget')
-rw-r--r--src/widget/form.js235
-rw-r--r--src/widget/input.js1171
-rw-r--r--src/widget/select.js444
3 files changed, 0 insertions, 1850 deletions
diff --git a/src/widget/form.js b/src/widget/form.js
deleted file mode 100644
index e3823f41..00000000
--- a/src/widget/form.js
+++ /dev/null
@@ -1,235 +0,0 @@
-'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/widget/input.js b/src/widget/input.js
deleted file mode 100644
index af446c6b..00000000
--- a/src/widget/input.js
+++ /dev/null
@@ -1,1171 +0,0 @@
-'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/widget/select.js b/src/widget/select.js
deleted file mode 100644
index 5ed1367f..00000000
--- a/src/widget/select.js
+++ /dev/null
@@ -1,444 +0,0 @@
-'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());
- }
- }
- }
- }
-}];