'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*$/; /** * @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 {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
Single word: Required! Single word only!
text = {{text}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
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'); });
*/ /** * @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 {number=} ng:minlength Sets `MINLENGTH` validation error key if the value is shorter than * minlength. * @param {number=} ng:maxlength Sets `MAXLENGTH` validation error key if the value is longer than * maxlength. * @param {string=} ng:pattern Sets `PATTERN` validation error key if the value does not match the * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for * patterns defined as scope expressions. * * @example
Email: Required! Not valid email!
text = {{text}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
myForm.$error.EMAIL = {{!!myForm.$error.EMAIL}}
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'); });
*/ 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"); }); }); /** * @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 {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
URL: Required! Not valid url!
text = {{text}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
myForm.$error.url = {{!!myForm.$error.url}}
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'); });
*/ 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"); }); }); /** * @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. * @param {string=} ng:change Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
List: Required!
names = {{names}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
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'); });
*/ 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(', '); } }; }); /** * @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 {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
Number: Required! Not valid number!
value = {{value}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
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'); });
*/ 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
Integer: Required! Not valid integer!
value = {{value}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
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'); });
*/ angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER')); /** * @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=} ng:true-value The value to which the expression should be set when selected. * @param {string=} ng:false-value The value to which the expression should be set when not selected. * @param {string=} ng:change Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
Value1:
Value2:
value1 = {{value1}}
value2 = {{value2}}
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'); });
*/ angularInputType('checkbox', function(inputElement) { var widget = this, 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 = this.$modelValue === trueValue; }; widget.$parseView = function() { widget.$modelValue = widget.$viewValue ? trueValue : falseValue; }; }); /** * @ngdoc inputType * @name angular.inputType.radio * * @description * HTML radio button. * * @param {string} ng:model Assignable angular expression to data-bind to. * @param {string} value The value to which the expression should be set when selected. * @param {string=} name Property name of the form under which the widgets is published. * @param {string=} ng:change Angular expression to be executed when input changes due to user * interaction with the input element. * * @example
Red
Green
Blue
color = {{color}}
it('should change state', function() { expect(binding('color')).toEqual('blue'); input('color').select('red'); expect(binding('color')).toEqual('red'); });
*/ angularInputType('radio', function(inputElement) { var widget = this; //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', widget.$value); } }); }); widget.$render = function() { inputElement[0].checked = isDefined(widget.$value) && (widget.$value == widget.$viewValue); }; if (inputElement[0].checked) { widget.$viewValue = widget.$value; } }); function numericRegexpInputType(regexp, error) { return ['$element', 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,password"); /** * @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 provide a shorthand for declaring new * inputs. This is a sharthand for text-box based inputs, and there is no need to go through the * full {@link angular.module.NG.$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 {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
User name: Required!
Last name: Too short! Too long!

user = {{user}}
myForm.userName.$valid = {{myForm.userName.$valid}}
myForm.userName.$error = {{myForm.userName.$error}}
myForm.lastName.$valid = {{myForm.lastName.$valid}}
myForm.userName.$error = {{myForm.lastName.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}
myForm.$error.MINLENGTH = {{!!myForm.$error.MINLENGTH}}
myForm.$error.MAXLENGTH = {{!!myForm.$error.MAXLENGTH}}
it('should initialize to model', function() { expect(binding('user')).toEqual('{\n \"last\":\"visitor",\n \"name\":\"guest\"}'); expect(binding('myForm.userName.$valid')).toEqual('true'); expect(binding('myForm.$valid')).toEqual('true'); }); it('should be invalid if empty when required', function() { input('user.name').enter(''); expect(binding('user')).toEqual('{\n \"last\":\"visitor",\n \"name\":\"\"}'); 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('{\n \"last\":\"",\n \"name\":\"guest\"}'); expect(binding('myForm.lastName.$valid')).toEqual('true'); expect(binding('myForm.$valid')).toEqual('true'); }); it('should be invalid if less than required min length', function() { input('user.last').enter('xx'); expect(binding('user')).toEqual('{\n \"last\":\"xx",\n \"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('{\n \"last\":\"some ridiculously long name",\n \"name\":\"guest\"}'); expect(binding('myForm.lastName.$valid')).toEqual('false'); expect(binding('myForm.lastName.$error')).toMatch(/MAXLENGTH/); expect(binding('myForm.$valid')).toEqual('false'); });
*/ angularWidget('input', function(inputElement){ this.directives(true); this.descend(true); var modelExp = inputElement.attr('ng:model'); return modelExp && ['$defer', '$formFactory', '$element', 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')), minlength = parseInt(inputElement.attr('ng:minlength'), 10), maxlength = parseInt(inputElement.attr('ng:maxlength'), 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(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. } } //TODO(misko): setting $inject is a hack !TypeController.$inject && (TypeController.$inject = ['$element']); widget = form.$createWidget({ scope: modelScope, model: modelExp, onChange: inputElement.attr('ng:change'), alias: inputElement.attr('name'), controller: TypeController, controllerArgs: {$element: inputElement} }); watchElementProperty(this, widget, 'value', inputElement); watchElementProperty(this, widget, 'required', inputElement); watchElementProperty(this, widget, 'readonly', inputElement); watchElementProperty(this, widget, 'disabled', inputElement); widget.$pristine = !(widget.$dirty = false); widget.$on('$validate', function() { var $viewValue = trim(widget.$viewValue), inValid = widget.$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(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); } }); } }); } }]; }); /** * @ngdoc widget * @name angular.widget.textarea * * @description * HTML textarea element widget with angular data-binding. The data-binding and validation * properties of this element are exactly the same as those of the * {@link angular.widget.input input element}. * * @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 {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. */ angularWidget('textarea', angularWidget('input')); function watchElementProperty(modelScope, widget, name, element) { var bindAttr = fromJson(element.attr('ng:bind-attr') || '{}'), match = /\s*{{(.*)}}\s*/.exec(bindAttr[name]), isBoolean = BOOLEAN_ATTR[name]; widget['$' + name] = isBoolean ? ( // 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]) : element.attr(name); if (bindAttr[name] && match) { modelScope.$watch(match[1], function(scope, value){ widget['$' + name] = isBoolean ? !!value : value; widget.$emit('$validate'); }); } }