diff options
Diffstat (limited to 'src/widget')
| -rw-r--r-- | src/widget/form.js | 134 | ||||
| -rw-r--r-- | src/widget/input.js | 1007 | ||||
| -rw-r--r-- | src/widget/select.js | 138 |
3 files changed, 755 insertions, 524 deletions
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 }); } |
