'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
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'); });
*/ /** * @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
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"); }); }); /** * @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
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"); }); }); /** * @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
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(', '); } }; }); /** * @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
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')); /** * @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
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')); /** * @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
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('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
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, 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
text: Required!
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'); });
*/ 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'); }); } }