aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--angularFiles.js1
-rw-r--r--docs/content/api/angular.inputType.ngdoc58
-rw-r--r--docs/content/cookbook/advancedform.ngdoc2
-rw-r--r--docs/content/guide/dev_guide.forms.ngdoc82
-rw-r--r--docs/src/templates/index.html2
-rw-r--r--src/Angular.js12
-rw-r--r--src/AngularPublic.js9
-rw-r--r--src/scenario/Scenario.js2
-rw-r--r--src/scenario/dsl.js2
-rw-r--r--src/service/formFactory.js414
-rw-r--r--src/widget/form.js134
-rw-r--r--src/widget/input.js1007
-rw-r--r--src/widget/select.js138
-rw-r--r--test/BinderSpec.js5
-rw-r--r--test/service/formFactorySpec.js206
-rw-r--r--test/widget/formSpec.js207
-rw-r--r--test/widget/inputSpec.js1255
-rw-r--r--test/widget/selectSpec.js806
18 files changed, 2233 insertions, 2109 deletions
diff --git a/angularFiles.js b/angularFiles.js
index af859471..af38755c 100644
--- a/angularFiles.js
+++ b/angularFiles.js
@@ -23,7 +23,6 @@ angularFiles = {
'src/service/filter/filters.js',
'src/service/filter/limitTo.js',
'src/service/filter/orderBy.js',
- 'src/service/formFactory.js',
'src/service/interpolate.js',
'src/service/location.js',
'src/service/log.js',
diff --git a/docs/content/api/angular.inputType.ngdoc b/docs/content/api/angular.inputType.ngdoc
index 9cbf9eb2..a5d1f74a 100644
--- a/docs/content/api/angular.inputType.ngdoc
+++ b/docs/content/api/angular.inputType.ngdoc
@@ -32,61 +32,3 @@ All `inputType` widgets support:
- **`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>
- angular.inputType('json', function(element, scope) {
- scope.$parseView = function() {
- try {
- this.$modelValue = angular.fromJson(this.$viewValue);
- if (this.$error.JSON) {
- this.$emit('$valid', 'JSON');
- }
- } catch (e) {
- this.$emit('$invalid', 'JSON');
- }
- }
-
- scope.$parseModel = function() {
- this.$viewValue = angular.toJson(this.$modelValue);
- }
- });
-
- function Ctrl($scope) {
- $scope.data = {
- framework:'angular',
- codenames:'supper-powers'
- }
- $scope.required = false;
- $scope.disabled = false;
- $scope.readonly = false;
- }
- </script>
- <div ng:controller="Ctrl">
- <form name="myForm">
- <input type="json" ng:model="data" size="80"
- ng:required="{{required}}" ng:disabled="{{disabled}}"
- ng:readonly="{{readonly}}"/><br/>
- Required: <input type="checkbox" ng:model="required"> <br/>
- Disabled: <input type="checkbox" ng:model="disabled"> <br/>
- Readonly: <input type="checkbox" ng:model="readonly"> <br/>
- <pre>data={{data}}</pre>
- <pre>myForm={{myForm}}</pre>
- </form>
- </div>
-</doc:source>
-<doc:scenario>
- it('should invalidate on wrong input', function() {
- expect(element('form[name=myForm]').prop('className')).toMatch('ng-valid');
- input('data').enter('{}');
- expect(binding('data')).toEqual('{}');
- input('data').enter('{');
- expect(element('form[name=myForm]').prop('className')).toMatch('ng-invalid');
- });
-</doc:scenario>
-</doc:example>
diff --git a/docs/content/cookbook/advancedform.ngdoc b/docs/content/cookbook/advancedform.ngdoc
index 58a8dfd5..3e3b2d28 100644
--- a/docs/content/cookbook/advancedform.ngdoc
+++ b/docs/content/cookbook/advancedform.ngdoc
@@ -52,7 +52,7 @@ detection, and preventing invalid form submission.
};
$scope.isSaveDisabled = function() {
- return $scope.myForm.$invalid || angular.equals(master, $scope.form);
+ return $scope.myForm.invalid || angular.equals(master, $scope.form);
};
$scope.cancel();
diff --git a/docs/content/guide/dev_guide.forms.ngdoc b/docs/content/guide/dev_guide.forms.ngdoc
index 5a6fe54e..97d82bb1 100644
--- a/docs/content/guide/dev_guide.forms.ngdoc
+++ b/docs/content/guide/dev_guide.forms.ngdoc
@@ -138,7 +138,7 @@ The following example demonstrates:
};
$scope.isSaveDisabled = function() {
- return $scope.userForm.$invalid || angular.equals($scope.master, $scope.form);
+ return $scope.userForm.invalid || angular.equals($scope.master, $scope.form);
};
$scope.cancel();
@@ -150,7 +150,7 @@ The following example demonstrates:
<label>Name:</label><br/>
<input type="text" name="customer" ng:model="form.customer" required/>
- <span class="error" ng:show="userForm.customer.$error.REQUIRED">
+ <span class="error" ng:show="userForm.customer.error.REQUIRED">
Customer name is required!</span>
<br/><br/>
@@ -165,15 +165,15 @@ The following example demonstrates:
<input type="text" name="zip" ng:pattern="zip" size="5" required
ng:model="form.address.zip"/><br/><br/>
- <span class="error" ng:show="addressForm.$invalid">
+ <span class="error" ng:show="addressForm.invalid">
Incomplete address:
- <span class="error" ng:show="addressForm.state.$error.REQUIRED">
+ <span class="error" ng:show="addressForm.state.error.REQUIRED">
Missing state!</span>
- <span class="error" ng:show="addressForm.state.$error.PATTERN">
+ <span class="error" ng:show="addressForm.state.error.PATTERN">
Invalid state!</span>
- <span class="error" ng:show="addressForm.zip.$error.REQUIRED">
+ <span class="error" ng:show="addressForm.zip.error.REQUIRED">
Missing zip!</span>
- <span class="error" ng:show="addressForm.zip.$error.PATTERN">
+ <span class="error" ng:show="addressForm.zip.error.PATTERN">
Invalid zip!</span>
</span>
</ng:form>
@@ -284,56 +284,38 @@ This example shows how to implement a custom HTML editor widget in Angular.
$scope.htmlContent = '<b>Hello</b> <i>World</i>!';
}
- HTMLEditorWidget.$inject = ['$scope', '$element', '$sanitize'];
- function HTMLEditorWidget(scope, element, $sanitize) {
- scope.$parseModel = function() {
- // need to protect for script injection
- try {
- scope.$viewValue = $sanitize(
- scope.$modelValue || '');
- if (this.$error.HTML) {
- // we were invalid, but now we are OK.
- scope.$emit('$valid', 'HTML');
- }
- } catch (e) {
- // if HTML not parsable invalidate form.
- scope.$emit('$invalid', 'HTML');
- }
- }
+ angular.module('formModule', []).directive('ngHtmlEditor', function ($sanitize) {
+ return {
+ require: 'ngModel',
+ link: function(scope, elm, attr, ctrl) {
+ attr.$set('contentEditable', true);
- scope.$render = function() {
- element.html(this.$viewValue);
- }
+ ctrl.$render = function() {
+ elm.html(ctrl.viewValue);
+ };
- element.bind('keyup', function() {
- scope.$apply(function() {
- scope.$emit('$viewChange', element.html());
- });
- });
- }
+ ctrl.formatters.push(function(value) {
+ try {
+ value = $sanitize(value || '');
+ ctrl.emitValidity('HTML', true);
+ } catch (e) {
+ ctrl.emitValidity('HTML', false);
+ }
+
+ });
- angular.module('formModule', [], function($compileProvider){
- $compileProvider.directive('ngHtmlEditorModel', function ($formFactory) {
- return function(scope, element, attr) {
- var form = $formFactory.forElement(element),
- widget;
- element.attr('contentEditable', true);
- widget = form.$createWidget({
- scope: scope,
- model: attr.ngHtmlEditorModel,
- controller: HTMLEditorWidget,
- controllerArgs: {$element: element}});
- // if the element is destroyed, then we need to
- // notify the form.
- element.bind('$destroy', function() {
- widget.$destroy();
+ elm.bind('keyup', function() {
+ scope.$apply(function() {
+ ctrl.read(elm.html());
+ });
});
- };
- });
+
+ }
+ };
});
</script>
<form name='editorForm' ng:controller="EditorCntl">
- <div ng:html-editor-model="htmlContent"></div>
+ <div ng:html-editor ng:model="htmlContent"></div>
<hr/>
HTML: <br/>
<textarea ng:model="htmlContent" cols="80"></textarea>
diff --git a/docs/src/templates/index.html b/docs/src/templates/index.html
index 93d8cdda..46a77da3 100644
--- a/docs/src/templates/index.html
+++ b/docs/src/templates/index.html
@@ -101,7 +101,7 @@
<div id="sidebar">
<input type="text" ng:model="search" id="search-box" placeholder="search the docs"
- tabindex="1" accesskey="s">
+ tabindex="1" accesskey="s" ng:bind-immediate>
<ul id="content-list" ng:class="sectionId" ng:cloak>
<li ng:repeat="page in pages | filter:search" ng:class="getClass(page)">
diff --git a/src/Angular.js b/src/Angular.js
index fec866f5..1265ad9f 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -91,7 +91,6 @@ var $boolean = 'boolean',
angular = window.angular || (window.angular = {}),
angularModule,
/** @name angular.module.ng */
- angularInputType = extensionMap(angular, 'inputType', lowercase),
nodeName_,
uid = ['0', '0', '0'],
DATE_ISOSTRING_LN = 24;
@@ -272,17 +271,6 @@ identity.$inject = [];
function valueFn(value) {return function() {return value;};}
-function extensionMap(angular, name, transform) {
- var extPoint;
- return angular[name] || (extPoint = angular[name] = function(name, fn, prop){
- name = (transform || identity)(name);
- if (isDefined(fn)) {
- extPoint[name] = extend(fn, prop || {});
- }
- return extPoint[name];
- });
-}
-
/**
* @ngdoc function
* @name angular.isUndefined
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index d1ae4a18..20ca5edb 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -98,7 +98,13 @@ function publishExternalAPI(angular){
ngSwitchDefault: ngSwitchDefaultDirective,
ngOptions: ngOptionsDirective,
ngView: ngViewDirective,
- ngTransclude: ngTranscludeDirective
+ ngTransclude: ngTranscludeDirective,
+ ngModel: ngModelDirective,
+ ngList: ngListDirective,
+ ngChange: ngChangeDirective,
+ ngBindImmediate: ngBindImmediateDirective,
+ required: requiredDirective,
+ ngRequired: requiredDirective
}).
directive(ngEventDirectives).
directive(ngAttributeAliasDirectives);
@@ -110,7 +116,6 @@ function publishExternalAPI(angular){
$provide.service('$exceptionHandler', $ExceptionHandlerProvider);
$provide.service('$filter', $FilterProvider);
$provide.service('$interpolate', $InterpolateProvider);
- $provide.service('$formFactory', $FormFactoryProvider);
$provide.service('$http', $HttpProvider);
$provide.service('$httpBackend', $HttpBackendProvider);
$provide.service('$location', $LocationProvider);
diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js
index 7e33181c..cd3c335f 100644
--- a/src/scenario/Scenario.js
+++ b/src/scenario/Scenario.js
@@ -327,7 +327,7 @@ function browserTrigger(element, type, keys) {
(function(fn){
var parentTrigger = fn.trigger;
fn.trigger = function(type) {
- if (/(click|change|keydown)/.test(type)) {
+ if (/(click|change|keydown|blur)/.test(type)) {
var processDefaults = [];
this.each(function(index, node) {
processDefaults.push(browserTrigger(node, type));
diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js
index fb0037e0..f6cc8086 100644
--- a/src/scenario/dsl.js
+++ b/src/scenario/dsl.js
@@ -203,7 +203,7 @@ angular.scenario.dsl('input', function() {
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) {
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
input.val(value);
- input.trigger('keydown');
+ input.trigger('blur');
done();
});
};
diff --git a/src/service/formFactory.js b/src/service/formFactory.js
deleted file mode 100644
index b051f7b9..00000000
--- a/src/service/formFactory.js
+++ /dev/null
@@ -1,414 +0,0 @@
-'use strict';
-
-/**
- * @ngdoc object
- * @name angular.module.ng.$formFactory
- *
- * @description
- * Use `$formFactory` to create a new instance of a {@link angular.module.ng.$formFactory.Form Form}
- * controller or to find the nearest form instance for a given DOM element.
- *
- * The form instance is a collection of widgets, and is responsible for life cycle and validation
- * of widget.
- *
- * Keep in mind that both form and widget instances are {@link api/angular.module.ng.$rootScope.Scope scopes}.
- *
- * @param {Form=} parentForm The form which should be the parent form of the new form controller.
- * If none specified default to the `rootForm`.
- * @returns {Form} A new {@link angular.module.ng.$formFactory.Form Form} instance.
- *
- * @example
- *
- * This example shows how one could write a widget which would enable data-binding on
- * `contenteditable` feature of HTML.
- *
- <doc:example module="formModule">
- <doc:source>
- <script>
- function EditorCntl($scope) {
- $scope.htmlContent = '<b>Hello</b> <i>World</i>!';
- }
-
- HTMLEditorWidget.$inject = ['$scope', '$element', '$sanitize'];
- function HTMLEditorWidget(scope, element, $sanitize) {
- scope.$parseModel = function() {
- // need to protect for script injection
- try {
- scope.$viewValue = $sanitize(
- scope.$modelValue || '');
- if (this.$error.HTML) {
- // we were invalid, but now we are OK.
- scope.$emit('$valid', 'HTML');
- }
- } catch (e) {
- // if HTML not parsable invalidate form.
- scope.$emit('$invalid', 'HTML');
- }
- }
-
- scope.$render = function() {
- element.html(this.$viewValue);
- }
-
- element.bind('keyup', function() {
- scope.$apply(function() {
- scope.$emit('$viewChange', element.html());
- });
- });
- }
-
- angular.module('formModule', [], function($compileProvider){
- $compileProvider.directive('ngHtmlEditorModel', function ($formFactory) {
- return function(scope, element, attr) {
- var form = $formFactory.forElement(element),
- widget;
- element.attr('contentEditable', true);
- widget = form.$createWidget({
- scope: scope,
- model: attr.ngHtmlEditorModel,
- controller: HTMLEditorWidget,
- controllerArgs: {$element: element}});
- // if the element is destroyed, then we need to
- // notify the form.
- element.bind('$destroy', function() {
- widget.$destroy();
- });
- };
- });
- });
- </script>
- <form name='editorForm' ng:controller="EditorCntl">
- <div ng:html-editor-model="htmlContent"></div>
- <hr/>
- HTML: <br/>
- <textarea ng:model="htmlContent" cols="80"></textarea>
- <hr/>
- <pre>editorForm = {{editorForm|json}}</pre>
- </form>
- </doc:source>
- <doc:scenario>
- it('should enter invalid HTML', function() {
- expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
- input('htmlContent').enter('<');
- expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
- });
- </doc:scenario>
- </doc:example>
- */
-
-/**
- * @ngdoc object
- * @name angular.module.ng.$formFactory.Form
- * @description
- * The `Form` is a controller which keeps track of the validity of the widgets contained within it.
- */
-
-function $FormFactoryProvider() {
- var $parse;
- this.$get = ['$rootScope', '$parse', '$controller',
- function($rootScope, $parse_, $controller) {
- $parse = $parse_;
- /**
- * @ngdoc proprety
- * @name rootForm
- * @propertyOf angular.module.ng.$formFactory
- * @description
- * Static property on `$formFactory`
- *
- * Each application ({@link guide/dev_guide.scopes.internals root scope}) gets a root form which
- * is the top-level parent of all forms.
- */
- formFactory.rootForm = formFactory($rootScope);
-
-
- /**
- * @ngdoc method
- * @name forElement
- * @methodOf angular.module.ng.$formFactory
- * @description
- * Static method on `$formFactory` service.
- *
- * Retrieve the closest form for a given element or defaults to the `root` form. Used by the
- * {@link angular.module.ng.$compileProvider.directive.form form} element.
- * @param {Element} element The element where the search for form should initiate.
- */
- formFactory.forElement = function(element) {
- return element.inheritedData('$form') || formFactory.rootForm;
- };
- return formFactory;
-
- function formFactory(parent) {
- var scope = (parent || formFactory.rootForm).$new();
- $controller(FormController, {$scope: scope});
- return scope;
- }
-
- }];
-
- function propertiesUpdate(widget) {
- widget.$valid = !(widget.$invalid =
- !(widget.$readonly || widget.$disabled || equals(widget.$error, {})));
- }
-
- /**
- * @ngdoc property
- * @name $error
- * @propertyOf angular.module.ng.$formFactory.Form
- * @description
- * Property of the form and widget instance.
- *
- * Summary of all of the errors on the page. If a widget emits `$invalid` with `REQUIRED` key,
- * then the `$error` object will have a `REQUIRED` key with an array of widgets which have
- * emitted this key. `form.$error.REQUIRED == [ widget ]`.
- */
-
- /**
- * @ngdoc property
- * @name $invalid
- * @propertyOf angular.module.ng.$formFactory.Form
- * @description
- * Property of the form and widget instance.
- *
- * True if any of the widgets of the form are invalid.
- */
-
- /**
- * @ngdoc property
- * @name $valid
- * @propertyOf angular.module.ng.$formFactory.Form
- * @description
- * Property of the form and widget instance.
- *
- * True if all of the widgets of the form are valid.
- */
-
- /**
- * @ngdoc event
- * @name angular.module.ng.$formFactory.Form#$valid
- * @eventOf angular.module.ng.$formFactory.Form
- * @eventType listen on form
- * @description
- * Upon receiving the `$valid` event from the widget update the `$error`, `$valid` and `$invalid`
- * properties of both the widget as well as the from.
- *
- * @param {string} validationKey The validation key to be used when updating the `$error` object.
- * The validation key is what will allow the template to bind to a specific validation error
- * such as `<div ng:show="form.$error.KEY">error for key</div>`.
- */
-
- /**
- * @ngdoc event
- * @name angular.module.ng.$formFactory.Form#$invalid
- * @eventOf angular.module.ng.$formFactory.Form
- * @eventType listen on form
- * @description
- * Upon receiving the `$invalid` event from the widget update the `$error`, `$valid` and `$invalid`
- * properties of both the widget as well as the from.
- *
- * @param {string} validationKey The validation key to be used when updating the `$error` object.
- * The validation key is what will allow the template to bind to a specific validation error
- * such as `<div ng:show="form.$error.KEY">error for key</div>`.
- */
-
- /**
- * @ngdoc event
- * @name angular.module.ng.$formFactory.Form#$validate
- * @eventOf angular.module.ng.$formFactory.Form
- * @eventType emit on widget
- * @description
- * Emit the `$validate` event on the widget, giving a widget a chance to emit a
- * `$valid` / `$invalid` event base on its state. The `$validate` event is triggered when the
- * model or the view changes.
- */
-
- /**
- * @ngdoc event
- * @name angular.module.ng.$formFactory.Form#$viewChange
- * @eventOf angular.module.ng.$formFactory.Form
- * @eventType listen on widget
- * @description
- * A widget is responsible for emitting this event whenever the view changes do to user interaction.
- * The event takes a `$viewValue` parameter, which is the new value of the view. This
- * event triggers a call to `$parseView()` as well as `$validate` event on widget.
- *
- * @param {*} viewValue The new value for the view which will be assigned to `widget.$viewValue`.
- */
-
- FormController.$inject = ['$scope', '$injector'];
- function FormController($scope, $injector) {
- this.$injector = $injector;
-
- var form = this.form = $scope,
- $error = form.$error = {};
-
- form.$on('$destroy', function(event){
- var widget = event.targetScope;
- if (widget.$widgetId) {
- delete form[widget.$widgetId];
- }
- forEach($error, removeWidget, widget);
- });
-
- form.$on('$valid', function(event, error){
- var widget = event.targetScope;
- delete widget.$error[error];
- propertiesUpdate(widget);
- removeWidget($error[error], error, widget);
- });
-
- form.$on('$invalid', function(event, error){
- var widget = event.targetScope;
- addWidget(error, widget);
- widget.$error[error] = true;
- propertiesUpdate(widget);
- });
-
- propertiesUpdate(form);
- form.$createWidget = bind(this, this.$createWidget);
-
- function removeWidget(queue, errorKey, widget) {
- if (queue) {
- widget = widget || this; // so that we can be used in forEach;
- for (var i = 0, length = queue.length; i < length; i++) {
- if (queue[i] === widget) {
- queue.splice(i, 1);
- if (!queue.length) {
- delete $error[errorKey];
- }
- }
- }
- propertiesUpdate(form);
- }
- }
-
- function addWidget(errorKey, widget) {
- var queue = $error[errorKey];
- if (queue) {
- for (var i = 0, length = queue.length; i < length; i++) {
- if (queue[i] === widget) {
- return;
- }
- }
- } else {
- $error[errorKey] = queue = [];
- }
- queue.push(widget);
- propertiesUpdate(form);
- }
- }
-
-
- /**
- * @ngdoc method
- * @name $createWidget
- * @methodOf angular.module.ng.$formFactory.Form
- * @description
- *
- * Use form's `$createWidget` instance method to create new widgets. The widgets can be created
- * using an alias which makes the accessible from the form and available for data-binding,
- * useful for displaying validation error messages.
- *
- * The creation of a widget sets up:
- *
- * - `$watch` of `expression` on `model` scope. This code path syncs the model to the view.
- * The `$watch` listener will:
- *
- * - assign the new model value of `expression` to `widget.$modelValue`.
- * - call `widget.$parseModel` method if present. The `$parseModel` is responsible for copying
- * the `widget.$modelValue` to `widget.$viewValue` and optionally converting the data.
- * (For example to convert a number into string)
- * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid`
- * event.
- * - call `widget.$render()` method on widget. The `$render` method is responsible for
- * reading the `widget.$viewValue` and updating the DOM.
- *
- * - Listen on `$viewChange` event from the `widget`. This code path syncs the view to the model.
- * The `$viewChange` listener will:
- *
- * - assign the value to `widget.$viewValue`.
- * - call `widget.$parseView` method if present. The `$parseView` is responsible for copying
- * the `widget.$viewValue` to `widget.$modelValue` and optionally converting the data.
- * (For example to convert a string into number)
- * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid`
- * event.
- * - Assign the `widget.$modelValue` to the `expression` on the `model` scope.
- *
- * - Creates these set of properties on the `widget` which are updated as a response to the
- * `$valid` / `$invalid` events:
- *
- * - `$error` - object - validation errors will be published as keys on this object.
- * Data-binding to this property is useful for displaying the validation errors.
- * - `$valid` - boolean - true if there are no validation errors
- * - `$invalid` - boolean - opposite of `$valid`.
- * @param {Object} params Named parameters:
- *
- * - `scope` - `{Scope}` - The scope to which the model for this widget is attached.
- * - `model` - `{string}` - The name of the model property on model scope.
- * - `controller` - {WidgetController} - The controller constructor function.
- * The controller constructor should create these instance methods.
- * - `$parseView()`: optional method responsible for copying `$viewVale` to `$modelValue`.
- * The method may fire `$valid`/`$invalid` events.
- * - `$parseModel()`: optional method responsible for copying `$modelVale` to `$viewValue`.
- * The method may fire `$valid`/`$invalid` events.
- * - `$render()`: required method which needs to update the DOM of the widget to match the
- * `$viewValue`.
- *
- * - `controllerArgs` - `{Array}` (Optional) - Any extra arguments will be curried to the
- * WidgetController constructor.
- * - `onChange` - `{(string|function())}` (Optional) - Expression to execute when user changes the
- * value.
- * - `alias` - `{string}` (Optional) - The name of the form property under which the widget
- * instance should be published. The name should be unique for each form.
- * @returns {Widget} Instance of a widget scope.
- */
- FormController.prototype.$createWidget = function(params) {
- var form = this.form,
- modelScope = params.scope,
- onChange = params.onChange,
- alias = params.alias,
- scopeGet = $parse(params.model),
- scopeSet = scopeGet.assign,
- widget = form.$new();
-
- this.$injector.instantiate(params.controller, extend({$scope: widget}, params.controllerArgs));
-
- if (!scopeSet) {
- throw Error("Expression '" + params.model + "' is not assignable!");
- }
-
- widget.$error = {};
- // Set the state to something we know will change to get the process going.
- widget.$modelValue = Number.NaN;
- // watch for scope changes and update the view appropriately
- modelScope.$watch(scopeGet, function(value) {
- if (!equals(widget.$modelValue, value)) {
- widget.$modelValue = value;
- widget.$parseModel ? widget.$parseModel() : (widget.$viewValue = value);
- widget.$emit('$validate');
- widget.$render && widget.$render();
- }
- });
-
- widget.$on('$viewChange', function(event, viewValue){
- if (!equals(widget.$viewValue, viewValue)) {
- widget.$viewValue = viewValue;
- widget.$parseView ? widget.$parseView() : (widget.$modelValue = widget.$viewValue);
- scopeSet(modelScope, widget.$modelValue);
- if (onChange) modelScope.$eval(onChange);
- widget.$emit('$validate');
- }
- });
-
- propertiesUpdate(widget);
-
- // assign the widgetModel to the form
- if (alias && !form.hasOwnProperty(alias)) {
- form[alias] = widget;
- widget.$widgetId = alias;
- } else {
- alias = null;
- }
-
- return widget;
- };
-}
diff --git a/src/widget/form.js b/src/widget/form.js
index deaf38d5..23b07107 100644
--- a/src/widget/form.js
+++ b/src/widget/form.js
@@ -1,5 +1,86 @@
'use strict';
+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);
+ }
+}
+
+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
@@ -57,55 +138,54 @@
$scope.text = 'guest';
}
</script>
- <div ng:controller="Ctrl">
- <form name="myForm">
- text: <input type="text" name="input" ng:model="text" required>
- <span class="error" ng:show="myForm.text.$error.REQUIRED">Required!</span>
- </form>
+ <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/>
- </div>
+ <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');
+ 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');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
-var ngFormDirective = ['$formFactory', function($formFactory) {
+var ngFormDirective = [function() {
return {
+ name: 'form',
restrict: 'E',
+ scope: true,
+ inject: {
+ name: 'accessor'
+ },
+ controller: FormController,
compile: function() {
return {
- pre: function(scope, formElement, attr) {
- var name = attr.name,
- parentForm = $formFactory.forElement(formElement),
- form = $formFactory(parentForm);
- formElement.data('$form', form);
- formElement.bind('submit', function(event){
+ pre: function(scope, formElement, attr, controller) {
+ formElement.data('$form', controller);
+ formElement.bind('submit', function(event) {
if (!attr.action) event.preventDefault();
});
- if (name) {
- scope[name] = form;
- }
- watch('valid');
- watch('invalid');
- function watch(name) {
- form.$watch('$' + name, function(value) {
+
+ 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
index 05390b38..6c95327c 100644
--- a/src/widget/input.js
+++ b/src/widget/input.js
@@ -4,7 +4,6 @@
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 INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;
/**
@@ -36,37 +35,36 @@ var INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;
$scope.word = /^\w*$/;
}
</script>
- <div ng:controller="Ctrl">
- <form name="myForm">
- 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>
- </form>
+ <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/>
- </div>
+ <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');
+ 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');
+ 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');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
@@ -100,48 +98,39 @@ var INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;
$scope.text = 'me@example.com';
}
</script>
- <div ng:controller="Ctrl">
- <form name="myForm">
+ <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">
+ <span class="error" ng:show="myForm.input.error.REQUIRED">
Required!</span>
- <span class="error" ng:show="myForm.input.$error.EMAIL">
+ <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>
- <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/>
- </div>
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('text')).toEqual('me@example.com');
- expect(binding('myForm.input.$valid')).toEqual('true');
+ 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');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
it('should be invalid if not email', function() {
input('text').enter('xxx');
- expect(binding('text')).toEqual('xxx');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
-angularInputType('email', function(element, widget) {
- widget.$on('$validate', function(event) {
- var value = widget.$viewValue;
- widget.$emit(!value || value.match(EMAIL_REGEXP) ? "$valid" : "$invalid", "EMAIL");
- });
-});
/**
@@ -173,48 +162,39 @@ angularInputType('email', function(element, widget) {
$scope.text = 'http://google.com';
}
</script>
- <div ng:controller="Ctrl">
- <form name="myForm">
- 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>
- </form>
+ <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/>
- </div>
+ <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');
+ 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');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
it('should be invalid if not url', function() {
input('text').enter('xxx');
- expect(binding('text')).toEqual('xxx');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
-angularInputType('url', function(element, widget) {
- widget.$on('$validate', function(event) {
- var value = widget.$viewValue;
- widget.$emit(!value || value.match(URL_REGEXP) ? "$valid" : "$invalid", "URL");
- });
-});
/**
@@ -241,53 +221,58 @@ angularInputType('url', function(element, widget) {
$scope.names = ['igor', 'misko', 'vojta'];
}
</script>
- <div ng:controller="Ctrl">
- <form name="myForm">
- List: <input type="list" name="input" ng:model="names" required>
- <span class="error" ng:show="myForm.list.$error.REQUIRED">
- Required!</span>
- </form>
+ <form name="myForm" ng:controller="Ctrl">
+ List: <input type="list" name="input" ng:model="names" 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/>
- </div>
+ <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');
+ 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');
+ expect(binding('names')).toEqual('');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
-angularInputType('list', function(element, widget) {
- function parse(viewValue) {
- var list = [];
- forEach(viewValue.split(/\s*,\s*/), function(value){
- if (value) list.push(trim(value));
- });
- return list;
- }
- widget.$parseView = function() {
- isString(widget.$viewValue) && (widget.$modelValue = parse(widget.$viewValue));
- };
- widget.$parseModel = function() {
- var modelValue = widget.$modelValue;
- if (isArray(modelValue)
- && (!isString(widget.$viewValue) || !equals(parse(widget.$viewValue), modelValue))) {
- widget.$viewValue = modelValue.join(', ');
+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;
+ });
}
};
-});
-
+};
/**
* @ngdoc inputType
@@ -320,113 +305,40 @@ angularInputType('list', function(element, widget) {
$scope.value = 12;
}
</script>
- <div ng:controller="Ctrl">
- <form name="myForm">
- 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>
- </form>
+ <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/>
- </div>
+ <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');
+ 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');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
it('should be invalid if over max', function() {
input('value').enter('123');
- expect(binding('value')).toEqual('123');
- expect(binding('myForm.input.$valid')).toEqual('false');
- });
- </doc:scenario>
- </doc:example>
- */
-angularInputType('number', numericRegexpInputType(NUMBER_REGEXP, 'NUMBER'));
-
-
-/**
- * @ngdoc inputType
- * @name angular.inputType.integer
- *
- * @description
- * Text input with integer validation and transformation. Sets the `INTEGER`
- * validation error key if not a valid integer.
- *
- * @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>
- <div ng:controller="Ctrl">
- <form name="myForm">
- Integer: <input type="integer" 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.INTEGER">
- Not valid integer!</span>
- </form>
- <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/>
- </div>
- </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('1.2');
- expect(binding('value')).toEqual('12');
- expect(binding('myForm.input.$valid')).toEqual('false');
- });
-
- it('should be invalid if over max', function() {
- input('value').enter('123');
- expect(binding('value')).toEqual('123');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ expect(binding('value')).toEqual('12');
+ expect(binding('myForm.input.valid')).toEqual('false');
});
</doc:scenario>
</doc:example>
*/
-angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));
/**
@@ -452,15 +364,13 @@ angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));
$scope.value2 = 'YES'
}
</script>
- <div ng:controller="Ctrl">
- <form name="myForm">
- Value1: <input type="checkbox" ng:model="value1"> <br/>
- Value2: <input type="checkbox" ng:model="value2"
- ng:true-value="YES" ng:false-value="NO"> <br/>
- </form>
+ <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/>
- </div>
+ </form>
</doc:source>
<doc:scenario>
it('should change state', function() {
@@ -475,31 +385,7 @@ angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));
</doc:scenario>
</doc:example>
*/
-angularInputType('checkbox', function(inputElement, widget) {
- var trueValue = inputElement.attr('ng:true-value'),
- falseValue = inputElement.attr('ng:false-value');
-
- if (!isString(trueValue)) trueValue = true;
- if (!isString(falseValue)) falseValue = false;
- inputElement.bind('click', function() {
- widget.$apply(function() {
- widget.$emit('$viewChange', inputElement[0].checked);
- });
- });
-
- widget.$render = function() {
- inputElement[0].checked = widget.$viewValue;
- };
-
- widget.$parseModel = function() {
- widget.$viewValue = widget.$modelValue === trueValue;
- };
-
- widget.$parseView = function() {
- widget.$modelValue = widget.$viewValue ? trueValue : falseValue;
- };
-});
/**
@@ -523,14 +409,12 @@ angularInputType('checkbox', function(inputElement, widget) {
$scope.color = 'blue';
}
</script>
- <div ng:controller="Ctrl">
- <form name="myForm">
- <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/>
- </form>
+ <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/>
- </div>
+ </form>
</doc:source>
<doc:scenario>
it('should change state', function() {
@@ -542,63 +426,6 @@ angularInputType('checkbox', function(inputElement, widget) {
</doc:scenario>
</doc:example>
*/
-angularInputType('radio', function(inputElement, widget, attr) {
- //correct the name
- attr.$set('name', widget.$id + '@' + attr.name);
- inputElement.bind('click', function() {
- widget.$apply(function() {
- if (inputElement[0].checked) {
- widget.$emit('$viewChange', attr.value);
- }
- });
- });
-
- widget.$render = function() {
- inputElement[0].checked = isDefined(attr.value) && (attr.value == widget.$viewValue);
- };
-
- if (inputElement[0].checked) {
- widget.$viewValue = attr.value;
- }
-});
-
-
-function numericRegexpInputType(regexp, error) {
- return function(inputElement, widget) {
- var min = 1 * (inputElement.attr('min') || Number.MIN_VALUE),
- max = 1 * (inputElement.attr('max') || Number.MAX_VALUE);
-
- widget.$on('$validate', function(event){
- var value = widget.$viewValue,
- filled = value && trim(value) != '',
- valid = isString(value) && value.match(regexp);
-
- widget.$emit(!filled || valid ? "$valid" : "$invalid", error);
- filled && (value = 1 * value);
- widget.$emit(valid && value < min ? "$invalid" : "$valid", "MIN");
- widget.$emit(valid && value > max ? "$invalid" : "$valid", "MAX");
- });
-
- widget.$parseView = function() {
- if (widget.$viewValue.match(regexp)) {
- widget.$modelValue = 1 * widget.$viewValue;
- } else if (widget.$viewValue == '') {
- widget.$modelValue = null;
- }
- };
-
- widget.$parseModel = function() {
- widget.$viewValue = isNumber(widget.$modelValue)
- ? '' + widget.$modelValue
- : '';
- };
- };
-}
-
-
-var HTML5_INPUTS_TYPES = makeMap(
- "search,tel,url,email,datetime,date,month,week,time,datetime-local,number,range,color," +
- "radio,checkbox,text,button,submit,reset,hidden,password");
/**
@@ -641,188 +468,67 @@ var HTML5_INPUTS_TYPES = makeMap(
<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">
+ <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">
+ <span class="error" ng:show="myForm.lastName.error.MINLENGTH">
Too short!</span>
- <span class="error" ng:show="myForm.lastName.$error.MAXLENGTH">
+ <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>
+ <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');
+ 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":""}');
- expect(binding('myForm.userName.$valid')).toEqual('false');
- expect(binding('myForm.$valid')).toEqual('false');
+ 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');
+ 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":"xx","name":"guest"}');
- expect(binding('myForm.lastName.$valid')).toEqual('false');
- expect(binding('myForm.lastName.$error')).toMatch(/MINLENGTH/);
- expect(binding('myForm.$valid')).toEqual('false');
+ 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":"some ridiculously long name","name":"guest"}');
- expect(binding('myForm.lastName.$valid')).toEqual('false');
- expect(binding('myForm.lastName.$error')).toMatch(/MAXLENGTH/);
- expect(binding('myForm.$valid')).toEqual('false');
+ .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 = ['$defer', '$formFactory', function($defer, $formFactory) {
- return {
- restrict: 'E',
- link: function(modelScope, inputElement, attr) {
- if (!attr.ngModel) return;
-
- var form = $formFactory.forElement(inputElement),
- // We have to use .getAttribute, since jQuery tries to be smart and use the
- // type property. Trouble is some browser change unknown to text.
- type = attr.type || 'text',
- TypeController,
- patternMatch, widget,
- pattern = attr.ngPattern,
- modelExp = attr.ngModel,
- minlength = parseInt(attr.ngMinlength, 10),
- maxlength = parseInt(attr.ngMaxlength, 10),
- loadFromScope = type.match(/^\s*\@\s*(.*)/);
-
- if (!pattern) {
- patternMatch = valueFn(true);
- } else {
- if (pattern.match(/^\/(.*)\/$/)) {
- pattern = new RegExp(pattern.substr(1, pattern.length - 2));
- patternMatch = function(value) {
- return pattern.test(value);
- };
- } else {
- patternMatch = function(value) {
- var patternObj = modelScope.$eval(pattern);
- if (!patternObj || !patternObj.test) {
- throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj);
- }
- return patternObj.test(value);
- };
- }
- }
-
- type = lowercase(type);
- TypeController = (loadFromScope
- ? assertArgFn(modelScope.$eval(loadFromScope[1]), loadFromScope[1])
- : angularInputType(type)) || noop;
-
- if (!HTML5_INPUTS_TYPES[type]) {
- try {
- // jquery will not let you so we have to go to bare metal
- inputElement[0].setAttribute('type', 'text');
- } catch(e){
- // also turns out that ie8 will not allow changing of types, but since it is not
- // html5 anyway we can ignore the error.
- }
- }
-
- //TODO(misko): setting $inject is a hack
- !TypeController.$inject && (TypeController.$inject = ['$element', '$scope', '$attr']);
- widget = form.$createWidget({
- scope: modelScope,
- model: modelExp,
- onChange: attr.ngChange,
- alias: attr.name,
- controller: TypeController,
- controllerArgs: {$element: inputElement, $attr: attr}
- });
-
- widget.$pristine = !(widget.$dirty = false);
-
- widget.$on('$validate', function() {
- var $viewValue = trim(widget.$viewValue),
- inValid = attr.required && !$viewValue,
- tooLong = maxlength && $viewValue && $viewValue.length > maxlength,
- tooShort = minlength && $viewValue && $viewValue.length < minlength,
- missMatch = $viewValue && !patternMatch($viewValue);
-
- if (widget.$error.REQUIRED != inValid){
- widget.$emit(inValid ? '$invalid' : '$valid', 'REQUIRED');
- }
- if (widget.$error.PATTERN != missMatch){
- widget.$emit(missMatch ? '$invalid' : '$valid', 'PATTERN');
- }
- if (widget.$error.MINLENGTH != tooShort){
- widget.$emit(tooShort ? '$invalid' : '$valid', 'MINLENGTH');
- }
- if (widget.$error.MAXLENGTH != tooLong){
- widget.$emit(tooLong ? '$invalid' : '$valid', 'MAXLENGTH');
- }
- });
-
- forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) {
- widget.$watch('$' + name, function(value) {
- inputElement[value ? 'addClass' : 'removeClass']('ng-' + name);
- });
- });
-
- inputElement.bind('$destroy', function() {
- widget.$destroy();
- });
-
- if (type != 'checkbox' && type != 'radio') {
- // TODO (misko): checkbox / radio does not really belong here, but until we can do
- // widget registration with CSS, we are hacking it this way.
- widget.$render = function() {
- inputElement.val(widget.$viewValue || '');
- };
-
- inputElement.bind('keydown change input', function(event){
- var key = event.keyCode;
- if (/*command*/ key != 91 &&
- /*modifiers*/ !(15 < key && key < 19) &&
- /*arrow*/ !(37 < key && key < 40)) {
- $defer(function() {
- widget.$dirty = !(widget.$pristine = false);
- var value = trim(inputElement.val());
- if (widget.$viewValue !== value ) {
- widget.$emit('$viewChange', value);
- }
- });
- }
- });
- }
- }
- };
-}];
/**
@@ -850,3 +556,452 @@ var inputDirective = ['$defer', '$formFactory', function($defer, $formFactory) {
* @param {string=} ng:change Angular expression to be executed when input changes due to user
* interaction with the input element.
*/
+var inputType = {
+ 'text': textInputType,
+ 'number': numberInputType,
+ 'url': urlInputType,
+ 'email': emailInputType,
+
+ 'radio': radioInputType,
+ '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(ctrl.viewValue || '');
+ };
+
+ // pattern validator
+ var pattern = attr.ngPattern,
+ patternValidator;
+
+ var emit = function(regexp, value) {
+ if (isEmpty(value) || regexp.test(value)) {
+ ctrl.emitValidity('PATTERN', true);
+ return value;
+ } else {
+ ctrl.emitValidity('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.emitValidity('MINLENGTH', false);
+ return undefined;
+ } else {
+ ctrl.emitValidity('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.emitValidity('MAXLENGTH', false);
+ return undefined;
+ } else {
+ ctrl.emitValidity('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.emitValidity('NUMBER', true);
+ return value === '' ? null : (empty ? value : parseFloat(value));
+ } else {
+ ctrl.emitValidity('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.emitValidity('MIN', false);
+ return undefined;
+ } else {
+ ctrl.emitValidity('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.emitValidity('MAX', false);
+ return undefined;
+ } else {
+ ctrl.emitValidity('MAX', true);
+ return value;
+ }
+ };
+
+ ctrl.parsers.push(maxValidator);
+ ctrl.formatters.push(maxValidator);
+ }
+
+ ctrl.formatters.push(function(value) {
+
+ if (isEmpty(value) || isNumber(value)) {
+ ctrl.emitValidity('NUMBER', true);
+ return value;
+ } else {
+ ctrl.emitValidity('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.emitValidity('URL', true);
+ return value;
+ } else {
+ ctrl.emitValidity('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.emitValidity('EMAIL', true);
+ return value;
+ } else {
+ ctrl.emitValidity('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;
+ });
+}
+
+
+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);
+ }
+ }
+ };
+}];
+
+
+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;
+
+ this.touch = function() {
+ if (this.dirty) return false;
+
+ this.dirty = true;
+ this.pristine = false;
+ try {
+ $scope.$emit('$viewTouch');
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ return true;
+ };
+
+ // don't $emit valid if already valid, the same for $invalid
+ // not sure about this method name, should the argument be reversed ? emitError ?
+ this.emitValidity = function(name, isValid) {
+
+ if (!isValid && this.error[name]) return;
+ if (isValid && !this.error[name]) return;
+
+ if (!isValid) {
+ this.error[name] = true;
+ this.invalid = true;
+ this.valid = false;
+ }
+
+ if (isValid) {
+ delete this.error[name];
+ if (equals(this.error, {})) {
+ this.valid = true;
+ this.invalid = false;
+ }
+ }
+
+ return $scope.$emit(isValid ? '$valid' : '$invalid', name, this);
+ };
+
+ // view -> model
+ 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();
+ }
+ });
+}];
+
+
+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);
+ });
+ }
+ };
+}];
+
+
+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);
+ });
+ }
+});
+
+
+var ngBindImmediateDirective = ['$browser', function($browser) {
+ return {
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ element.bind('keydown change input', function(event) {
+ var key = event.keyCode;
+
+ // command modifiers arrows
+ if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
+
+ $browser.defer(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 requiredDirective = [function() {
+ return {
+ require: '?ngModel',
+ link: function(scope, elm, attr, ctrl) {
+ if (!ctrl) return;
+
+ var validator = function(value) {
+ if (attr.required && isEmpty(value)) {
+ ctrl.emitValidity('REQUIRED', false);
+ return null;
+ } else {
+ ctrl.emitValidity('REQUIRED', true);
+ return value;
+ }
+ };
+
+ ctrl.formatters.push(validator);
+ ctrl.parsers.unshift(validator);
+
+ attr.$observe('required', function() {
+ validator(ctrl.viewValue);
+ });
+ }
+ };
+}];
diff --git a/src/widget/select.js b/src/widget/select.js
index f70575a6..e7386147 100644
--- a/src/widget/select.js
+++ b/src/widget/select.js
@@ -123,87 +123,79 @@
*/
var ngOptionsDirective = valueFn({ terminal: true });
-var selectDirective = ['$formFactory', '$compile', '$parse',
- function($formFactory, $compile, $parse){
+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',
- link: function(modelScope, selectElement, attr) {
- if (!attr.ngModel) return;
- var form = $formFactory.forElement(selectElement),
- multiple = attr.multiple,
- optionsExp = attr.ngOptions,
- modelExp = attr.ngModel,
- widget = form.$createWidget({
- scope: modelScope,
- model: modelExp,
- onChange: attr.ngChange,
- alias: attr.name,
- controller: ['$scope', optionsExp ? Options : (multiple ? Multiple : Single)]});
-
- selectElement.bind('$destroy', function() { widget.$destroy(); });
-
- widget.$pristine = !(widget.$dirty = false);
-
- widget.$on('$validate', function() {
- var valid = !attr.required || !!widget.$modelValue;
- if (valid && multiple && attr.required) valid = !!widget.$modelValue.length;
- if (valid !== !widget.$error.REQUIRED) {
- widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED');
- }
- });
+ 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.emitValidity('REQUIRED', !attr.required || (value && value.length));
+ return value;
+ };
- widget.$on('$viewChange', function() {
- widget.$pristine = !(widget.$dirty = true);
- });
+ ctrl.parsers.push(requiredValidator);
+ ctrl.formatters.unshift(requiredValidator);
- forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) {
- widget.$watch('$' + name, function(value) {
- selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ 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 Multiple(widget) {
- widget.$render = function() {
- var items = new HashMap(this.$viewValue);
- forEach(selectElement.children(), function(option){
- option.selected = isDefined(items.get(option.value));
- });
+
+
+ function Single(scope, selectElement, ctrl) {
+ ctrl.render = function() {
+ selectElement.val(ctrl.viewValue);
};
selectElement.bind('change', function() {
- widget.$apply(function() {
- var array = [];
- forEach(selectElement.children(), function(option){
- if (option.selected) {
- array.push(option.value);
- }
- });
- widget.$emit('$viewChange', array);
+ scope.$apply(function() {
+ ctrl.touch();
+ ctrl.read(selectElement.val());
});
});
-
}
- function Single(widget) {
- widget.$render = function() {
- selectElement.val(widget.$viewValue);
+ 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() {
- widget.$apply(function() {
- widget.$emit('$viewChange', selectElement.val());
+ scope.$apply(function() {
+ var array = [];
+ forEach(selectElement.children(), function(option) {
+ if (option.selected) {
+ array.push(option.value);
+ }
+ });
+ ctrl.touch();
+ ctrl.read(array);
});
});
-
- widget.$viewValue = selectElement.val();
}
- function Options(widget) {
+ function Options(scope, selectElement, ctrl) {
var match;
if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
@@ -234,15 +226,15 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
// 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)(modelScope);
+ $compile(nullOption)(scope);
}
});
selectElement.html(''); // clear contents
selectElement.bind('change', function() {
- widget.$apply(function() {
+ scope.$apply(function() {
var optionGroup,
- collection = valuesFn(modelScope) || [],
+ collection = valuesFn(scope) || [],
locals = {},
key, value, optionElement, index, groupIndex, length, groupLength;
@@ -259,7 +251,7 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
key = optionElement.val();
if (keyName) locals[keyName] = key;
locals[valueName] = collection[key];
- value.push(valueFn(modelScope, locals));
+ value.push(valueFn(scope, locals));
}
}
}
@@ -272,17 +264,21 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
} else {
locals[valueName] = collection[key];
if (keyName) locals[keyName] = key;
- value = valueFn(modelScope, locals);
+ value = valueFn(scope, locals);
}
}
- if (isDefined(value) && modelScope.$viewVal !== value) {
- widget.$emit('$viewChange', value);
+ ctrl.touch();
+
+ if (ctrl.viewValue !== value) {
+ ctrl.read(value);
}
});
});
- widget.$watch(render);
- widget.$render = render;
+ 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
@@ -291,8 +287,8 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
optionGroup,
option,
existingParent, existingOptions, existingOption,
- modelValue = widget.$modelValue,
- values = valuesFn(modelScope) || [],
+ modelValue = ctrl.modelValue,
+ values = valuesFn(scope) || [],
keys = keyName ? sortedKeys(values) : values,
groupLength, length,
groupIndex, index,
@@ -313,20 +309,20 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
// 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(modelScope, locals) || '';
+ optionGroupName = groupByFn(scope, locals) || '';
if (!(optionGroup = optionGroups[optionGroupName])) {
optionGroup = optionGroups[optionGroupName] = [];
optionGroupNames.push(optionGroupName);
}
if (multiple) {
- selected = selectedSet.remove(valueFn(modelScope, locals)) != undefined;
+ selected = selectedSet.remove(valueFn(scope, locals)) != undefined;
} else {
- selected = modelValue === valueFn(modelScope, locals);
+ 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(modelScope, locals) || '', // what will be seen by the user
+ label: displayFn(scope, locals) || '', // what will be seen by the user
selected: selected // determine if we should be selected
});
}
diff --git a/test/BinderSpec.js b/test/BinderSpec.js
index 5e27fd0f..eb916d00 100644
--- a/test/BinderSpec.js
+++ b/test/BinderSpec.js
@@ -156,7 +156,7 @@ describe('Binder', function() {
expect(html.indexOf('action="foo();"')).toBeGreaterThan(0);
});
- it('RepeaterAdd', inject(function($rootScope, $compile, $browser) {
+ it('RepeaterAdd', inject(function($rootScope, $compile) {
element = $compile('<div><input type="text" ng:model="item.x" ng:repeat="item in items"></div>')($rootScope);
$rootScope.items = [{x:'a'}, {x:'b'}];
$rootScope.$apply();
@@ -166,8 +166,7 @@ describe('Binder', function() {
expect(second.val()).toEqual('b');
first.val('ABC');
- browserTrigger(first, 'keydown');
- $browser.defer.flush();
+ browserTrigger(first, 'blur');
expect($rootScope.items[0].x).toEqual('ABC');
}));
diff --git a/test/service/formFactorySpec.js b/test/service/formFactorySpec.js
deleted file mode 100644
index 1a23aa49..00000000
--- a/test/service/formFactorySpec.js
+++ /dev/null
@@ -1,206 +0,0 @@
-'use strict';
-
-describe('$formFactory', function() {
-
- it('should have global form', inject(function($rootScope, $formFactory) {
- expect($formFactory.rootForm).toBeTruthy();
- expect($formFactory.rootForm.$createWidget).toBeTruthy();
- }));
-
-
- describe('new form', function() {
- var form;
- var scope;
- var log;
-
- function WidgetCtrl($formFactory, $scope) {
- log += '<init>';
- $scope.$render = function() {
- log += '$render();';
- };
- $scope.$on('$validate', function(e){
- log += '$validate();';
- });
-
- this.$formFactory = $formFactory;
- }
-
- WidgetCtrl.$inject = ['$formFactory', '$scope'];
-
- WidgetCtrl.prototype = {
- getFormFactory: function() {
- return this.$formFactory;
- }
- };
-
- beforeEach(inject(function($rootScope, $formFactory) {
- log = '';
- scope = $rootScope.$new();
- form = $formFactory(scope);
- }));
-
- describe('$createWidget', function() {
- var widget;
-
- beforeEach(function() {
- widget = form.$createWidget({
- scope:scope,
- model:'text',
- alias:'text',
- controller:WidgetCtrl
- });
- });
-
-
- describe('data flow', function() {
- it('should have status properties', inject(function($rootScope, $formFactory) {
- expect(widget.$error).toEqual({});
- expect(widget.$valid).toBe(true);
- expect(widget.$invalid).toBe(false);
- }));
-
-
- it('should update view when model changes', inject(function($rootScope, $formFactory) {
- scope.text = 'abc';
- scope.$digest();
- expect(log).toEqual('<init>$validate();$render();');
- expect(widget.$modelValue).toEqual('abc');
-
- scope.text = 'xyz';
- scope.$digest();
- expect(widget.$modelValue).toEqual('xyz');
-
- }));
- });
-
-
- describe('validation', function() {
- it('should update state on error', inject(function($rootScope, $formFactory) {
- widget.$emit('$invalid', 'E');
- expect(widget.$valid).toEqual(false);
- expect(widget.$invalid).toEqual(true);
-
- widget.$emit('$valid', 'E');
- expect(widget.$valid).toEqual(true);
- expect(widget.$invalid).toEqual(false);
- }));
-
-
- it('should have called the model setter before the validation', inject(function($rootScope, $formFactory) {
- var modelValue;
- widget.$on('$validate', function() {
- modelValue = scope.text;
- });
- widget.$emit('$viewChange', 'abc');
- expect(modelValue).toEqual('abc');
- }));
-
-
- describe('form', function() {
- it('should invalidate form when widget is invalid', inject(function($rootScope, $formFactory) {
- expect(form.$error).toEqual({});
- expect(form.$valid).toEqual(true);
- expect(form.$invalid).toEqual(false);
-
- widget.$emit('$invalid', 'REASON');
-
- expect(form.$error.REASON).toEqual([widget]);
- expect(form.$valid).toEqual(false);
- expect(form.$invalid).toEqual(true);
-
- var widget2 = form.$createWidget({
- scope:scope, model:'text',
- alias:'text',
- controller:WidgetCtrl
- });
- widget2.$emit('$invalid', 'REASON');
-
- expect(form.$error.REASON).toEqual([widget, widget2]);
- expect(form.$valid).toEqual(false);
- expect(form.$invalid).toEqual(true);
-
- widget.$emit('$valid', 'REASON');
-
- expect(form.$error.REASON).toEqual([widget2]);
- expect(form.$valid).toEqual(false);
- expect(form.$invalid).toEqual(true);
-
- widget2.$emit('$valid', 'REASON');
-
- expect(form.$error).toEqual({});
- expect(form.$valid).toEqual(true);
- expect(form.$invalid).toEqual(false);
- }));
- });
-
- });
-
- describe('id assignment', function() {
- it('should default to name expression', inject(function($rootScope, $formFactory) {
- expect(form.text).toEqual(widget);
- }));
-
-
- it('should use ng:id', inject(function($rootScope, $formFactory) {
- widget = form.$createWidget({
- scope:scope,
- model:'text',
- alias:'my.id',
- controller:WidgetCtrl
- });
- expect(form['my.id']).toEqual(widget);
- }));
-
-
- it('should not override existing names', inject(function($rootScope, $formFactory) {
- var widget2 = form.$createWidget({
- scope:scope,
- model:'text',
- alias:'text',
- controller:WidgetCtrl
- });
- expect(form.text).toEqual(widget);
- expect(widget2).not.toEqual(widget);
- }));
- });
-
- describe('dealocation', function() {
- it('should dealocate', inject(function($rootScope, $formFactory) {
- var widget2 = form.$createWidget({
- scope:scope,
- model:'text',
- alias:'myId',
- controller:WidgetCtrl
- });
- expect(form.myId).toEqual(widget2);
- var widget3 = form.$createWidget({
- scope:scope,
- model:'text',
- alias:'myId',
- controller:WidgetCtrl
- });
- expect(form.myId).toEqual(widget2);
-
- widget3.$destroy();
- expect(form.myId).toEqual(widget2);
-
- widget2.$destroy();
- expect(form.myId).toBeUndefined();
- }));
-
-
- it('should remove invalid fields from errors, when child widget removed', inject(function($rootScope, $formFactory) {
- widget.$emit('$invalid', 'MyError');
-
- expect(form.$error.MyError).toEqual([widget]);
- expect(form.$invalid).toEqual(true);
-
- widget.$destroy();
-
- expect(form.$error.MyError).toBeUndefined();
- expect(form.$invalid).toEqual(false);
- }));
- });
- });
- });
-});
diff --git a/test/widget/formSpec.js b/test/widget/formSpec.js
index f2e90d9e..bc5f3ea7 100644
--- a/test/widget/formSpec.js
+++ b/test/widget/formSpec.js
@@ -1,122 +1,205 @@
'use strict';
describe('form', function() {
- var doc;
+ var doc, widget, scope, $compile;
+
+ beforeEach(module(function($compileProvider) {
+ $compileProvider.directive('storeModelCtrl', function() {
+ return {
+ require: 'ngModel',
+ link: function(scope, elm, attr, ctrl) {
+ widget = ctrl;
+ }
+ };
+ });
+ }));
+
+ beforeEach(inject(function($injector) {
+ $compile = $injector.get('$compile');
+ scope = $injector.get('$rootScope');
+ }));
afterEach(function() {
dealoc(doc);
});
- it('should attach form to DOM', inject(function($rootScope, $compile) {
- doc = angular.element('<form>');
- $compile(doc)($rootScope);
+ it('should instantiate form and attach it to DOM', function() {
+ doc = $compile('<form>')(scope);
expect(doc.data('$form')).toBeTruthy();
- }));
+ expect(doc.data('$form') instanceof FormController).toBe(true);
+ });
+
+ it('should remove the widget when element removed', function() {
+ doc = $compile(
+ '<form name="form">' +
+ '<input type="text" name="alias" ng:model="value" store-model-ctrl/>' +
+ '</form>')(scope);
- it('should prevent form submission', inject(function($rootScope, $compile) {
+ var form = scope.form;
+ widget.emitValidity('REQUIRED', false);
+ expect(form.alias).toBe(widget);
+ expect(form.error.REQUIRED).toEqual([widget]);
+
+ doc.find('input').remove();
+ expect(form.error.REQUIRED).toBeUndefined();
+ expect(form.alias).toBeUndefined();
+ });
+
+
+ it('should prevent form submission', function() {
var startingUrl = '' + window.location;
- doc = angular.element('<form name="myForm"><input type=submit val=submit>');
- $compile(doc)($rootScope);
+ doc = jqLite('<form name="myForm"><input type="submit" value="submit" />');
+ $compile(doc)(scope);
+
browserTrigger(doc.find('input'));
waitsFor(
function() { return true; },
'let browser breath, so that the form submision can manifest itself', 10);
+
runs(function() {
expect('' + window.location).toEqual(startingUrl);
});
- }));
+ });
- it('should not prevent form submission if action attribute present',
- inject(function($compile, $rootScope) {
+ it('should not prevent form submission if action attribute present', function() {
var callback = jasmine.createSpy('submit').andCallFake(function(event) {
expect(event.isDefaultPrevented()).toBe(false);
event.preventDefault();
});
- doc = angular.element('<form name="x" action="some.py" />');
- $compile(doc)($rootScope);
+ doc = $compile('<form name="x" action="some.py" />')(scope);
doc.bind('submit', callback);
browserTrigger(doc, 'submit');
expect(callback).toHaveBeenCalledOnce();
- }));
+ });
- it('should publish form to scope', inject(function($rootScope, $compile) {
- doc = angular.element('<form name="myForm"></form>');
- $compile(doc)($rootScope);
- expect($rootScope.myForm).toBeTruthy();
+ it('should publish form to scope', function() {
+ doc = $compile('<form name="myForm"></form>')(scope);
+ expect(scope.myForm).toBeTruthy();
expect(doc.data('$form')).toBeTruthy();
- expect(doc.data('$form')).toEqual($rootScope.myForm);
- }));
-
+ expect(doc.data('$form')).toEqual(scope.myForm);
+ });
- it('should have ng-valide/ng-invalid style', inject(function($rootScope, $compile) {
- doc = angular.element('<form name="myForm"><input type=text ng:model=text required>');
- $compile(doc)($rootScope);
- $rootScope.text = 'misko';
- $rootScope.$digest();
- expect(doc.hasClass('ng-valid')).toBe(true);
- expect(doc.hasClass('ng-invalid')).toBe(false);
+ it('should allow name to be an expression', function() {
+ doc = $compile('<form name="obj.myForm"></form>')(scope);
- $rootScope.text = '';
- $rootScope.$digest();
- expect(doc.hasClass('ng-valid')).toBe(false);
- expect(doc.hasClass('ng-invalid')).toBe(true);
- }));
+ expect(scope.obj).toBeDefined();
+ expect(scope.obj.myForm).toBeTruthy();
+ });
- it('should chain nested forms', inject(function($rootScope, $compile) {
- doc = angular.element(
- '<ng:form name=parent>' +
- '<ng:form name=child>' +
- '<input type=text ng:model=text name=text>' +
+ it('should chain nested forms', function() {
+ doc = jqLite(
+ '<ng:form name="parent">' +
+ '<ng:form name="child">' +
+ '<input type="text" ng:model="text" name="text">' +
'</ng:form>' +
'</ng:form>');
- $compile(doc)($rootScope);
- var parent = $rootScope.parent;
- var child = $rootScope.child;
+ $compile(doc)(scope);
+
+ var parent = scope.parent;
+ var child = scope.child;
var input = child.text;
- input.$emit('$invalid', 'MyError');
- expect(parent.$error.MyError).toEqual([input]);
- expect(child.$error.MyError).toEqual([input]);
+ input.emitValidity('MyError', false);
+ expect(parent.error.MyError).toEqual([input]);
+ expect(child.error.MyError).toEqual([input]);
- input.$emit('$valid', 'MyError');
- expect(parent.$error.MyError).toBeUndefined();
- expect(child.$error.MyError).toBeUndefined();
- }));
+ input.emitValidity('MyError', true);
+ expect(parent.error.MyError).toBeUndefined();
+ expect(child.error.MyError).toBeUndefined();
+ });
- it('should chain nested forms in repeater', inject(function($rootScope, $compile) {
- doc = angular.element(
+ it('should chain nested forms in repeater', function() {
+ doc = jqLite(
'<ng:form name=parent>' +
'<ng:form ng:repeat="f in forms" name=child>' +
'<input type=text ng:model=text name=text>' +
'</ng:form>' +
'</ng:form>');
- $compile(doc)($rootScope);
- $rootScope.forms = [1];
- $rootScope.$digest();
+ $compile(doc)(scope);
- var parent = $rootScope.parent;
+ scope.$apply(function() {
+ scope.forms = [1];
+ });
+
+ var parent = scope.parent;
var child = doc.find('input').scope().child;
var input = child.text;
+
expect(parent).toBeDefined();
expect(child).toBeDefined();
expect(input).toBeDefined();
- input.$emit('$invalid', 'myRule');
- expect(input.$error.myRule).toEqual(true);
- expect(child.$error.myRule).toEqual([input]);
- expect(parent.$error.myRule).toEqual([input]);
+ input.emitValidity('myRule', false);
+ expect(input.error.myRule).toEqual(true);
+ expect(child.error.myRule).toEqual([input]);
+ expect(parent.error.myRule).toEqual([input]);
- input.$emit('$valid', 'myRule');
- expect(parent.$error.myRule).toBeUndefined();
- expect(child.$error.myRule).toBeUndefined();
- }));
+ input.emitValidity('myRule', true);
+ expect(parent.error.myRule).toBeUndefined();
+ expect(child.error.myRule).toBeUndefined();
+ });
+
+
+ it('should publish widgets', function() {
+ doc = jqLite('<form name="form"><input type="text" name="w1" ng:model="some" /></form>');
+ $compile(doc)(scope);
+
+ var widget = scope.form.w1;
+ expect(widget).toBeDefined();
+ expect(widget.pristine).toBe(true);
+ expect(widget.dirty).toBe(false);
+ expect(widget.valid).toBe(true);
+ expect(widget.invalid).toBe(false);
+ });
+
+
+ describe('validation', function() {
+
+ beforeEach(function() {
+ doc = $compile(
+ '<form name="form">' +
+ '<input type="text" ng:model="name" name="name" store-model-ctrl/>' +
+ '</form>')(scope);
+
+ scope.$digest();
+ });
+
+
+ it('should have ng-valid/ng-invalid css class', function() {
+ expect(doc).toBeValid();
+
+ widget.emitValidity('ERROR', false);
+ scope.$apply();
+ expect(doc).toBeInvalid();
+
+ widget.emitValidity('ANOTHER', false);
+ scope.$apply();
+
+ widget.emitValidity('ERROR', true);
+ scope.$apply();
+ expect(doc).toBeInvalid();
+
+ widget.emitValidity('ANOTHER', true);
+ scope.$apply();
+ expect(doc).toBeValid();
+ });
+
+
+ it('should have ng-pristine/ng-dirty css class', function() {
+ expect(doc).toBePristine();
+
+ widget.touch();
+ scope.$apply();
+ expect(doc).toBeDirty();
+ });
+ });
});
diff --git a/test/widget/inputSpec.js b/test/widget/inputSpec.js
index 3b3aa282..daea7246 100644
--- a/test/widget/inputSpec.js
+++ b/test/widget/inputSpec.js
@@ -1,630 +1,953 @@
'use strict';
-describe('widget: input', function() {
- var compile = null, element = null, scope = null, defer = null;
- var $compile = null;
- var doc = null;
+describe('NgModelController', function() {
+ var ctrl, scope, ngModelAccessor;
- beforeEach(inject(function($rootScope, $compile, $browser) {
+ beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope;
- defer = $browser.defer;
- set$compile($compile);
- element = null;
- compile = function(html, parent) {
- if (parent) {
- parent.html(html);
- element = parent.children();
- } else {
- element = jqLite(html);
- }
- $compile(element)(scope);
- scope.$apply();
- return scope;
- };
+ ngModelAccessor = jasmine.createSpy('ngModel accessor');
+ ctrl = $controller(NgModelController, {$scope: scope, ngModel: ngModelAccessor});
+
+ // mock accessor (locals)
+ ngModelAccessor.andCallFake(function(val) {
+ if (isDefined(val)) scope.value = val;
+ return scope.value;
+ });
}));
- function set$compile(c) { $compile = c; }
- afterEach(function() {
- dealoc(element);
- dealoc(doc);
- });
+ it('should init the properties', function() {
+ expect(ctrl.dirty).toBe(false);
+ expect(ctrl.pristine).toBe(true);
+ expect(ctrl.valid).toBe(true);
+ expect(ctrl.invalid).toBe(false);
+ expect(ctrl.viewValue).toBeDefined();
+ expect(ctrl.modelValue).toBeDefined();
- describe('text', function() {
- var form = null,
- formElement = null,
- inputElement = null;
+ expect(ctrl.formatters).toEqual([]);
+ expect(ctrl.parsers).toEqual([]);
+ });
- function createInput(flags){
- var prefix = '';
- forEach(flags, function(value, key){
- prefix += key + '="' + value + '" ';
- });
- formElement = doc = angular.element('<form name="form"><input ' + prefix +
- 'type="text" ng:model="name" name="name" ng:change="change()"></form>');
- inputElement = formElement.find('input');
- $compile(doc)(scope);
- form = formElement.inheritedData('$form');
- };
+ describe('touch', function() {
+ it('should only fire $viewTouch when pristine', function() {
+ var spy = jasmine.createSpy('$viewTouch');
+ scope.$on('$viewTouch', spy);
- it('should bind update scope from model', function() {
- createInput();
- scope.name = 'misko';
- scope.$digest();
- expect(inputElement.val()).toEqual('misko');
+ ctrl.touch();
+ expect(ctrl.pristine).toBe(false);
+ expect(ctrl.dirty).toBe(true);
+ expect(spy).toHaveBeenCalledOnce();
+
+ spy.reset();
+ ctrl.touch();
+ expect(ctrl.pristine).toBe(false);
+ expect(ctrl.dirty).toBe(true);
+ expect(spy).not.toHaveBeenCalled();
});
+ });
- it('should require', function() {
- createInput({required:''});
- scope.$digest();
- expect(scope.form.name.$valid).toBe(false);
- scope.name = 'misko';
- scope.$digest();
- expect(scope.form.name.$valid).toBe(true);
+ describe('emitValidity', function() {
+
+ it('should emit $invalid only when $valid', function() {
+ var spy = jasmine.createSpy('$invalid');
+ scope.$on('$invalid', spy);
+
+ ctrl.emitValidity('ERROR', false);
+ expect(spy).toHaveBeenCalledOnce();
+
+ spy.reset();
+ ctrl.emitValidity('ERROR', false);
+ expect(spy).not.toHaveBeenCalled();
});
- it('should call $destroy on element remove', function() {
- createInput();
- var log = '';
- form.$on('$destroy', function() {
- log += 'destroy;';
- });
- inputElement.remove();
- expect(log).toEqual('destroy;');
+ it('should set and unset the error', function() {
+ ctrl.emitValidity('REQUIRED', false);
+ expect(ctrl.error.REQUIRED).toBe(true);
+
+ ctrl.emitValidity('REQUIRED', true);
+ expect(ctrl.error.REQUIRED).toBeUndefined();
});
- it('should update the model and trim input', function() {
- createInput();
- var log = '';
- scope.change = function() {
- log += 'change();';
- };
- inputElement.val(' a ');
- browserTrigger(inputElement);
- defer.flush();
- expect(scope.name).toEqual('a');
- expect(log).toEqual('change();');
+ it('should set valid/invalid', function() {
+ ctrl.emitValidity('FIRST', false);
+ expect(ctrl.valid).toBe(false);
+ expect(ctrl.invalid).toBe(true);
+
+ ctrl.emitValidity('SECOND', false);
+ expect(ctrl.valid).toBe(false);
+ expect(ctrl.invalid).toBe(true);
+
+ ctrl.emitValidity('SECOND', true);
+ expect(ctrl.valid).toBe(false);
+ expect(ctrl.invalid).toBe(true);
+
+ ctrl.emitValidity('FIRST', true);
+ expect(ctrl.valid).toBe(true);
+ expect(ctrl.invalid).toBe(false);
});
- it('should change non-html5 types to text', inject(function($rootScope, $compile) {
- doc = angular.element('<form name="form"><input type="abc" ng:model="name"></form>');
- $compile(doc)($rootScope);
- expect(doc.find('input').attr('type')).toEqual('text');
- }));
+ it('should emit $valid only when $invalid', function() {
+ var spy = jasmine.createSpy('$valid');
+ scope.$on('$valid', spy);
+ ctrl.emitValidity('ERROR', true);
+ expect(spy).not.toHaveBeenCalled();
- it('should not change html5 types to text', inject(function($rootScope, $compile) {
- doc = angular.element('<form name="form"><input type="number" ng:model="name"></form>');
- $compile(doc)($rootScope);
- expect(doc.find('input')[0].getAttribute('type')).toEqual('number');
- }));
+ ctrl.emitValidity('ERROR', false);
+ ctrl.emitValidity('ERROR', true);
+ expect(spy).toHaveBeenCalledOnce();
+ });
});
- describe("input", function() {
+ describe('view -> model', function() {
- describe("text", function() {
- it('should input-text auto init and listen on keydown/change/input events', function() {
- compile('<input type="text" ng:model="name"/>');
+ it('should set the value to $viewValue', function() {
+ ctrl.read('some-val');
+ expect(ctrl.viewValue).toBe('some-val');
+ });
- scope.name = 'Adam';
- scope.$digest();
- expect(element.val()).toEqual("Adam");
- element.val('Shyam');
- browserTrigger(element, 'keydown');
- // keydown event must be deferred
- expect(scope.name).toEqual('Adam');
- defer.flush();
- expect(scope.name).toEqual('Shyam');
+ it('should pipeline all registered parsers and set result to $modelValue', function() {
+ var log = [];
- element.val('Kai');
- browserTrigger(element, 'change');
- defer.flush();
- expect(scope.name).toEqual('Kai');
+ ctrl.parsers.push(function(value) {
+ log.push(value);
+ return value + '-a';
+ });
- if (!(msie<=8)) {
- element.val('Lunar');
- browserTrigger(element, 'input');
- defer.flush();
- expect(scope.name).toEqual('Lunar');
- }
+ ctrl.parsers.push(function(value) {
+ log.push(value);
+ return value + '-b';
});
+ ctrl.read('init');
+ expect(log).toEqual(['init', 'init-a']);
+ expect(ctrl.modelValue).toBe('init-a-b');
+ });
- it('should not trigger eval if value does not change', function() {
- compile('<input type="text" ng:model="name" ng:change="count = count + 1" ng:init="count=0"/>');
- scope.name = 'Misko';
- scope.$digest();
- expect(scope.name).toEqual("Misko");
- expect(scope.count).toEqual(0);
- browserTrigger(element, 'keydown');
- defer.flush();
- expect(scope.name).toEqual("Misko");
- expect(scope.count).toEqual(0);
+
+ it('should fire $viewChange only if value changed and is valid', function() {
+ var spy = jasmine.createSpy('$viewChange');
+ scope.$on('$viewChange', spy);
+
+ ctrl.read('val');
+ expect(spy).toHaveBeenCalledOnce();
+ spy.reset();
+
+ // invalid
+ ctrl.parsers.push(function() {return undefined;});
+ ctrl.read('val');
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+
+ describe('model -> view', function() {
+
+ it('should set the value to $modelValue', function() {
+ scope.$apply(function() {
+ scope.value = 10;
});
+ expect(ctrl.modelValue).toBe(10);
+ });
- it('should allow complex reference binding', function() {
- compile('<div>'+
- '<input type="text" ng:model="obj[\'abc\'].name"/>'+
- '</div>');
- scope.obj = { abc: { name: 'Misko'} };
- scope.$digest();
- expect(element.find('input').val()).toEqual('Misko');
+ it('should pipeline all registered formatters in reversed order and set result to $viewValue',
+ function() {
+ var log = [];
+
+ ctrl.formatters.unshift(function(value) {
+ log.push(value);
+ return value + 2;
});
+ ctrl.formatters.unshift(function(value) {
+ log.push(value);
+ return value + '';
+ });
- describe("ng:format", function() {
- it("should format text", function() {
- compile('<input type="list" ng:model="list"/>');
+ scope.$apply(function() {
+ scope.value = 3;
+ });
+ expect(log).toEqual([3, 5]);
+ expect(ctrl.viewValue).toBe('5');
+ });
- scope.list = ['x', 'y', 'z'];
- scope.$digest();
- expect(element.val()).toEqual("x, y, z");
- element.val('1, 2, 3');
- browserTrigger(element);
- defer.flush();
- expect(scope.list).toEqual(['1', '2', '3']);
- });
+ it('should $render only if value changed and is valid', function() {
+ spyOn(ctrl, 'render');
+ scope.$apply(function() {
+ scope.value= 3;
+ });
+ expect(ctrl.render).toHaveBeenCalledOnce();
+ ctrl.render.reset();
- it("should render as blank if null", function() {
- compile('<input type="text" ng:model="age" ng:format="number" ng:init="age=null"/>');
- expect(scope.age).toBeNull();
- expect(element[0].value).toEqual('');
- });
+ // invalid
+ ctrl.formatters.push(function() {return undefined;});
+ scope.$apply(function() {
+ scope.value= 5;
+ });
+ expect(ctrl.render).not.toHaveBeenCalled();
+ });
+ });
+});
+describe('ng:model', function() {
- it("should show incorrect text while number does not parse", function() {
- compile('<input type="number" ng:model="age"/>');
- scope.age = 123;
- scope.$digest();
- expect(element.val()).toEqual('123');
- try {
- // to allow non-number values, we have to change type so that
- // the browser which have number validation will not interfere with
- // this test. IE8 won't allow it hence the catch.
- element[0].setAttribute('type', 'text');
- } catch (e){}
- element.val('123X');
- browserTrigger(element, 'change');
- defer.flush();
- expect(element.val()).toEqual('123X');
- expect(scope.age).toEqual(123);
- expect(element).toBeInvalid();
- });
+ it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty)',
+ inject(function($compile, $rootScope) {
+ var element = $compile('<input type="email" ng:model="value" />')($rootScope);
+ $rootScope.$digest();
+ expect(element).toBeValid();
+ expect(element).toBePristine();
- it("should not clobber text if model changes due to itself", function() {
- // When the user types 'a,b' the 'a,' stage parses to ['a'] but if the
- // $parseModel function runs it will change to 'a', in essence preventing
- // the user from ever typying ','.
- compile('<input type="list" ng:model="list"/>');
-
- element.val('a ');
- browserTrigger(element, 'change');
- defer.flush();
- expect(element.val()).toEqual('a ');
- expect(scope.list).toEqual(['a']);
-
- element.val('a ,');
- browserTrigger(element, 'change');
- defer.flush();
- expect(element.val()).toEqual('a ,');
- expect(scope.list).toEqual(['a']);
-
- element.val('a , ');
- browserTrigger(element, 'change');
- defer.flush();
- expect(element.val()).toEqual('a , ');
- expect(scope.list).toEqual(['a']);
-
- element.val('a , b');
- browserTrigger(element, 'change');
- defer.flush();
- expect(element.val()).toEqual('a , b');
- expect(scope.list).toEqual(['a', 'b']);
- });
+ $rootScope.$apply(function() {
+ $rootScope.value = 'invalid-email';
+ });
+ expect(element).toBeInvalid();
+ expect(element).toBePristine();
+ element.val('invalid-again');
+ browserTrigger(element, 'blur');
+ expect(element).toBeInvalid();
+ expect(element).toBeDirty();
- it("should come up blank when no value specified", function() {
- compile('<input type="number" ng:model="age"/>');
- scope.$digest();
- expect(element.val()).toEqual('');
- expect(scope.age).toEqual(null);
- });
- });
+ element.val('vojta@google.com');
+ browserTrigger(element, 'blur');
+ expect(element).toBeValid();
+ expect(element).toBeDirty();
+ dealoc(element);
+ }));
+});
- describe("checkbox", function() {
- it("should format booleans", function() {
- compile('<input type="checkbox" ng:model="name" ng:init="name=false"/>');
- expect(scope.name).toBe(false);
- expect(element[0].checked).toBe(false);
- });
+describe('input', function() {
+ var formElm, inputElm, scope, $compile;
- it('should support type="checkbox" with non-standard capitalization', function() {
- compile('<input type="checkBox" ng:model="checkbox"/>');
+ function compileInput(inputHtml) {
+ formElm = jqLite('<form name="form">' + inputHtml + '</form>');
+ inputElm = formElm.find('input');
+ $compile(formElm)(scope);
+ }
- browserTrigger(element);
- expect(scope.checkbox).toBe(true);
+ function changeInputValueTo(value) {
+ inputElm.val(value);
+ browserTrigger(inputElm, 'blur');
+ }
- browserTrigger(element);
- expect(scope.checkbox).toBe(false);
- });
+ beforeEach(inject(function($injector) {
+ $compile = $injector.get('$compile');
+ scope = $injector.get('$rootScope');
+ }));
+ afterEach(function() {
+ dealoc(formElm);
+ });
- it('should allow custom enumeration', function() {
- compile('<input type="checkbox" ng:model="name" ng:true-value="y" ng:false-value="n">');
- scope.name='y';
- scope.$digest();
- expect(element[0].checked).toBe(true);
+ it('should bind to a model', function() {
+ compileInput('<input type="text" ng:model="name" name="alias" ng:change="change()" />');
- scope.name='n';
- scope.$digest();
- expect(element[0].checked).toBe(false);
+ scope.$apply(function() {
+ scope.name = 'misko';
+ });
- scope.name='abc';
- scope.$digest();
- expect(element[0].checked).toBe(false);
+ expect(inputElm.val()).toBe('misko');
+ });
- browserTrigger(element);
- expect(scope.name).toEqual('y');
- browserTrigger(element);
- expect(scope.name).toEqual('n');
- });
+ it('should call $destroy on element remove', function() {
+ compileInput('<input type="text" ng:model="name" name="alias" ng:change="change()" />');
+ var spy = jasmine.createSpy('on destroy');
+ scope.$on('$destroy', spy);
- it('should fire ng:change when the value changes', function() {
- compile('<input type="checkbox" ng:model="foo" ng:change="changeFn()">');
- scope.changeFn = jasmine.createSpy('changeFn');
- scope.$digest();
- expect(scope.changeFn).not.toHaveBeenCalledOnce();
- browserTrigger(element);
- expect(scope.changeFn).toHaveBeenCalledOnce();
- });
- });
+ inputElm.remove();
+ expect(spy).toHaveBeenCalled();
+ });
+
+
+ it('should update the model on "blur" event', function() {
+ compileInput('<input type="text" ng:model="name" name="alias" ng:change="change()" />');
+
+ changeInputValueTo('adam');
+ expect(scope.name).toEqual('adam');
+ });
+
+
+ it('should update the model and trim the value', function() {
+ compileInput('<input type="text" ng:model="name" name="alias" ng:change="change()" />');
+
+ changeInputValueTo(' a ');
+ expect(scope.name).toEqual('a');
+ });
+
+
+ it('should allow complex reference binding', function() {
+ compileInput('<input type="text" ng:model="obj[\'abc\'].name"/>');
+
+ scope.$apply(function() {
+ scope.obj = { abc: { name: 'Misko'} };
});
+ expect(inputElm.val()).toEqual('Misko');
+ });
- it("should process required", inject(function($formFactory) {
- compile('<input type="text" ng:model="price" name="p" required/>', jqLite(document.body));
- expect(element.hasClass('ng-invalid')).toBeTruthy();
+ it('should ignore input without ng:model attr', function() {
+ compileInput('<input type="text" name="whatever" required />');
- scope.price = 'xxx';
+ browserTrigger(inputElm, 'blur');
+ expect(inputElm.hasClass('ng-valid')).toBe(false);
+ expect(inputElm.hasClass('ng-invalid')).toBe(false);
+ expect(inputElm.hasClass('ng-pristine')).toBe(false);
+ expect(inputElm.hasClass('ng-dirty')).toBe(false);
+ });
+
+
+ it('should report error on assignment error', function() {
+ expect(function() {
+ compileInput('<input type="text" ng:model="throw \'\'">');
scope.$digest();
- expect(element.hasClass('ng-invalid')).toBeFalsy();
+ }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at [''].");
+ });
- element.val('');
- browserTrigger(element);
- defer.flush();
- expect(element.hasClass('ng-invalid')).toBeTruthy();
- }));
+ it("should render as blank if null", function() {
+ compileInput('<input type="text" ng:model="age" />');
- it('should allow bindings on ng:required', function() {
- compile('<input type="text" ng:model="price" ng:required="{{required}}"/>',
- jqLite(document.body));
- scope.price = '';
- scope.required = false;
+ scope.$apply(function() {
+ scope.age = null;
+ });
+
+ expect(scope.age).toBeNull();
+ expect(inputElm.val()).toEqual('');
+ });
+
+
+ describe('pattern', function() {
+
+ it('should validate in-lined pattern', function() {
+ compileInput('<input type="text" ng:model="value" ng:pattern="/^\\d\\d\\d-\\d\\d-\\d\\d\\d\\d$/" />');
scope.$digest();
- expect(element).toBeValid();
- scope.price = 'xxx';
+ changeInputValueTo('x000-00-0000x');
+ expect(inputElm).toBeInvalid();
+
+ changeInputValueTo('000-00-0000');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('000-00-0000x');
+ expect(inputElm).toBeInvalid();
+
+ changeInputValueTo('123-45-6789');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('x');
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should validate pattern from scope', function() {
+ compileInput('<input type="text" ng:model="value" ng:pattern="regexp" />');
+ scope.regexp = /^\d\d\d-\d\d-\d\d\d\d$/;
scope.$digest();
- expect(element).toBeValid();
- scope.price = '';
- scope.required = true;
+ changeInputValueTo('x000-00-0000x');
+ expect(inputElm).toBeInvalid();
+
+ changeInputValueTo('000-00-0000');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('000-00-0000x');
+ expect(inputElm).toBeInvalid();
+
+ changeInputValueTo('123-45-6789');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('x');
+ expect(inputElm).toBeInvalid();
+
+ scope.regexp = /abc?/;
+
+ changeInputValueTo('ab');
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('xx');
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ xit('should throw an error when scope pattern can\'t be found', function() {
+ compileInput('<input type="text" ng:model="foo" ng:pattern="fooRegexp" />');
+
+ expect(function() { changeInputValueTo('xx'); }).
+ toThrow('Expected fooRegexp to be a RegExp but was undefined');
+ });
+ });
+
+
+ describe('minlength', function() {
+
+ it('should invalid shorter than given minlenght', function() {
+ compileInput('<input type="text" ng:model="value" ng:minlength="3" />');
+
+ changeInputValueTo('aa');
+ expect(scope.value).toBeUndefined();
+
+ changeInputValueTo('aaa');
+ expect(scope.value).toBe('aaa');
+ });
+ });
+
+
+ describe('maxlength', function() {
+
+ it('should invalid shorter than given maxlenght', function() {
+ compileInput('<input type="text" ng:model="value" ng:maxlength="5" />');
+
+ changeInputValueTo('aaaaaaaa');
+ expect(scope.value).toBeUndefined();
+
+ changeInputValueTo('aaa');
+ expect(scope.value).toBe('aaa');
+ });
+ });
+
+
+ // INPUT TYPES
+
+ describe('number', function() {
+
+ it('should not update model if view invalid', function() {
+ compileInput('<input type="number" ng:model="age"/>');
+
+ scope.$apply(function() {
+ scope.age = 123;
+ });
+ expect(inputElm.val()).toBe('123');
+
+ try {
+ // to allow non-number values, we have to change type so that
+ // the browser which have number validation will not interfere with
+ // this test. IE8 won't allow it hence the catch.
+ inputElm[0].setAttribute('type', 'text');
+ } catch (e) {}
+
+ changeInputValueTo('123X');
+ expect(inputElm.val()).toBe('123X');
+ expect(scope.age).toBe(123);
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should render as blank if null', function() {
+ compileInput('<input type="number" ng:model="age" />');
+
+ scope.$apply(function() {
+ scope.age = null;
+ });
+
+ expect(scope.age).toBeNull();
+ expect(inputElm.val()).toEqual('');
+ });
+
+
+ it('should come up blank when no value specified', function() {
+ compileInput('<input type="number" ng:model="age" />');
+
scope.$digest();
- expect(element).toBeInvalid();
+ expect(inputElm.val()).toBe('');
+
+ scope.$apply(function() {
+ scope.age = null;
+ });
- element.val('abc');
- browserTrigger(element);
- defer.flush();
- expect(element).toBeValid();
+ expect(scope.age).toBeNull();
+ expect(inputElm.val()).toBe('');
});
- describe('textarea', function() {
- it("should process textarea", function() {
- compile('<textarea ng:model="name"></textarea>');
+ it('should parse empty string to null', function() {
+ compileInput('<input type="number" ng:model="age" />');
- scope.name = 'Adam';
+ scope.$apply(function() {
+ scope.age = 10;
+ });
+
+ changeInputValueTo('');
+ expect(scope.age).toBeNull();
+ expect(inputElm).toBeValid();
+ });
+
+
+ describe('min', function() {
+
+ it('should validate', function() {
+ compileInput('<input type="number" ng:model="value" name="alias" min="10" />');
scope.$digest();
- expect(element.val()).toEqual("Adam");
- element.val('Shyam');
- browserTrigger(element);
- defer.flush();
- expect(scope.name).toEqual('Shyam');
+ changeInputValueTo('1');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeFalsy();
+ expect(scope.form.alias.error.MIN).toBeTruthy();
- element.val('Kai');
- browserTrigger(element);
- defer.flush();
- expect(scope.name).toEqual('Kai');
+ changeInputValueTo('100');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(100);
+ expect(scope.form.alias.error.MIN).toBeFalsy();
});
});
- describe('radio', function() {
- it('should support type="radio"', function() {
- compile('<div>' +
- '<input type="radio" name="r" ng:model="chose" value="A"/>' +
- '<input type="radio" name="r" ng:model="chose" value="B"/>' +
- '<input type="radio" name="r" ng:model="chose" value="C"/>' +
- '</div>');
- var a = element[0].childNodes[0];
- var b = element[0].childNodes[1];
- expect(b.name.split('@')[1]).toEqual('r');
- scope.chose = 'A';
- scope.$digest();
- expect(a.checked).toBe(true);
+ describe('max', function() {
- scope.chose = 'B';
+ it('should validate', function() {
+ compileInput('<input type="number" ng:model="value" name="alias" max="10" />');
scope.$digest();
- expect(a.checked).toBe(false);
- expect(b.checked).toBe(true);
- expect(scope.clicked).not.toBeDefined();
- browserTrigger(a);
- expect(scope.chose).toEqual('A');
+ changeInputValueTo('20');
+ expect(inputElm).toBeInvalid();
+ expect(scope.value).toBeFalsy();
+ expect(scope.form.alias.error.MAX).toBeTruthy();
+
+ changeInputValueTo('0');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(scope.form.alias.error.MAX).toBeFalsy();
});
+ });
- it('should honor model over html checked keyword after', function() {
- compile('<div ng:init="choose=\'C\'">' +
- '<input type="radio" ng:model="choose" value="A""/>' +
- '<input type="radio" ng:model="choose" value="B" checked/>' +
- '<input type="radio" ng:model="choose" value="C"/>' +
- '</div>');
+ describe('required', function() {
- expect(scope.choose).toEqual('C');
- var inputs = element.find('input');
- expect(inputs[1].checked).toBe(false);
- expect(inputs[2].checked).toBe(true);
+ it('should be valid even if value is 0', function() {
+ compileInput('<input type="number" ng:model="value" name="alias" required />');
+
+ changeInputValueTo('0');
+ expect(inputElm).toBeValid();
+ expect(scope.value).toBe(0);
+ expect(scope.form.alias.error.REQUIRED).toBeFalsy();
});
+ it('should be valid even if value 0 is set from model', function() {
+ compileInput('<input type="number" ng:model="value" name="alias" required />');
- it('should honor model over html checked keyword before', function() {
- compile('<div ng:init="choose=\'A\'">' +
- '<input type="radio" ng:model="choose" value="A""/>' +
- '<input type="radio" ng:model="choose" value="B" checked/>' +
- '<input type="radio" ng:model="choose" value="C"/>' +
- '</div>');
+ scope.$apply(function() {
+ scope.value = 0;
+ });
- expect(scope.choose).toEqual('A');
- var inputs = element.find('input');
- expect(inputs[0].checked).toBe(true);
- expect(inputs[1].checked).toBe(false);
+ expect(inputElm).toBeValid();
+ expect(inputElm.val()).toBe('0')
+ expect(scope.form.alias.error.REQUIRED).toBeFalsy();
});
+ });
+ });
+ describe('email', function() {
- it('it should work with value attribute that is data-bound', function(){
- compile(
- '<li>'+
- '<input ng:repeat="item in [\'a\', \'b\']" ' +
- ' type="radio" ng:model="choice" value="{{item}}" name="choice">'+
- '</li>');
+ it('should validate e-mail', function() {
+ compileInput('<input type="email" ng:model="email" name="alias" />');
- var inputs = element.find('input');
- expect(inputs[0].checked).toBe(false);
- expect(inputs[1].checked).toBe(false);
+ var widget = scope.form.alias;
+ changeInputValueTo('vojta@google.com');
- scope.choice = 'b';
- scope.$digest();
- expect(inputs[0].checked).toBe(false);
- expect(inputs[1].checked).toBe(true);
+ expect(scope.email).toBe('vojta@google.com');
+ expect(inputElm).toBeValid();
+ expect(widget.error.EMAIL).toBeUndefined();
+
+ changeInputValueTo('invalid@');
+ expect(scope.email).toBe('vojta@google.com');
+ expect(inputElm).toBeInvalid();
+ expect(widget.error.EMAIL).toBeTruthy();
+ });
+
+
+ describe('EMAIL_REGEXP', function() {
+
+ it('should validate email', function() {
+ expect(EMAIL_REGEXP.test('a@b.com')).toBe(true);
+ expect(EMAIL_REGEXP.test('a@B.c')).toBe(false);
});
+ });
+ });
+
+
+ describe('url', function() {
- it('should data-bind the value attribute on initialization', inject(
- function($rootScope, $compile){
- $rootScope.choice = 'b';
- $rootScope.items = ['a', 'b'];
- element = $compile(
- '<li>'+
- '<input ng:repeat="item in items" ' +
- ' type="radio" ng:model="choice" value="{{item}}" name="choice">'+
- '</li>')($rootScope);
+ it('should validate url', function() {
+ compileInput('<input type="url" ng:model="url" name="alias" />');
+ var widget = scope.form.alias;
- $rootScope.$digest();
- var inputs = element.find('input');
- expect(inputs[0].checked).toBe(false);
- expect(inputs[1].checked).toBe(true);
- }));
+ changeInputValueTo('http://www.something.com');
+ expect(scope.url).toBe('http://www.something.com');
+ expect(inputElm).toBeValid();
+ expect(widget.error.URL).toBeUndefined();
+
+ changeInputValueTo('invalid.com');
+ expect(scope.url).toBe('http://www.something.com');
+ expect(inputElm).toBeInvalid();
+ expect(widget.error.URL).toBeTruthy();
});
- describe('password', function () {
- it('should not change password type to text', function () {
- compile('<input type="password" ng:model="name" >');
- expect(element.attr('type')).toBe('password');
+ describe('URL_REGEXP', function() {
+
+ it('should validate url', function() {
+ expect(URL_REGEXP.test('http://server:123/path')).toBe(true);
+ expect(URL_REGEXP.test('a@B.c')).toBe(false);
});
});
+ });
+
- describe('number', function(){
- it('should clear number on non-number', inject(function($compile, $rootScope){
- $rootScope.value = 123;
- var element = $compile('<input type="number" ng:model="value" >')($rootScope);
- $rootScope.$digest();
- expect(element.val()).toEqual('123');
- $rootScope.value = undefined;
- $rootScope.$digest();
- expect(element.val()).toEqual('');
- dealoc(element);
- }));
+ describe('radio', function() {
+
+ it('should update the model', function() {
+ compileInput(
+ '<input type="radio" ng:model="color" value="white" />' +
+ '<input type="radio" ng:model="color" value="red" />' +
+ '<input type="radio" ng:model="color" value="blue" />');
+
+ scope.$apply(function() {
+ scope.color = 'white';
+ });
+ expect(inputElm[0].checked).toBe(true);
+ expect(inputElm[1].checked).toBe(false);
+ expect(inputElm[2].checked).toBe(false);
+
+ scope.$apply(function() {
+ scope.color = 'red';
+ });
+ expect(inputElm[0].checked).toBe(false);
+ expect(inputElm[1].checked).toBe(true);
+ expect(inputElm[2].checked).toBe(false);
+
+ browserTrigger(inputElm[2]);
+ expect(scope.color).toBe('blue');
});
- it('should ignore text widget which have no name', function() {
- compile('<input type="text"/>');
- expect(element.attr('ng-exception')).toBeFalsy();
- expect(element.hasClass('ng-exception')).toBeFalsy();
+ // TODO(vojta): change interpolate ?
+ xit('should allow {{expr}} as value', function() {
+ scope.some = 11;
+ compileInput(
+ '<input type="radio" ng:model="value" value="{{some}}" />' +
+ '<input type="radio" ng:model="value" value="{{other}}" />');
+
+ browserTrigger(inputElm[0]);
+ expect(scope.value).toBe(true);
+
+ browserTrigger(inputElm[1]);
+ expect(scope.value).toBe(false);
});
+ });
- it('should ignore checkbox widget which have no name', function() {
- compile('<input type="checkbox"/>');
- expect(element.attr('ng-exception')).toBeFalsy();
- expect(element.hasClass('ng-exception')).toBeFalsy();
+ describe('checkbox', function() {
+
+ it('should ignore checkbox without ng:model attr', function() {
+ compileInput('<input type="checkbox" name="whatever" required />');
+
+ browserTrigger(inputElm, 'blur');
+ expect(inputElm.hasClass('ng-valid')).toBe(false);
+ expect(inputElm.hasClass('ng-invalid')).toBe(false);
+ expect(inputElm.hasClass('ng-pristine')).toBe(false);
+ expect(inputElm.hasClass('ng-dirty')).toBe(false);
});
- it('should report error on assignment error', inject(function($log) {
- expect(function() {
- compile('<input type="text" ng:model="throw \'\'">');
- }).toThrow("Syntax Error: Token '''' is an unexpected token at column 7 of the expression [throw ''] starting at [''].");
- $log.error.logs.shift();
- }));
+ it('should format booleans', function() {
+ compileInput('<input type="checkbox" ng:model="name" />');
+
+ scope.$apply(function() {
+ scope.name = false;
+ });
+ expect(inputElm[0].checked).toBe(false);
+
+ scope.$apply(function() {
+ scope.name = true;
+ });
+ expect(inputElm[0].checked).toBe(true);
+ });
+
+
+ it('should support type="checkbox" with non-standard capitalization', function() {
+ compileInput('<input type="checkBox" ng:model="checkbox" />');
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(true);
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.checkbox).toBe(false);
+ });
+
+
+ it('should allow custom enumeration', function() {
+ compileInput('<input type="checkbox" ng:model="name" ng:true-value="y" ' +
+ 'ng:false-value="n">');
+
+ scope.$apply(function() {
+ scope.name = 'y';
+ });
+ expect(inputElm[0].checked).toBe(true);
+
+ scope.$apply(function() {
+ scope.name = 'n';
+ });
+ expect(inputElm[0].checked).toBe(false);
+
+ scope.$apply(function() {
+ scope.name = 'something else';
+ });
+ expect(inputElm[0].checked).toBe(false);
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.name).toEqual('y');
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.name).toEqual('n');
+ });
});
- describe('scope declaration', function() {
- it('should read the declaration from scope', inject(function($rootScope, $compile, $formFactory) {
- var input, formFactory;
- var element = angular.element('<input type="@MyType" ng:model="abc">');
- $rootScope.MyType = function($f, i) {
- input = i;
- formFactory = $f;
- };
- $rootScope.MyType.$inject = ['$formFactory', '$element'];
+ describe('textarea', function() {
- $compile(element)($rootScope);
+ it("should process textarea", function() {
+ compileInput('<textarea ng:model="name"></textarea>');
+ inputElm = formElm.find('textarea');
- expect(formFactory).toBe($formFactory);
- expect(input[0]).toBe(element[0]);
- dealoc(element);
- }));
+ scope.$apply(function() {
+ scope.name = 'Adam';
+ });
+ expect(inputElm.val()).toEqual('Adam');
- it('should throw an error of Controller not declared in scope', inject(function($rootScope, $compile) {
- var input;
- var element = angular.element('<input type="@DontExist" ng:model="abc">');
- var error;
- try {
- $compile(element)($rootScope);
- error = 'no error thrown';
- } catch (e) {
- error = e;
- }
- expect(error.message).toEqual("Argument 'DontExist' is not a function, got undefined");
- }));
+ changeInputValueTo('Shyam');
+ expect(scope.name).toEqual('Shyam');
+
+ changeInputValueTo('Kai');
+ expect(scope.name).toEqual('Kai');
+ });
+
+
+ it('should ignore textarea without ng:model attr', function() {
+ compileInput('<textarea name="whatever" required></textarea>');
+ inputElm = formElm.find('textarea');
+
+ browserTrigger(inputElm, 'blur');
+ expect(inputElm.hasClass('ng-valid')).toBe(false);
+ expect(inputElm.hasClass('ng-invalid')).toBe(false);
+ expect(inputElm.hasClass('ng-pristine')).toBe(false);
+ expect(inputElm.hasClass('ng-dirty')).toBe(false);
+ });
});
- describe('text subtypes', function() {
+ describe('ng:list', function() {
- function itShouldVerify(type, validList, invalidList, params, fn) {
- describe(type, function() {
- forEach(validList, function(value){
- it('should validate "' + value + '"', function() {
- setup(value);
- expect(element).toBeValid();
- });
- });
- forEach(invalidList, function(value){
- it('should NOT validate "' + value + '"', function() {
- setup(value);
- expect(element).toBeInvalid();
- });
- });
+ it('should parse text into an array', function() {
+ compileInput('<input type="text" ng:model="list" ng:list />');
- function setup(value){
- var html = ['<input type="', type.split(' ')[0], '" '];
- forEach(params||{}, function(value, key){
- html.push(key + '="' + value + '" ');
- });
- html.push('ng:model="value">');
- compile(html.join(''));
- (fn||noop)(scope);
- scope.value = null;
- try {
- // to allow non-number values, we have to change type so that
- // the browser which have number validation will not interfere with
- // this test. IE8 won't allow it hence the catch.
- element[0].setAttribute('type', 'text');
- } catch (e){}
- if (value != undefined) {
- element.val(value);
- browserTrigger(element, 'keydown');
- defer.flush();
- }
- scope.$digest();
- }
+ // model -> view
+ scope.$apply(function() {
+ scope.list = ['x', 'y', 'z'];
});
- }
+ expect(inputElm.val()).toBe('x, y, z');
+ // view -> model
+ changeInputValueTo('1, 2, 3');
+ expect(scope.list).toEqual(['1', '2', '3']);
+ });
- itShouldVerify('email', ['a@b.com'], ['a@B.c']);
+ it("should not clobber text if model changes due to itself", function() {
+ // When the user types 'a,b' the 'a,' stage parses to ['a'] but if the
+ // $parseModel function runs it will change to 'a', in essence preventing
+ // the user from ever typying ','.
+ compileInput('<input type="text" ng:model="list" ng:list />');
- itShouldVerify('url', ['http://server:123/path'], ['a@b.c']);
+ changeInputValueTo('a ');
+ expect(inputElm.val()).toEqual('a ');
+ expect(scope.list).toEqual(['a']);
+ changeInputValueTo('a ,');
+ expect(inputElm.val()).toEqual('a ,');
+ expect(scope.list).toEqual(['a']);
- itShouldVerify('number',
- ['', '1', '12.34', '-4', '+13', '.1'],
- ['x', '12b', '-6', '101'],
- {min:-5, max:100});
+ changeInputValueTo('a , ');
+ expect(inputElm.val()).toEqual('a , ');
+ expect(scope.list).toEqual(['a']);
+ changeInputValueTo('a , b');
+ expect(inputElm.val()).toEqual('a , b');
+ expect(scope.list).toEqual(['a', 'b']);
+ });
- itShouldVerify('integer',
- [null, '', '1', '12', '-4', '+13'],
- ['x', '12b', '-6', '101', '1.', '1.2'],
- {min:-5, max:100});
+ xit('should require at least one item', function() {
+ compileInput('<input type="text" ng:model="list" ng:list required />');
- itShouldVerify('integer',
- [null, '', '0', '1'],
- ['-1', '2'],
- {min:0, max:1});
+ changeInputValueTo(' , ');
+ expect(inputElm).toBeInvalid();
+ });
- itShouldVerify('text with inlined pattern constraint',
- ['', '000-00-0000', '123-45-6789'],
- ['x000-00-0000x', 'x000-00-0000', '000-00-0000x', 'x'],
- {'ng:pattern':'/^\\d\\d\\d-\\d\\d-\\d\\d\\d\\d$/'});
+ it('should convert empty string to an empty array', function() {
+ compileInput('<input type="text" ng:model="list" ng:list />');
+ changeInputValueTo('');
+ expect(scope.list).toEqual([]);
+ });
+ });
+
+ describe('required', function() {
+
+ it('should allow bindings on required', function() {
+ compileInput('<input type="text" ng:model="value" required="{{required}}" />');
+
+ scope.$apply(function() {
+ scope.required = false;
+ });
+
+ changeInputValueTo('');
+ expect(inputElm).toBeValid();
+
+
+ scope.$apply(function() {
+ scope.required = true;
+ });
+ expect(inputElm).toBeInvalid();
+
+ scope.$apply(function() {
+ scope.value = 'some';
+ });
+ expect(inputElm).toBeValid();
+
+ changeInputValueTo('');
+ expect(inputElm).toBeInvalid();
+
+ scope.$apply(function() {
+ scope.required = false;
+ });
+ expect(inputElm).toBeValid();
+ });
+
+
+ it('should invalid initial value with bound required', function() {
+ compileInput('<input type="text" ng:model="value" required="{{required}}" />');
+
+ scope.$apply(function() {
+ scope.required = true;
+ });
+
+ expect(inputElm).toBeInvalid();
+ });
+
+
+ it('should be $invalid but $pristine if not touched', function() {
+ compileInput('<input type="text" ng:model="name" name="alias" required />');
+
+ scope.$apply(function() {
+ scope.name = '';
+ });
+
+ expect(inputElm).toBeInvalid();
+ expect(inputElm).toBePristine();
+
+ changeInputValueTo('');
+ expect(inputElm).toBeInvalid();
+ expect(inputElm).toBeDirty();
+ });
+
+
+ it('should allow empty string if not required', function() {
+ compileInput('<input type="text" ng:model="foo" />');
+ changeInputValueTo('a');
+ changeInputValueTo('');
+ expect(scope.foo).toBe('');
+ });
+
+
+ it('should set $invalid when model undefined', function() {
+ compileInput('<input type="text" ng:model="notDefiend" required />');
+ scope.$digest();
+ expect(inputElm).toBeInvalid();
+ })
+ });
+
+
+ describe('ng:change', function() {
+
+ it('should $eval expression after new value is set in the model', function() {
+ compileInput('<input type="text" ng:model="value" ng:change="change()" />');
+
+ scope.change = jasmine.createSpy('change').andCallFake(function() {
+ expect(scope.value).toBe('new value');
+ });
+
+ changeInputValueTo('new value');
+ expect(scope.change).toHaveBeenCalledOnce();
+ });
+
+ it('should not $eval the expression if changed from model', function() {
+ compileInput('<input type="text" ng:model="value" ng:change="change()" />');
+
+ scope.change = jasmine.createSpy('change');
+ scope.$apply(function() {
+ scope.value = true;
+ });
+
+ expect(scope.change).not.toHaveBeenCalled();
+ });
+
+
+ it('should $eval ng:change expression on checkbox', function() {
+ compileInput('<input type="checkbox" ng:model="foo" ng:change="changeFn()">');
+
+ scope.changeFn = jasmine.createSpy('changeFn');
+ scope.$digest();
+ expect(scope.changeFn).not.toHaveBeenCalled();
+
+ browserTrigger(inputElm, 'click');
+ expect(scope.changeFn).toHaveBeenCalledOnce();
+ });
+ });
- itShouldVerify('text with pattern constraint on scope',
- ['', '000-00-0000', '123-45-6789'],
- ['x000-00-0000x', 'x'],
- {'ng:pattern':'regexp'}, function(scope){
- scope.regexp = /^\d\d\d-\d\d-\d\d\d\d$/;
- });
+ describe('ng:bind-change', function() {
- itShouldVerify('text with ng:minlength limit',
- ['', 'aaa', 'aaaaa', 'aaaaaaaaa'],
- ['a', 'aa'],
- {'ng:minlength': 3});
+ it('should bind keydown, change, input events', inject(function($browser) {
+ compileInput('<input type="text" ng:model="value" ng:bind-immediate />');
+ inputElm.val('value1');
+ browserTrigger(inputElm, 'keydown');
- itShouldVerify('text with ng:maxlength limit',
- ['', 'a', 'aa', 'aaa'],
- ['aaaa', 'aaaaa', 'aaaaaaaaa'],
- {'ng:maxlength': 3});
+ // should be async (because of keydown)
+ expect(scope.value).toBeUndefined();
+ $browser.defer.flush();
+ expect(scope.value).toBe('value1');
- it('should throw an error when scope pattern can\'t be found', inject(function($rootScope, $compile) {
- var el = jqLite('<input ng:model="foo" ng:pattern="fooRegexp">');
- $compile(el)($rootScope);
+ inputElm.val('value2');
+ browserTrigger(inputElm, 'change');
+ $browser.defer.flush();
+ expect(scope.value).toBe('value2');
- el.val('xx');
- browserTrigger(el, 'keydown');
- expect(function() { defer.flush(); }).
- toThrow('Expected fooRegexp to be a RegExp but was undefined');
+ if (msie < 9) return;
- dealoc(el);
+ inputElm.val('value3');
+ browserTrigger(inputElm, 'input');
+ $browser.defer.flush();
+ expect(scope.value).toBe('value3');
}));
});
});
diff --git a/test/widget/selectSpec.js b/test/widget/selectSpec.js
index 00bc2192..9db47c05 100644
--- a/test/widget/selectSpec.js
+++ b/test/widget/selectSpec.js
@@ -1,60 +1,69 @@
'use strict';
describe('select', function() {
- var compile = null, element = null, scope = null;
+ var scope, formElement, element, $compile;
- beforeEach(inject(function($compile, $rootScope) {
+ function compile(html) {
+ formElement = jqLite('<form name="form">' + html + '</form>');
+ element = formElement.find('select');
+ $compile(formElement)(scope);
+ scope.$apply();
+ }
+
+ beforeEach(inject(function($injector, $rootScope) {
scope = $rootScope;
- element = null;
- compile = function(html, parent) {
- if (parent) {
- parent.html(html);
- element = parent.children();
- } else {
- element = jqLite(html);
- }
- element = $compile(element)($rootScope);
- scope.$apply();
- return scope;
- };
+ $compile = $injector.get('$compile');
+ formElement = element = null;
}));
afterEach(function() {
- dealoc(element);
+ dealoc(formElement);
});
describe('select-one', function() {
- it('should compile children of a select without a name, but not create a model for it',
+ it('should compile children of a select without a ng:model, but not create a model for it',
function() {
compile('<select>' +
'<option selected="true">{{a}}</option>' +
'<option value="">{{b}}</option>' +
'<option>C</option>' +
'</select>');
- scope.a = 'foo';
- scope.b = 'bar';
- scope.$digest();
+ scope.$apply(function() {
+ scope.a = 'foo';
+ scope.b = 'bar';
+ });
expect(element.text()).toBe('foobarC');
});
- it('should require', inject(function($formFactory) {
- compile('<select name="select" ng:model="selection" required ng:change="log=log+\'change;\'">' +
+
+ it('should require', function() {
+ compile(
+ '<select name="select" ng:model="selection" required ng:change="change()">' +
'<option value=""></option>' +
'<option value="c">C</option>' +
'</select>');
- scope.log = '';
- scope.selection = 'c';
- scope.$digest();
- expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(undefined);
+
+ scope.change = function() {
+ scope.log += 'change;';
+ };
+
+ scope.$apply(function() {
+ scope.log = '';
+ scope.selection = 'c';
+ });
+
+ expect(scope.form.select.error.REQUIRED).toBeFalsy();
expect(element).toBeValid();
expect(element).toBePristine();
- scope.selection = '';
- scope.$digest();
- expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(true);
+ scope.$apply(function() {
+ scope.selection = '';
+ });
+
+ expect(scope.form.select.error.REQUIRED).toBeTruthy();
expect(element).toBeInvalid();
expect(element).toBePristine();
expect(scope.log).toEqual('');
@@ -64,10 +73,12 @@ describe('select', function() {
expect(element).toBeValid();
expect(element).toBeDirty();
expect(scope.log).toEqual('change;');
- }));
+ });
+
it('should not be invalid if no require', function() {
- compile('<select name="select" ng:model="selection">' +
+ compile(
+ '<select name="select" ng:model="selection">' +
'<option value=""></option>' +
'<option value="c">C</option>' +
'</select>');
@@ -75,35 +86,45 @@ describe('select', function() {
expect(element).toBeValid();
expect(element).toBePristine();
});
-
});
describe('select-multiple', function() {
+
it('should support type="select-multiple"', function() {
- compile('<select ng:model="selection" multiple>' +
- '<option>A</option>' +
- '<option>B</option>' +
- '</select>');
- scope.selection = ['A'];
- scope.$digest();
+ compile(
+ '<select ng:model="selection" multiple>' +
+ '<option>A</option>' +
+ '<option>B</option>' +
+ '</select>');
+
+ scope.$apply(function() {
+ scope.selection = ['A'];
+ });
+
expect(element[0].childNodes[0].selected).toEqual(true);
});
- it('should require', inject(function($formFactory) {
- compile('<select name="select" ng:model="selection" multiple required>' +
+
+ it('should require', function() {
+ compile(
+ '<select name="select" ng:model="selection" multiple required>' +
'<option>A</option>' +
'<option>B</option>' +
'</select>');
- scope.selection = [];
- scope.$digest();
- expect($formFactory.forElement(element).select.$error.REQUIRED).toEqual(true);
+ scope.$apply(function() {
+ scope.selection = [];
+ });
+
+ expect(scope.form.select.error.REQUIRED).toBeTruthy();
expect(element).toBeInvalid();
expect(element).toBePristine();
- scope.selection = ['A'];
- scope.$digest();
+ scope.$apply(function() {
+ scope.selection = ['A'];
+ });
+
expect(element).toBeValid();
expect(element).toBePristine();
@@ -111,17 +132,14 @@ describe('select', function() {
browserTrigger(element, 'change');
expect(element).toBeValid();
expect(element).toBeDirty();
- }));
-
+ });
});
describe('ng:options', function() {
- var select, scope;
-
- function createSelect(attrs, blank, unknown){
+ function createSelect(attrs, blank, unknown) {
var html = '<select';
- forEach(attrs, function(value, key){
+ forEach(attrs, function(value, key) {
if (isBoolean(value)) {
if (value) html += ' ' + key;
} else {
@@ -132,18 +150,18 @@ describe('select', function() {
(blank ? (isString(blank) ? blank : '<option value="">blank</option>') : '') +
(unknown ? (isString(unknown) ? unknown : '<option value="?">unknown</option>') : '') +
'</select>';
- select = jqLite(html);
- scope = compile(select);
+
+ compile(html);
}
- function createSingleSelect(blank, unknown){
+ function createSingleSelect(blank, unknown) {
createSelect({
'ng:model':'selected',
'ng:options':'value.name for value in values'
}, blank, unknown);
}
- function createMultiSelect(blank, unknown){
+ function createMultiSelect(blank, unknown) {
createSelect({
'ng:model':'selected',
'multiple':true,
@@ -151,366 +169,474 @@ describe('select', function() {
}, blank, unknown);
}
- afterEach(function() {
- dealoc(select);
- dealoc(scope);
- });
- it('should throw when not formated "? for ? in ?"', inject(function($rootScope, $exceptionHandler) {
+ it('should throw when not formated "? for ? in ?"', function() {
expect(function() {
compile('<select ng:model="selected" ng:options="i dont parse"></select>');
}).toThrow("Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in" +
" _collection_' but got 'i dont parse'.");
- }));
+ });
+
it('should render a list', function() {
createSingleSelect();
- scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
- scope.selected = scope.values[0];
- scope.$digest();
- var options = select.find('option');
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ var options = element.find('option');
expect(options.length).toEqual(3);
expect(sortedHtml(options[0])).toEqual('<option value="0">A</option>');
expect(sortedHtml(options[1])).toEqual('<option value="1">B</option>');
expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>');
});
+
it('should render an object', function() {
createSelect({
- 'ng:model':'selected',
+ 'ng:model': 'selected',
'ng:options': 'value as key for (key, value) in object'
});
- scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
- scope.selected = scope.object.red;
- scope.$digest();
- var options = select.find('option');
+
+ scope.$apply(function() {
+ scope.object = {'red': 'FF0000', 'green': '00FF00', 'blue': '0000FF'};
+ scope.selected = scope.object.red;
+ });
+
+ var options = element.find('option');
expect(options.length).toEqual(3);
expect(sortedHtml(options[0])).toEqual('<option value="blue">blue</option>');
expect(sortedHtml(options[1])).toEqual('<option value="green">green</option>');
expect(sortedHtml(options[2])).toEqual('<option value="red">red</option>');
expect(options[2].selected).toEqual(true);
- scope.object.azur = '8888FF';
- scope.$digest();
- options = select.find('option');
+ scope.$apply(function() {
+ scope.object.azur = '8888FF';
+ });
+
+ options = element.find('option');
expect(options[3].selected).toEqual(true);
});
+
it('should grow list', function() {
createSingleSelect();
- scope.values = [];
- scope.$digest();
- expect(select.find('option').length).toEqual(1); // because we add special empty option
- expect(sortedHtml(select.find('option')[0])).toEqual('<option value="?"></option>');
-
- scope.values.push({name:'A'});
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.find('option').length).toEqual(1);
- expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
-
- scope.values.push({name:'B'});
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
- expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>');
+
+ scope.$apply(function() {
+ scope.values = [];
+ });
+
+ expect(element.find('option').length).toEqual(1); // because we add special empty option
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="?"></option>');
+
+ scope.$apply(function() {
+ scope.values.push({name:'A'});
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(1);
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="0">A</option>');
+
+ scope.$apply(function() {
+ scope.values.push({name:'B'});
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="0">A</option>');
+ expect(sortedHtml(element.find('option')[1])).toEqual('<option value="1">B</option>');
});
+
it('should shrink list', function() {
createSingleSelect();
- scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.find('option').length).toEqual(3);
-
- scope.values.pop();
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
- expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>');
-
- scope.values.pop();
- scope.$digest();
- expect(select.find('option').length).toEqual(1);
- expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
-
- scope.values.pop();
- scope.selected = null;
- scope.$digest();
- expect(select.find('option').length).toEqual(1); // we add back the special empty option
+
+ scope.$apply(function() {
+ scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(3);
+
+ scope.$apply(function() {
+ scope.values.pop();
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="0">A</option>');
+ expect(sortedHtml(element.find('option')[1])).toEqual('<option value="1">B</option>');
+
+ scope.$apply(function() {
+ scope.values.pop();
+ });
+
+ expect(element.find('option').length).toEqual(1);
+ expect(sortedHtml(element.find('option')[0])).toEqual('<option value="0">A</option>');
+
+ scope.$apply(function() {
+ scope.values.pop();
+ scope.selected = null;
+ });
+
+ expect(element.find('option').length).toEqual(1); // we add back the special empty option
});
+
it('should shrink and then grow list', function() {
createSingleSelect();
- scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.find('option').length).toEqual(3);
-
- scope.values = [{name:'1'}, {name:'2'}];
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
-
- scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.find('option').length).toEqual(3);
+
+ scope.$apply(function() {
+ scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(3);
+
+ scope.$apply(function() {
+ scope.values = [{name: '1'}, {name: '2'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(2);
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(3);
});
+
it('should update list', function() {
createSingleSelect();
- scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
- scope.selected = scope.values[0];
- scope.$digest();
-
- scope.values = [{name:'B'}, {name:'C'}, {name:'D'}];
- scope.selected = scope.values[0];
- scope.$digest();
- var options = select.find('option');
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ scope.$apply(function() {
+ scope.values = [{name: 'B'}, {name: 'C'}, {name: 'D'}];
+ scope.selected = scope.values[0];
+ });
+
+ var options = element.find('option');
expect(options.length).toEqual(3);
expect(sortedHtml(options[0])).toEqual('<option value="0">B</option>');
expect(sortedHtml(options[1])).toEqual('<option value="1">C</option>');
expect(sortedHtml(options[2])).toEqual('<option value="2">D</option>');
});
+
it('should preserve existing options', function() {
createSingleSelect(true);
- scope.values = [];
- scope.$digest();
- expect(select.find('option').length).toEqual(1);
-
- scope.values = [{name:'A'}];
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(jqLite(select.find('option')[0]).text()).toEqual('blank');
- expect(jqLite(select.find('option')[1]).text()).toEqual('A');
-
- scope.values = [];
- scope.selected = null;
- scope.$digest();
- expect(select.find('option').length).toEqual(1);
- expect(jqLite(select.find('option')[0]).text()).toEqual('blank');
+ scope.$apply(function() {
+ scope.values = [];
+ });
+
+ expect(element.find('option').length).toEqual(1);
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(jqLite(element.find('option')[0]).text()).toEqual('blank');
+ expect(jqLite(element.find('option')[1]).text()).toEqual('A');
+
+ scope.$apply(function() {
+ scope.values = [];
+ scope.selected = null;
+ });
+
+ expect(element.find('option').length).toEqual(1);
+ expect(jqLite(element.find('option')[0]).text()).toEqual('blank');
});
+
describe('binding', function() {
+
it('should bind to scope value', function() {
createSingleSelect();
- scope.values = [{name:'A'}, {name:'B'}];
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.val()).toEqual('0');
- scope.selected = scope.values[1];
- scope.$digest();
- expect(select.val()).toEqual('1');
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[1];
+ });
+
+ expect(element.val()).toEqual('1');
});
+
it('should bind to scope value and group', function() {
createSelect({
- 'ng:model':'selected',
- 'ng:options':'item.name group by item.group for item in values'
- });
- scope.values = [{name:'A'},
- {name:'B', group:'first'},
- {name:'C', group:'second'},
- {name:'D', group:'first'},
- {name:'E', group:'second'}];
- scope.selected = scope.values[3];
- scope.$digest();
- expect(select.val()).toEqual('3');
+ 'ng:model': 'selected',
+ 'ng:options': 'item.name group by item.group for item in values'
+ });
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'},
+ {name: 'B', group: 'first'},
+ {name: 'C', group: 'second'},
+ {name: 'D', group: 'first'},
+ {name: 'E', group: 'second'}];
+ scope.selected = scope.values[3];
+ });
+
+ expect(element.val()).toEqual('3');
- var first = jqLite(select.find('optgroup')[0]);
+ var first = jqLite(element.find('optgroup')[0]);
var b = jqLite(first.find('option')[0]);
var d = jqLite(first.find('option')[1]);
expect(first.attr('label')).toEqual('first');
expect(b.text()).toEqual('B');
expect(d.text()).toEqual('D');
- var second = jqLite(select.find('optgroup')[1]);
+ var second = jqLite(element.find('optgroup')[1]);
var c = jqLite(second.find('option')[0]);
var e = jqLite(second.find('option')[1]);
expect(second.attr('label')).toEqual('second');
expect(c.text()).toEqual('C');
expect(e.text()).toEqual('E');
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.val()).toEqual('0');
+ scope.$apply(function() {
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
});
+
it('should bind to scope value through experession', function() {
- createSelect({'ng:model':'selected', 'ng:options':'item.id as item.name for item in values'});
- scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];
- scope.selected = scope.values[0].id;
- scope.$digest();
- expect(select.val()).toEqual('0');
+ createSelect({
+ 'ng:model': 'selected',
+ 'ng:options': 'item.id as item.name for item in values'
+ });
- scope.selected = scope.values[1].id;
- scope.$digest();
- expect(select.val()).toEqual('1');
+ scope.$apply(function() {
+ scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
+ scope.selected = scope.values[0].id;
+ });
+
+ expect(element.val()).toEqual('0');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[1].id;
+ });
+
+ expect(element.val()).toEqual('1');
});
+
it('should bind to object key', function() {
createSelect({
- 'ng:model':'selected',
- 'ng:options':'key as value for (key, value) in object'
+ 'ng:model': 'selected',
+ 'ng:options': 'key as value for (key, value) in object'
});
- scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
- scope.selected = 'green';
- scope.$digest();
- expect(select.val()).toEqual('green');
- scope.selected = 'blue';
- scope.$digest();
- expect(select.val()).toEqual('blue');
+ scope.$apply(function() {
+ scope.object = {red: 'FF0000', green: '00FF00', blue: '0000FF'};
+ scope.selected = 'green';
+ });
+
+ expect(element.val()).toEqual('green');
+
+ scope.$apply(function() {
+ scope.selected = 'blue';
+ });
+
+ expect(element.val()).toEqual('blue');
});
+
it('should bind to object value', function() {
createSelect({
- 'ng:model':'selected',
- 'ng:options':'value as key for (key, value) in object'
+ 'ng:model': 'selected',
+ 'ng:options': 'value as key for (key, value) in object'
});
- scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
- scope.selected = '00FF00';
- scope.$digest();
- expect(select.val()).toEqual('green');
- scope.selected = '0000FF';
- scope.$digest();
- expect(select.val()).toEqual('blue');
+ scope.$apply(function() {
+ scope.object = {red: 'FF0000', green: '00FF00', blue:'0000FF'};
+ scope.selected = '00FF00';
+ });
+
+ expect(element.val()).toEqual('green');
+
+ scope.$apply(function() {
+ scope.selected = '0000FF';
+ });
+
+ expect(element.val()).toEqual('blue');
});
+
it('should insert a blank option if bound to null', function() {
createSingleSelect();
- scope.values = [{name:'A'}];
- scope.selected = null;
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(select.val()).toEqual('');
- expect(jqLite(select.find('option')[0]).val()).toEqual('');
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.val()).toEqual('0');
- expect(select.find('option').length).toEqual(1);
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}];
+ scope.selected = null;
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.val()).toEqual('');
+ expect(jqLite(element.find('option')[0]).val()).toEqual('');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+ expect(element.find('option').length).toEqual(1);
});
+
it('should reuse blank option if bound to null', function() {
createSingleSelect(true);
- scope.values = [{name:'A'}];
- scope.selected = null;
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(select.val()).toEqual('');
- expect(jqLite(select.find('option')[0]).val()).toEqual('');
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.val()).toEqual('0');
- expect(select.find('option').length).toEqual(2);
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}];
+ scope.selected = null;
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.val()).toEqual('');
+ expect(jqLite(element.find('option')[0]).val()).toEqual('');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+ expect(element.find('option').length).toEqual(2);
});
+
it('should insert a unknown option if bound to something not in the list', function() {
createSingleSelect();
- scope.values = [{name:'A'}];
- scope.selected = {};
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(select.val()).toEqual('?');
- expect(jqLite(select.find('option')[0]).val()).toEqual('?');
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.val()).toEqual('0');
- expect(select.find('option').length).toEqual(1);
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}];
+ scope.selected = {};
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.val()).toEqual('?');
+ expect(jqLite(element.find('option')[0]).val()).toEqual('?');
+
+ scope.$apply(function() {
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+ expect(element.find('option').length).toEqual(1);
});
+
it('should select correct input if previously selected option was "?"', function() {
createSingleSelect();
- scope.values = [{name:'A'},{name:'B'}];
- scope.selected = {};
- scope.$digest();
- expect(select.find('option').length).toEqual(3);
- expect(select.val()).toEqual('?');
- expect(select.find('option').eq(0).val()).toEqual('?');
- browserTrigger(select.find('option').eq(1));
- expect(select.val()).toEqual('0');
- expect(select.find('option').eq(0).prop('selected')).toBeTruthy();
- expect(select.find('option').length).toEqual(2);
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = {};
+ });
+
+ expect(element.find('option').length).toEqual(3);
+ expect(element.val()).toEqual('?');
+ expect(element.find('option').eq(0).val()).toEqual('?');
+
+ browserTrigger(element.find('option').eq(1));
+ expect(element.val()).toEqual('0');
+ expect(element.find('option').eq(0).prop('selected')).toBeTruthy();
+ expect(element.find('option').length).toEqual(2);
});
});
describe('blank option', function () {
+
it('should be compiled as template, be watched and updated', function () {
var option;
-
createSingleSelect('<option value="">blank is {{blankVal}}</option>');
- scope.blankVal = 'so blank';
- scope.values = [{name:'A'}];
- scope.$digest();
+
+ scope.$apply(function() {
+ scope.blankVal = 'so blank';
+ scope.values = [{name: 'A'}];
+ });
// check blank option is first and is compiled
- expect(select.find('option').length == 2);
- option = jqLite(select.find('option')[0]);
+ expect(element.find('option').length).toBe(2);
+ option = element.find('option').eq(0);
expect(option.val()).toBe('');
expect(option.text()).toBe('blank is so blank');
- // change blankVal and $digest
- scope.blankVal = 'not so blank';
- scope.$digest();
+ scope.$apply(function() {
+ scope.blankVal = 'not so blank';
+ });
// check blank option is first and is compiled
- expect(select.find('option').length == 2);
- option = jqLite(select.find('option')[0]);
+ expect(element.find('option').length).toBe(2);
+ option = element.find('option').eq(0);
expect(option.val()).toBe('');
expect(option.text()).toBe('blank is not so blank');
});
+
it('should support binding via ng:bind-template attribute', function () {
var option;
-
createSingleSelect('<option value="" ng:bind-template="blank is {{blankVal}}"></option>');
- scope.blankVal = 'so blank';
- scope.values = [{name:'A'}];
- scope.$digest();
+
+ scope.$apply(function() {
+ scope.blankVal = 'so blank';
+ scope.values = [{name: 'A'}];
+ });
// check blank option is first and is compiled
- expect(select.find('option').length == 2);
- option = jqLite(select.find('option')[0]);
+ expect(element.find('option').length).toBe(2);
+ option = element.find('option').eq(0);
expect(option.val()).toBe('');
expect(option.text()).toBe('blank is so blank');
});
+
it('should support biding via ng:bind attribute', function () {
var option;
-
createSingleSelect('<option value="" ng:bind="blankVal"></option>');
- scope.blankVal = 'is blank';
- scope.values = [{name:'A'}];
- scope.$digest();
+
+ scope.$apply(function() {
+ scope.blankVal = 'is blank';
+ scope.values = [{name: 'A'}];
+ });
// check blank option is first and is compiled
- expect(select.find('option').length == 2);
- option = jqLite(select.find('option')[0]);
+ expect(element.find('option').length).toBe(2);
+ option = element.find('option').eq(0);
expect(option.val()).toBe('');
expect(option.text()).toBe('is blank');
});
+
it('should be rendered with the attributes preserved', function () {
var option;
-
createSingleSelect('<option value="" class="coyote" id="road-runner" ' +
'custom-attr="custom-attr">{{blankVal}}</option>');
- scope.blankVal = 'is blank';
- scope.$digest();
+
+ scope.$apply(function() {
+ scope.blankVal = 'is blank';
+ });
// check blank option is first and is compiled
- option = jqLite(select.find('option')[0]);
+ option = element.find('option').eq(0);
expect(option.hasClass('coyote')).toBeTruthy();
expect(option.attr('id')).toBe('road-runner');
expect(option.attr('custom-attr')).toBe('custom-attr');
@@ -519,76 +645,101 @@ describe('select', function() {
describe('on change', function() {
+
it('should update model on change', function() {
createSingleSelect();
- scope.values = [{name:'A'}, {name:'B'}];
- scope.selected = scope.values[0];
- scope.$digest();
- expect(select.val()).toEqual('0');
- select.val('1');
- browserTrigger(select, 'change');
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = scope.values[0];
+ });
+
+ expect(element.val()).toEqual('0');
+
+ element.val('1');
+ browserTrigger(element, 'change');
expect(scope.selected).toEqual(scope.values[1]);
});
+
it('should update model on change through expression', function() {
- createSelect({'ng:model':'selected', 'ng:options':'item.id as item.name for item in values'});
- scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];
- scope.selected = scope.values[0].id;
- scope.$digest();
- expect(select.val()).toEqual('0');
+ createSelect({
+ 'ng:model': 'selected',
+ 'ng:options': 'item.id as item.name for item in values'
+ });
- select.val('1');
- browserTrigger(select, 'change');
+ scope.$apply(function() {
+ scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
+ scope.selected = scope.values[0].id;
+ });
+
+ expect(element.val()).toEqual('0');
+
+ element.val('1');
+ browserTrigger(element, 'change');
expect(scope.selected).toEqual(scope.values[1].id);
});
+
it('should update model to null on change', function() {
createSingleSelect(true);
- scope.values = [{name:'A'}, {name:'B'}];
- scope.selected = scope.values[0];
- select.val('0');
- scope.$digest();
- select.val('');
- browserTrigger(select, 'change');
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = scope.values[0];
+ element.val('0');
+ });
+
+ element.val('');
+ browserTrigger(element, 'change');
expect(scope.selected).toEqual(null);
});
});
+
describe('select-many', function() {
+
it('should read multiple selection', function() {
createMultiSelect();
- scope.values = [{name:'A'}, {name:'B'}];
- scope.selected = [];
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(jqLite(select.find('option')[0]).prop('selected')).toBeFalsy();
- expect(jqLite(select.find('option')[1]).prop('selected')).toBeFalsy();
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = [];
+ });
- scope.selected.push(scope.values[1]);
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(select.find('option')[0].selected).toEqual(false);
- expect(select.find('option')[1].selected).toEqual(true);
+ expect(element.find('option').length).toEqual(2);
+ expect(element.find('option')[0].selected).toBeFalsy();
+ expect(element.find('option')[1].selected).toBeFalsy();
- scope.selected.push(scope.values[0]);
- scope.$digest();
- expect(select.find('option').length).toEqual(2);
- expect(select.find('option')[0].selected).toEqual(true);
- expect(select.find('option')[1].selected).toEqual(true);
+ scope.$apply(function() {
+ scope.selected.push(scope.values[1]);
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.find('option')[0].selected).toBeFalsy();
+ expect(element.find('option')[1].selected).toBeTruthy();
+
+ scope.$apply(function() {
+ scope.selected.push(scope.values[0]);
+ });
+
+ expect(element.find('option').length).toEqual(2);
+ expect(element.find('option')[0].selected).toBeTruthy();
+ expect(element.find('option')[1].selected).toBeTruthy();
});
+
it('should update model on change', function() {
createMultiSelect();
- scope.values = [{name:'A'}, {name:'B'}];
- scope.selected = [];
- scope.$digest();
- select.find('option')[0].selected = true;
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}];
+ scope.selected = [];
+ });
- browserTrigger(select, 'change');
+ element.find('option')[0].selected = true;
+
+ browserTrigger(element, 'change');
expect(scope.selected).toEqual([scope.values[0]]);
});
@@ -602,16 +753,57 @@ describe('select', function() {
scope.selected = ['1'];
scope.$digest();
- expect(select.find('option')[1].selected).toBe(true);
+ expect(element.find('option')[1].selected).toBe(true);
- select.find('option')[0].selected = true;
- browserTrigger(select, 'change');
+ element.find('option')[0].selected = true;
+ browserTrigger(element, 'change');
expect(scope.selected).toEqual(['0', '1']);
- select.find('option')[1].selected = false;
- browserTrigger(select, 'change');
+ element.find('option')[1].selected = false;
+ browserTrigger(element, 'change');
expect(scope.selected).toEqual(['0']);
});
});
+
+
+ describe('ng:required', function() {
+
+ it('should allow bindings on ng:required', function() {
+ createSelect({
+ 'ng:model': 'value',
+ 'ng:options': 'item.name for item in values',
+ 'ng:required': '{{required}}'
+ }, true);
+
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A', id: 1}, {name: 'B', id: 2}];
+ scope.required = false;
+ });
+
+ element.val('');
+ browserTrigger(element, 'change');
+ expect(element).toBeValid();
+
+ scope.$apply(function() {
+ scope.required = true;
+ });
+ expect(element).toBeInvalid();
+
+ scope.$apply(function() {
+ scope.value = scope.values[0];
+ });
+ expect(element).toBeValid();
+
+ element.val('');
+ browserTrigger(element, 'change');
+ expect(element).toBeInvalid();
+
+ scope.$apply(function() {
+ scope.required = false;
+ });
+ expect(element).toBeValid();
+ });
+ });
});
});