aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorVojta Jina2012-02-15 17:16:02 -0800
committerVojta Jina2012-02-28 17:46:58 -0800
commit21c725f1a12d1de758cab6e4c4fafc5c420eb565 (patch)
tree4d1b362387de2c41748a63b5baee0f18c3c8e5ec /src
parente23fa768aaf6d1d966c335979fe8316330c2fe28 (diff)
downloadangular.js-21c725f1a12d1de758cab6e4c4fafc5c420eb565.tar.bz2
refactor(forms): Even better forms
- remove $formFactory completely - remove parallel scope hierarchy (forms, widgets) - use new compiler features (widgets, forms are controllers) - any directive can add formatter/parser (validators, convertors) Breaks no custom input types Breaks removed integer input type Breaks remove list input type (ng-list directive instead) Breaks inputs bind only blur event by default (added ng:bind-change directive)
Diffstat (limited to 'src')
-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
8 files changed, 764 insertions, 954 deletions
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
});
}