diff --git a/lib/directive/module.dart b/lib/directive/module.dart index 6bd55d707..ef8599276 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -82,6 +82,8 @@ class NgDirectiveModule extends Module { value(NgModelUrlValidator, null); value(NgModelEmailValidator, null); value(NgModelNumberValidator, null); + value(NgModelMaxNumberValidator, null); + value(NgModelMinNumberValidator, null); value(NgModelPatternValidator, null); value(NgModelMinLengthValidator, null); value(NgModelMaxLengthValidator, null); diff --git a/lib/directive/ng_model_validators.dart b/lib/directive/ng_model_validators.dart index c816efccf..9e4947263 100644 --- a/lib/directive/ng_model_validators.dart +++ b/lib/directive/ng_model_validators.dart @@ -79,6 +79,7 @@ class NgModelEmailValidator implements NgValidatable { * Validates the model to see if its contents match a valid number. */ @NgDirective(selector: 'input[type=number][ng-model]') +@NgDirective(selector: 'input[type=range][ng-model]') class NgModelNumberValidator implements NgValidatable { String get name => 'number'; @@ -101,6 +102,98 @@ class NgModelNumberValidator implements NgValidatable { } } +/** + * Validates the model to see if the numeric value than or equal to the max value. + */ +@NgDirective(selector: 'input[type=number][ng-model][max]') +@NgDirective(selector: 'input[type=range][ng-model][max]') +@NgDirective( + selector: 'input[type=number][ng-model][ng-max]', + map: const {'ng-max': '=>max'}) +@NgDirective( + selector: 'input[type=range][ng-model][ng-max]', + map: const {'ng-max': '=>max'}) +class NgModelMaxNumberValidator implements NgValidatable { + + double _max; + String get name => 'max'; + + NgModelMaxNumberValidator(NgModel ngModel) { + ngModel.addValidator(this); + } + + @NgAttr('max') + get max => _max; + set max(value) { + try { + num parsedValue = double.parse(value); + _max = parsedValue.isNaN ? _max : parsedValue; + } catch(e) {}; + } + + bool isValid(value) { + if (value == null || max == null) return true; + + try { + num parsedValue = double.parse(value.toString()); + if (!parsedValue.isNaN) { + return parsedValue <= max; + } + } catch(exception, stackTrace) {} + + //this validator doesn't care if the type conversation fails or the value + //is not a number (NaN) because NgModelNumberValidator will handle the + //number-based validation either way. + return true; + } +} + +/** + * Validates the model to see if the numeric value is greater than or equal to the min value. + */ +@NgDirective(selector: 'input[type=number][ng-model][min]') +@NgDirective(selector: 'input[type=range][ng-model][min]') +@NgDirective( + selector: 'input[type=number][ng-model][ng-min]', + map: const {'ng-min': '=>min'}) +@NgDirective( + selector: 'input[type=range][ng-model][ng-min]', + map: const {'ng-min': '=>min'}) +class NgModelMinNumberValidator implements NgValidatable { + + double _min; + String get name => 'min'; + + NgModelMinNumberValidator(NgModel ngModel) { + ngModel.addValidator(this); + } + + @NgAttr('min') + get min => _min; + set min(value) { + try { + num parsedValue = double.parse(value); + _min = parsedValue.isNaN ? _min : parsedValue; + } catch(e) {}; + } + + bool isValid(value) { + if (value == null || min == null) return true; + + try { + num parsedValue = double.parse(value.toString()); + if (!parsedValue.isNaN) { + return parsedValue >= min; + } + } catch(exception, stackTrace) {} + + //this validator doesn't care if the type conversation fails or the value + //is not a number (NaN) because NgModelNumberValidator will handle the + //number-based validation either way. + return true; + } +} + /** * Validates the model to see if its contents match the given pattern present on either the * HTML pattern or ng-pattern attributes present on the input element. diff --git a/test/directive/ng_model_validators_spec.dart b/test/directive/ng_model_validators_spec.dart index a549d0d6b..c745b0f33 100644 --- a/test/directive/ng_model_validators_spec.dart +++ b/test/directive/ng_model_validators_spec.dart @@ -3,6 +3,17 @@ library ng_model_validators; import '../_specs.dart'; void main() { + they(should, tokens, callback, [exclusive=false]) { + tokens.forEach((token) { + describe(token, () { + (exclusive ? iit : it)(should, () => callback(token)); + }); + }); + } + + tthey(should, tokens, callback) => + they(should, tokens, callback, true); + describe('ngModel validators', () { TestBed _; @@ -128,9 +139,12 @@ void main() { })); }); - describe('[type="number"]', () { - it('should validate the input field given a valid or invalid number', inject((RootScope scope) { - _.compile(''); + describe('[type="number|range"]', () { + they('should validate the input field given a valid or invalid number', + ['range', 'number'], + (type) { + + _.compile(''); Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); @@ -162,7 +176,165 @@ void main() { model.validate(); expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); - })); + }); + + they('should perform a max number validation if a max attribute value is present', + ['range', 'number'], + (type) { + + _.compile(''); + Probe probe = _.rootScope.context['i']; + var model = probe.directive(NgModel); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "8"; + }); + + model.validate(); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + expect(model.hasError('max')).toBe(false); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "99"; + }); + + model.validate(); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + expect(model.hasError('max')).toBe(true); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "a"; + }); + + model.validate(); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + expect(model.hasError('max')).toBe(false); + expect(model.hasError('number')).toBe(true); + }); + + they('should perform a max number validation if a ng-max attribute value is present and/or changed', + ['range', 'number'], + (type) { + + _.compile(''); + Probe probe = _.rootScope.context['i']; + var model = probe.directive(NgModel); + + //should be valid even when no number is present + model.validate(); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + expect(model.hasError('max')).toBe(false); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "20"; + }); + + model.validate(); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + expect(model.hasError('max')).toBe(false); + + _.rootScope.apply(() { + _.rootScope.context['maxVal'] = "19"; + }); + + model.validate(); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + expect(model.hasError('max')).toBe(true); + + _.rootScope.apply(() { + _.rootScope.context['maxVal'] = "22"; + }); + + model.validate(); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + expect(model.hasError('max')).toBe(false); + }); + + they('should perform a min number validation if a min attribute value is present', + ['range', 'number'], + (type) { + + _.compile(''); + Probe probe = _.rootScope.context['i']; + var model = probe.directive(NgModel); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "8"; + }); + + model.validate(); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + expect(model.hasError('min')).toBe(false); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "-20"; + }); + + model.validate(); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + expect(model.hasError('min')).toBe(true); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "x"; + }); + + model.validate(); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + expect(model.hasError('min')).toBe(false); + expect(model.hasError('number')).toBe(true); + }); + + they('should perform a min number validation if a ng-min attribute value is present and/or changed', + ['range', 'number'], + (type) { + + _.compile(''); + Probe probe = _.rootScope.context['i']; + var model = probe.directive(NgModel); + + //should be valid even when no number is present + model.validate(); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + expect(model.hasError('min')).toBe(false); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "5"; + }); + + model.validate(); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + expect(model.hasError('min')).toBe(false); + + _.rootScope.apply(() { + _.rootScope.context['minVal'] = "5.5"; + }); + + model.validate(); + expect(model.valid).toEqual(false); + expect(model.invalid).toEqual(true); + expect(model.hasError('min')).toBe(true); + + _.rootScope.apply(() { + _.rootScope.context['val'] = "5.6"; + }); + + model.validate(); + expect(model.valid).toEqual(true); + expect(model.invalid).toEqual(false); + expect(model.hasError('min')).toBe(false); + }); }); describe('pattern', () {