aboutsummaryrefslogtreecommitdiffstats
path: root/src/widget
diff options
context:
space:
mode:
Diffstat (limited to 'src/widget')
-rw-r--r--src/widget/form.js81
-rw-r--r--src/widget/input.js773
-rw-r--r--src/widget/select.js427
3 files changed, 1281 insertions, 0 deletions
diff --git a/src/widget/form.js b/src/widget/form.js
new file mode 100644
index 00000000..bc34bf0d
--- /dev/null
+++ b/src/widget/form.js
@@ -0,0 +1,81 @@
+'use strict';
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.form
+ *
+ * @description
+ * Angular widget that creates a form scope using the
+ * {@link angular.service.$formFactory $formFactory} API. The resulting form scope instance is
+ * attached to the DOM element using the jQuery `.data()` method under the `$form` key.
+ * See {@link guide/dev_guide.forms forms} on detailed discussion of forms and widgets.
+ *
+ *
+ * # Alias: `ng:form`
+ *
+ * In angular forms can be nested. This means that the outer form is valid when all of the child
+ * forms are valid as well. However browsers do not allow nesting of `<form>` elements, for this
+ * reason angular provides `<ng:form>` alias which behaves identical to `<form>` but allows
+ * element nesting.
+ *
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.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>
+ <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>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function(){
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function(){
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularWidget('form', function(form){
+ this.descend(true);
+ this.directives(true);
+ return annotate('$formFactory', function($formFactory, formElement) {
+ var name = formElement.attr('name'),
+ parentForm = $formFactory.forElement(formElement),
+ form = $formFactory(parentForm);
+ formElement.data('$form', form);
+ formElement.bind('submit', function(event){
+ event.preventDefault();
+ });
+ if (name) {
+ this[name] = form;
+ }
+ watch('valid');
+ watch('invalid');
+ function watch(name) {
+ form.$watch('$' + name, function(scope, value) {
+ formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ }
+ });
+});
+
+angularWidget('ng:form', angularWidget('form'));
diff --git a/src/widget/input.js b/src/widget/input.js
new file mode 100644
index 00000000..f82027f4
--- /dev/null
+++ b/src/widget/input.js
@@ -0,0 +1,773 @@
+'use strict';
+
+
+var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
+var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
+var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
+var INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.text
+ *
+ * @description
+ * Standard HTML text input with angular data binding.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'guest';
+ this.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>
+ <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>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if multi word', function() {
+ input('text').enter('hello world');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.email
+ *
+ * @description
+ * Text input with email validation. Sets the `EMAIL` validation error key if not a valid email
+ * address.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.text = 'me@example.com';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <form name="myForm">
+ Email: <input type="email" name="input" ng:model="text" required>
+ <span class="error" ng:show="myForm.input.$error.REQUIRED">
+ Required!</span>
+ <span class="error" ng:show="myForm.input.$error.EMAIL">
+ Not valid email!</span>
+ </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');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if not email', function() {
+ input('text').enter('xxx');
+ expect(binding('text')).toEqual('xxx');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('email', function() {
+ var widget = this;
+ this.$on('$validate', function(event){
+ var value = widget.$viewValue;
+ widget.$emit(!value || value.match(EMAIL_REGEXP) ? "$valid" : "$invalid", "EMAIL");
+ });
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.url
+ *
+ * @description
+ * Text input with URL validation. Sets the `URL` validation error key if the content is not a
+ * valid URL.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.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>
+ <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>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('http://google.com');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if not url', function() {
+ input('text').enter('xxx');
+ expect(binding('text')).toEqual('xxx');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('url', function() {
+ var widget = this;
+ this.$on('$validate', function(event){
+ var value = widget.$viewValue;
+ widget.$emit(!value || value.match(URL_REGEXP) ? "$valid" : "$invalid", "URL");
+ });
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.list
+ *
+ * @description
+ * Text input that converts between comma-seperated string into an array of strings.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.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>
+ <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>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('names')).toEqual('["igor","misko","vojta"]');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('names').enter('');
+ expect(binding('names')).toEqual('[]');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('list', function() {
+ function parse(viewValue) {
+ var list = [];
+ forEach(viewValue.split(/\s*,\s*/), function(value){
+ if (value) list.push(trim(value));
+ });
+ return list;
+ }
+ this.$parseView = function() {
+ isString(this.$viewValue) && (this.$modelValue = parse(this.$viewValue));
+ };
+ this.$parseModel = function() {
+ var modelValue = this.$modelValue;
+ if (isArray(modelValue)
+ && (!isString(this.$viewValue) || !equals(parse(this.$viewValue), modelValue))) {
+ this.$viewValue = modelValue.join(', ');
+ }
+ };
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.number
+ *
+ * @description
+ * Text input with number validation and transformation. Sets the `NUMBER` validation
+ * error if not a valid number.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} min Sets the `MIN` validation error key if the value entered is less then `min`.
+ * @param {string=} max Sets the `MAX` validation error key if the value entered is greater then `min`.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.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>
+ <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('');
+ expect(binding('value')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+
+ it('should be invalid if over max', function() {
+ input('value').enter('123');
+ expect(binding('value')).toEqual('123');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('number', numericRegexpInputType(NUMBER_REGEXP, 'NUMBER'));
+
+/**
+ * @workInProgress
+ * @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 {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.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');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.checkbox
+ *
+ * @description
+ * HTML checkbox.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} true-value The value to which the expression should be set when selected.
+ * @param {string=} false-value The value to which the expression should be set when not selected.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.value1 = true;
+ this.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"
+ true-value="YES" false-value="NO"> <br/>
+ </form>
+ <tt>value1 = {{value1}}</tt><br/>
+ <tt>value2 = {{value2}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should change state', function() {
+ expect(binding('value1')).toEqual('true');
+ expect(binding('value2')).toEqual('YES');
+
+ input('value1').check();
+ input('value2').check();
+ expect(binding('value1')).toEqual('false');
+ expect(binding('value2')).toEqual('NO');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('checkbox', function (inputElement) {
+ var widget = this,
+ trueValue = inputElement.attr('true-value'),
+ falseValue = inputElement.attr('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 = this.$modelValue === trueValue;
+ };
+
+ widget.$parseView = function() {
+ widget.$modelValue = widget.$viewValue ? trueValue : falseValue;
+ };
+
+});
+
+/**
+ * @workInProgress
+ * @ngdoc inputType
+ * @name angular.inputType.radio
+ *
+ * @description
+ * HTML radio.
+ *
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string} value The value to which the expression should be set when selected.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.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>
+ <tt>color = {{color}}</tt><br/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should change state', function() {
+ expect(binding('color')).toEqual('blue');
+
+ input('color').select('red');
+ expect(binding('color')).toEqual('red');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularInputType('radio', function(inputElement) {
+ var widget = this,
+ value = inputElement.attr('value');
+
+ //correct the name
+ inputElement.attr('name', widget.$id + '@' + inputElement.attr('name'));
+ inputElement.bind('click', function() {
+ widget.$apply(function() {
+ if (inputElement[0].checked) {
+ widget.$emit('$viewChange', value);
+ }
+ });
+ });
+
+ widget.$render = function() {
+ inputElement[0].checked = value == widget.$viewValue;
+ };
+
+ if (inputElement[0].checked) {
+ widget.$viewValue = value;
+ }
+});
+
+
+function numericRegexpInputType(regexp, error) {
+ return function(inputElement) {
+ var widget = this,
+ 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() {
+ if (isNumber(widget.$modelValue)) {
+ widget.$viewValue = '' + 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");
+
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.input
+ *
+ * @description
+ * HTML input element widget with angular data-binding. Input widget follows HTML5 input types
+ * and polyfills the HTML5 validation behavior for older browsers.
+ *
+ * The {@link angular.inputType custom angular.inputType}s provides a short hand for declaring new
+ * inputs. This is a shart hand for text-box based inputs, and there is no need to go through the
+ * full {@link angular.service.$formFactory $formFactory} widget lifecycle.
+ *
+ *
+ * @param {string} type Widget types as defined by {@link angular.inputType}. If the
+ * type is in the format of `@ScopeType` then `ScopeType` is loaded from the
+ * current scope, allowing quick definition of type.
+ * @param {string} ng:model Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the widgets is published.
+ * @param {string=} required Sets `REQUIRED` validation error key if the value is not entered.
+ * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the
+ * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
+ * patterns defined as scope expressions.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl(){
+ this.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.input.$error.REQUIRED">
+ Required!</span>
+ </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/>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should initialize to model', function() {
+ expect(binding('text')).toEqual('guest');
+ expect(binding('myForm.input.$valid')).toEqual('true');
+ });
+
+ it('should be invalid if empty', function() {
+ input('text').enter('');
+ expect(binding('text')).toEqual('');
+ expect(binding('myForm.input.$valid')).toEqual('false');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+angularWidget('input', function (inputElement){
+ this.directives(true);
+ this.descend(true);
+ var modelExp = inputElement.attr('ng:model');
+ return modelExp &&
+ annotate('$defer', '$formFactory', function($defer, $formFactory, inputElement){
+ 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 = inputElement[0].getAttribute('type') || 'text',
+ TypeController,
+ modelScope = this,
+ patternMatch, widget,
+ pattern = trim(inputElement.attr('ng:pattern')),
+ loadFromScope = type.match(/^\s*\@\s*(.*)/);
+
+
+ if (!pattern) {
+ patternMatch = valueFn(true);
+ } else {
+ if (pattern.match(/^\/(.*)\/$/)) {
+ pattern = new RegExp(pattern.substring(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(this.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn
+ : 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.
+ }
+ }
+
+ !TypeController.$inject && (TypeController.$inject = []);
+ widget = form.$createWidget({
+ scope: modelScope,
+ model: modelExp,
+ onChange: inputElement.attr('ng:change'),
+ alias: inputElement.attr('name'),
+ controller: TypeController,
+ controllerArgs: [inputElement]});
+
+ widget.$pattern =
+ watchElementProperty(this, widget, 'required', inputElement);
+ watchElementProperty(this, widget, 'readonly', inputElement);
+ watchElementProperty(this, widget, 'disabled', inputElement);
+
+
+ widget.$pristine = !(widget.$dirty = false);
+
+ widget.$on('$validate', function(event) {
+ var $viewValue = trim(widget.$viewValue);
+ var inValid = widget.$required && !$viewValue;
+ var 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');
+ }
+ });
+
+ forEach(['valid', 'invalid', 'pristine', 'dirty'], function (name) {
+ widget.$watch('$' + name, function(scope, 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', 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);
+ }
+ });
+ }
+ });
+ }
+ });
+
+});
+
+angularWidget('textarea', angularWidget('input'));
+
+
+function watchElementProperty(modelScope, widget, name, element) {
+ var bindAttr = fromJson(element.attr('ng:bind-attr') || '{}'),
+ match = /\s*{{(.*)}}\s*/.exec(bindAttr[name]);
+ widget['$' + name] =
+ // some browsers return true some '' when required is set without value.
+ isString(element.prop(name)) || !!element.prop(name) ||
+ // this is needed for ie9, since it will treat boolean attributes as false
+ !!element[0].attributes[name];
+ if (bindAttr[name] && match) {
+ modelScope.$watch(match[1], function(scope, value){
+ widget['$' + name] = !!value;
+ widget.$emit('$validate');
+ });
+ }
+}
+
diff --git a/src/widget/select.js b/src/widget/select.js
new file mode 100644
index 00000000..f397180e
--- /dev/null
+++ b/src/widget/select.js
@@ -0,0 +1,427 @@
+'use strict';
+
+/**
+ * @workInProgress
+ * @ngdoc widget
+ * @name angular.widget.select
+ *
+ * @description
+ * HTML `SELECT` element with angular data-binding.
+ *
+ * # `ng:options`
+ *
+ * Optionally `ng:options` attribute can be used to dynamically generate a list of `<option>`
+ * elements for a `<select>` element using an array or an object obtained by evaluating the
+ * `ng:options` expression.
+ *
+ * When an item in the select menu is select, the value of array element or object property
+ * represented by the selected option will be bound to the model identified by the `name` attribute
+ * of the parent select element.
+ *
+ * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
+ * be nested into the `<select>` element. This element will then represent `null` or "not selected"
+ * option. See example below for demonstration.
+ *
+ * Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
+ * of {@link angular.widget.@ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
+ * `<option>` element because of the following reasons:
+ *
+ * * value attribute of the option element that we need to bind to requires a string, but the
+ * source of data for the iteration might be in a form of array containing objects instead of
+ * strings
+ * * {@link angular.widget.@ng:repeat ng:repeat} unrolls after the select binds causing
+ * incorect rendering on most browsers.
+ * * binding to a value not in list confuses most browsers.
+ *
+ * @param {string} name assignable expression to data-bind to.
+ * @param {string=} required The widget is considered valid only if value is entered.
+ * @param {comprehension_expression=} ng:options in one of the following forms:
+ *
+ * * for array data sources:
+ * * `label` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * for object data sources:
+ * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`group by`** `group`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ *
+ * Where:
+ *
+ * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
+ * * `value`: local variable which will refer to each item in the `array` or each property value
+ * of `object` during iteration.
+ * * `key`: local variable which will refer to a property name in `object` during iteration.
+ * * `label`: The result of this expression will be the label for `<option>` element. The
+ * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
+ * * `select`: The result of this expression will be bound to the model of the parent `<select>`
+ * element. If not specified, `select` expression will default to `value`.
+ * * `group`: The result of this expression will be used to group options using the `<optgroup>`
+ * DOM element.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function MyCntrl(){
+ this.colors = [
+ {name:'black', shade:'dark'},
+ {name:'white', shade:'light'},
+ {name:'red', shade:'dark'},
+ {name:'blue', shade:'dark'},
+ {name:'yellow', shade:'light'}
+ ];
+ this.color = this.colors[2]; // red
+ }
+ </script>
+ <div ng:controller="MyCntrl">
+ <ul>
+ <li ng:repeat="color in colors">
+ Name: <input ng:model="color.name">
+ [<a href ng:click="colors.$remove(color)">X</a>]
+ </li>
+ <li>
+ [<a href ng:click="colors.push({})">add</a>]
+ </li>
+ </ul>
+ <hr/>
+ Color (null not allowed):
+ <select ng:model="color" ng:options="c.name for c in colors"></select><br>
+
+ Color (null allowed):
+ <div class="nullable">
+ <select ng:model="color" ng:options="c.name for c in colors">
+ <option value="">-- chose color --</option>
+ </select>
+ </div><br/>
+
+ Color grouped by shade:
+ <select ng:model="color" ng:options="c.name group by c.shade for c in colors">
+ </select><br/>
+
+
+ Select <a href ng:click="color={name:'not in list'}">bogus</a>.<br>
+ <hr/>
+ Currently selected: {{ {selected_color:color} }}
+ <div style="border:solid 1px black; height:20px"
+ ng:style="{'background-color':color.name}">
+ </div>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:options', function(){
+ expect(binding('color')).toMatch('red');
+ select('color').option('0');
+ expect(binding('color')).toMatch('black');
+ using('.nullable').select('color').option('');
+ expect(binding('color')).toMatch('null');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+
+ //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+(.*)$/;
+
+
+angularWidget('select', function (element){
+ this.directives(true);
+ this.descend(true);
+ return element.attr('ng:model') && annotate('$formFactory', function($formFactory, selectElement){
+ var modelScope = this,
+ match,
+ form = $formFactory.forElement(selectElement),
+ multiple = selectElement.attr('multiple'),
+ optionsExp = selectElement.attr('ng:options'),
+ modelExp = selectElement.attr('ng:model'),
+ widget = form.$createWidget({
+ scope: this,
+ model: modelExp,
+ onChange: selectElement.attr('ng:change'),
+ alias: selectElement.attr('name'),
+ controller: optionsExp ? Options : (multiple ? Multiple : Single)});
+
+ selectElement.bind('$destroy', function(){ widget.$destroy(); });
+
+ widget.$pristine = !(widget.$dirty = false);
+
+ watchElementProperty(modelScope, widget, 'required', selectElement);
+ watchElementProperty(modelScope, widget, 'readonly', selectElement);
+ watchElementProperty(modelScope, widget, 'disabled', selectElement);
+
+ widget.$on('$validate', function(){
+ var valid = !widget.$required || !!widget.$modelValue;
+ if (valid && multiple && widget.$required) valid = !!widget.$modelValue.length;
+ if (valid !== !widget.$error.REQUIRED) {
+ widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED');
+ }
+ });
+
+ widget.$on('$viewChange', function(){
+ widget.$pristine = !(widget.$dirty = true);
+ });
+
+ forEach(['valid', 'invalid', 'pristine', 'dirty'], function (name) {
+ widget.$watch('$' + name, function(scope, value) {
+ selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);
+ });
+ });
+
+ ////////////////////////////
+
+ function Multiple(){
+ var widget = this;
+
+ this.$render = function(){
+ var items = new HashMap(this.$viewValue);
+ forEach(selectElement.children(), function(option){
+ option.selected = isDefined(items.get(option.value));
+ });
+ };
+
+ 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);
+ });
+ });
+
+ }
+
+ function Single(){
+ var widget = this;
+
+ widget.$render = function(){
+ selectElement.val(widget.$viewValue);
+ };
+
+ selectElement.bind('change', function(){
+ widget.$apply(function(){
+ widget.$emit('$viewChange', selectElement.val());
+ });
+ });
+
+ widget.$viewValue = selectElement.val();
+ }
+
+ function Options(){
+ var widget = this,
+ match;
+
+ if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
+ throw Error(
+ "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
+ " but got '" + optionsExp + "'.");
+ }
+
+ var widgetScope = this,
+ displayFn = expressionCompile(match[2] || match[1]),
+ valueName = match[4] || match[6],
+ keyName = match[5],
+ groupByFn = expressionCompile(match[3] || ''),
+ valueFn = expressionCompile(match[2] ? match[1] : valueName),
+ valuesFn = expressionCompile(match[7]),
+ // we can't just jqLite('<option>') since jqLite is not smart enough
+ // to create it in <select> and IE barfs otherwise.
+ optionTemplate = jqLite(document.createElement('option')),
+ optGroupTemplate = jqLite(document.createElement('optgroup')),
+ nullOption = false, // if false then user will not be able to select it
+ // This is an array of array of existing option groups in DOM. We try to reuse these if possible
+ // optionGroupsCache[0] is the options with no option group
+ // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
+ optionGroupsCache = [[{element: selectElement, label:''}]],
+ inChangeEvent;
+
+ // find existing special options
+ forEach(selectElement.children(), function(option){
+ if (option.value == '')
+ // User is allowed to select the null.
+ nullOption = {label:jqLite(option).text(), id:''};
+ });
+ selectElement.html(''); // clear contents
+
+ selectElement.bind('change', function(){
+ widgetScope.$apply(function(){
+ var optionGroup,
+ collection = valuesFn(modelScope) || [],
+ key = selectElement.val(),
+ tempScope = inherit(modelScope),
+ value, optionElement, index, groupIndex, length, groupLength;
+
+ if (multiple) {
+ value = [];
+ for (groupIndex = 0, groupLength = optionGroupsCache.length;
+ groupIndex < groupLength;
+ groupIndex++) {
+ // list of options for that group. (first item has the parent)
+ optionGroup = optionGroupsCache[groupIndex];
+
+ for(index = 1, length = optionGroup.length; index < length; index++) {
+ if ((optionElement = optionGroup[index].element)[0].selected) {
+ if (keyName) tempScope[keyName] = key;
+ tempScope[valueName] = collection[optionElement.val()];
+ value.push(valueFn(tempScope));
+ }
+ }
+ }
+ } else {
+ if (key == '?') {
+ value = undefined;
+ } else if (key == ''){
+ value = null;
+ } else {
+ tempScope[valueName] = collection[key];
+ if (keyName) tempScope[keyName] = key;
+ value = valueFn(tempScope);
+ }
+ }
+ if (isDefined(value) && modelScope.$viewVal !== value) {
+ widgetScope.$emit('$viewChange', value);
+ }
+ });
+ });
+
+ widgetScope.$watch(render);
+ widgetScope.$render = render;
+
+ function render() {
+ var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
+ optionGroupNames = [''],
+ optionGroupName,
+ optionGroup,
+ option,
+ existingParent, existingOptions, existingOption,
+ modelValue = widget.$modelValue,
+ values = valuesFn(modelScope) || [],
+ keys = keyName ? sortedKeys(values) : values,
+ groupLength, length,
+ groupIndex, index,
+ optionScope = inherit(modelScope),
+ selected,
+ selectedSet = false, // nothing is selected yet
+ lastElement,
+ element;
+
+ if (multiple) {
+ selectedSet = new HashMap(modelValue);
+ } else if (modelValue === null || nullOption) {
+ // if we are not multiselect, and we are null then we have to add the nullOption
+ optionGroups[''].push(extend({selected:modelValue === null, id:'', label:''}, nullOption));
+ selectedSet = true;
+ }
+
+ // We now build up the list of options we need (we merge later)
+ for (index = 0; length = keys.length, index < length; index++) {
+ optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index];
+ optionGroupName = groupByFn(optionScope) || '';
+ if (!(optionGroup = optionGroups[optionGroupName])) {
+ optionGroup = optionGroups[optionGroupName] = [];
+ optionGroupNames.push(optionGroupName);
+ }
+ if (multiple) {
+ selected = selectedSet.remove(valueFn(optionScope)) != undefined;
+ } else {
+ selected = modelValue === valueFn(optionScope);
+ 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(optionScope) || '', // what will be seen by the user
+ selected: selected // determine if we should be selected
+ });
+ }
+ if (!multiple && !selectedSet) {
+ // nothing was selected, we have to insert the undefined item
+ optionGroups[''].unshift({id:'?', label:'', selected:true});
+ }
+
+ // Now we need to update the list of DOM nodes to match the optionGroups we computed above
+ for (groupIndex = 0, groupLength = optionGroupNames.length;
+ groupIndex < groupLength;
+ groupIndex++) {
+ // current option group name or '' if no group
+ optionGroupName = optionGroupNames[groupIndex];
+
+ // list of options for that group. (first item has the parent)
+ optionGroup = optionGroups[optionGroupName];
+
+ if (optionGroupsCache.length <= groupIndex) {
+ // we need to grow the optionGroups
+ optionGroupsCache.push(
+ existingOptions = [existingParent = {
+ element: optGroupTemplate.clone().attr('label', optionGroupName),
+ label: optionGroup.label
+ }]
+ );
+ selectElement.append(existingParent.element);
+ } else {
+ existingOptions = optionGroupsCache[groupIndex];
+ existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
+
+ // update the OPTGROUP label if not the same.
+ if (existingParent.label != optionGroupName) {
+ existingParent.element.attr('label', existingParent.label = optionGroupName);
+ }
+ }
+
+ lastElement = null; // start at the begining
+ for(index = 0, length = optionGroup.length; index < length; index++) {
+ option = optionGroup[index];
+ if ((existingOption = existingOptions[index+1])) {
+ // reuse elements
+ lastElement = existingOption.element;
+ if (existingOption.label !== option.label) {
+ lastElement.text(existingOption.label = option.label);
+ }
+ if (existingOption.id !== option.id) {
+ lastElement.val(existingOption.id = option.id);
+ }
+ if (existingOption.selected !== option.selected) {
+ lastElement.prop('selected', (existingOption.selected = option.selected));
+ }
+ } else {
+ // grow elements
+ // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
+ // in this version of jQuery on some browser the .text() returns a string
+ // rather then the element.
+ (element = optionTemplate.clone())
+ .val(option.id)
+ .attr('selected', option.selected)
+ .text(option.label);
+ existingOptions.push(existingOption = {
+ element: element,
+ label: option.label,
+ id: option.id,
+ selected: option.selected
+ });
+ if (lastElement) {
+ lastElement.after(element);
+ } else {
+ existingParent.element.append(element);
+ }
+ lastElement = element;
+ }
+ }
+ // remove any excessive OPTIONs in a group
+ index++; // increment since the existingOptions[0] is parent element not OPTION
+ while(existingOptions.length > index) {
+ existingOptions.pop().element.remove();
+ }
+ }
+ // remove any excessive OPTGROUPs from select
+ while(optionGroupsCache.length > groupIndex) {
+ optionGroupsCache.pop()[0].element.remove();
+ }
+ };
+ }
+ });
+});