From 0622f3a969b99b06c5f07da10ceb756b720a3331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Tue, 18 Mar 2014 17:45:30 -0400 Subject: [PATCH] fix(forms): ensure models are validated when validator attributes change --- lib/directive/ng_model.dart | 18 ++++- lib/directive/ng_model_validators.dart | 64 ++++++++++------ test/directive/ng_form_spec.dart | 24 ++++++ test/directive/ng_model_spec.dart | 51 ++++++++++++- test/directive/ng_model_validators_spec.dart | 77 ++++++++++++++++++++ 5 files changed, 206 insertions(+), 28 deletions(-) diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart index b3a894e08..14d1e98d8 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -31,6 +31,7 @@ class NgModel extends NgControl implements NgAttachAware { String _exp; final _validators = []; bool _alwaysProcessViewValue; + bool _toBeValidated = false; NgModelConverter _converter; Watch _removeWatch; @@ -89,6 +90,16 @@ class NgModel extends NgControl implements NgAttachAware { addInfoState(this, NgControl.NG_DIRTY); } + void validateLater() { + if (_toBeValidated) return; + _toBeValidated = true; + _scope.rootScope.runAsync(() { + if (_toBeValidated) { + validate(); + } + }); + } + get converter => _converter; set converter(NgModelConverter c) { _converter = c; @@ -136,7 +147,6 @@ class NgModel extends NgControl implements NgAttachAware { _scope.rootScope.runAsync(() { _modelValue = boundExpression(); _originalValue = modelValue; - validate(); processViewValue(_modelValue); }); } @@ -184,6 +194,7 @@ class NgModel extends NgControl implements NgAttachAware { * Executes a validation on the form against each of the validation present on the model. */ void validate() { + _toBeValidated = false; if (validators.isNotEmpty) { validators.forEach((validator) { validator.isValid(modelValue) == false @@ -191,6 +202,7 @@ class NgModel extends NgControl implements NgAttachAware { : this.removeError(validator.name); }); } + invalid ? addInfo(NgControl.NG_INVALID) : removeInfo(NgControl.NG_INVALID); @@ -201,7 +213,7 @@ class NgModel extends NgControl implements NgAttachAware { */ void addValidator(NgValidator v) { validators.add(v); - validate(); + validateLater(); } /** @@ -209,7 +221,7 @@ class NgModel extends NgControl implements NgAttachAware { */ void removeValidator(NgValidator v) { validators.remove(v); - validate(); + validateLater(); } } diff --git a/lib/directive/ng_model_validators.dart b/lib/directive/ng_model_validators.dart index 9c53e9bb7..797eccee2 100644 --- a/lib/directive/ng_model_validators.dart +++ b/lib/directive/ng_model_validators.dart @@ -14,12 +14,13 @@ abstract class NgValidator { selector: '[ng-model][ng-required]', map: const {'ng-required': '=>required'}) class NgModelRequiredValidator implements NgValidator { - bool _required = true; final String name = 'ng-required'; + bool _required = true; + final NgModel _ngModel; - NgModelRequiredValidator(NgModel ngModel) { - ngModel.addValidator(this); + NgModelRequiredValidator(NgModel this._ngModel) { + _ngModel.addValidator(this); } bool isValid(modelValue) { @@ -35,6 +36,7 @@ class NgModelRequiredValidator implements NgValidator { set required(value) { _required = value == null ? false : value; + _ngModel.validateLater(); } } @@ -81,6 +83,7 @@ class NgModelEmailValidator implements NgValidator { @NgDirective(selector: 'input[type=number][ng-model]') @NgDirective(selector: 'input[type=range][ng-model]') class NgModelNumberValidator implements NgValidator { + final String name = 'ng-number'; NgModelNumberValidator(NgModel ngModel) { @@ -115,11 +118,12 @@ class NgModelNumberValidator implements NgValidator { map: const {'ng-max': '=>max'}) class NgModelMaxNumberValidator implements NgValidator { - double _max; final String name = 'ng-max'; + double _max; + final NgModel _ngModel; - NgModelMaxNumberValidator(NgModel ngModel) { - ngModel.addValidator(this); + NgModelMaxNumberValidator(NgModel this._ngModel) { + _ngModel.addValidator(this); } @NgAttr('max') @@ -128,6 +132,7 @@ class NgModelMaxNumberValidator implements NgValidator { try { num parsedValue = double.parse(value); _max = parsedValue.isNaN ? _max : parsedValue; + _ngModel.validateLater(); } catch(e) {}; } @@ -161,11 +166,12 @@ class NgModelMaxNumberValidator implements NgValidator { map: const {'ng-min': '=>min'}) class NgModelMinNumberValidator implements NgValidator { - double _min; final String name = 'ng-min'; + double _min; + final NgModel _ngModel; - NgModelMinNumberValidator(NgModel ngModel) { - ngModel.addValidator(this); + NgModelMinNumberValidator(NgModel this._ngModel) { + _ngModel.addValidator(this); } @NgAttr('min') @@ -174,6 +180,7 @@ class NgModelMinNumberValidator implements NgValidator { try { num parsedValue = double.parse(value); _min = parsedValue.isNaN ? _min : parsedValue; + _ngModel.validateLater(); } catch(e) {}; } @@ -203,12 +210,13 @@ class NgModelMinNumberValidator implements NgValidator { selector: '[ng-model][ng-pattern]', map: const {'ng-pattern': '=>pattern'}) class NgModelPatternValidator implements NgValidator { - RegExp _pattern; final String name = 'ng-pattern'; + RegExp _pattern; + final NgModel _ngModel; - NgModelPatternValidator(NgModel ngModel) { - ngModel.addValidator(this); + NgModelPatternValidator(NgModel this._ngModel) { + _ngModel.addValidator(this); } bool isValid(modelValue) { @@ -218,8 +226,10 @@ class NgModelPatternValidator implements NgValidator { } @NgAttr('pattern') - set pattern(val) => - _pattern = val != null && val.length > 0 ? new RegExp(val) : null; + void set pattern(val) { + _pattern = val != null && val.length > 0 ? new RegExp(val) : null; + _ngModel.validateLater(); + } } /** @@ -232,12 +242,13 @@ class NgModelPatternValidator implements NgValidator { selector: '[ng-model][ng-minlength]', map: const {'ng-minlength': '=>minlength'}) class NgModelMinLengthValidator implements NgValidator { - int _minlength; final String name = 'ng-minlength'; + int _minlength; + final NgModel _ngModel; - NgModelMinLengthValidator(NgModel ngModel) { - ngModel.addValidator(this); + NgModelMinLengthValidator(NgModel this._ngModel) { + _ngModel.addValidator(this); } bool isValid(modelValue) { @@ -247,8 +258,10 @@ class NgModelMinLengthValidator implements NgValidator { } @NgAttr('minlength') - set minlength(value) => - _minlength = value == null ? 0 : int.parse(value.toString()); + void set minlength(value) { + _minlength = value == null ? 0 : int.parse(value.toString()); + _ngModel.validateLater(); + } } /** @@ -261,18 +274,21 @@ class NgModelMinLengthValidator implements NgValidator { selector: '[ng-model][ng-maxlength]', map: const {'ng-maxlength': '=>maxlength'}) class NgModelMaxLengthValidator implements NgValidator { - int _maxlength = 0; final String name = 'ng-maxlength'; + int _maxlength = 0; + final NgModel _ngModel; - NgModelMaxLengthValidator(NgModel ngModel) { - ngModel.addValidator(this); + NgModelMaxLengthValidator(NgModel this._ngModel) { + _ngModel.addValidator(this); } bool isValid(modelValue) => _maxlength == 0 || (modelValue == null ? 0 : modelValue.length) <= _maxlength; @NgAttr('maxlength') - set maxlength(value) => - _maxlength = value == null ? 0 : int.parse(value.toString()); + void set maxlength(value) { + _maxlength = value == null ? 0 : int.parse(value.toString()); + _ngModel.validateLater(); + } } diff --git a/test/directive/ng_form_spec.dart b/test/directive/ng_form_spec.dart index 30d159443..16d78ef20 100644 --- a/test/directive/ng_form_spec.dart +++ b/test/directive/ng_form_spec.dart @@ -514,6 +514,7 @@ void main() { _.compile('
' + ' ' + '
'); + scope.apply(); NgForm form = _.rootScope.context['superForm']; Probe probe = _.rootScope.context['i']; @@ -600,6 +601,29 @@ void main() { expect(form.classes.contains('ng-required-invalid')).toBe(false); }); + it('should re-validate itself when validators are toggled on and off', + (TestBed _, Scope scope) { + + scope.context['required'] = true; + _.compile('
' + + '' + + '
'); + scope.apply(); + + var form = scope.context['myForm']; + var model = scope.context['i'].directive(NgModel); + + expect(form.invalid).toBe(true); + expect(model.invalid).toBe(true); + + scope.context['required'] = false; + scope.apply(); + + expect(form.valid).toBe(true); + expect(model.valid).toBe(true); + }); + + describe('custom validators', () { beforeEachModule((Module module) { module.type(MyCustomFormValidator); diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart index 3a2b68c42..10b22f58c 100644 --- a/test/directive/ng_model_spec.dart +++ b/test/directive/ng_model_spec.dart @@ -10,7 +10,8 @@ void main() { beforeEachModule((Module module) { module ..type(ControllerWithNoLove) - ..type(MyCustomInputValidator); + ..type(MyCustomInputValidator) + ..type(CountingValidator); }); beforeEach((TestBed tb) => _ = tb); @@ -1128,6 +1129,7 @@ void main() { it('should happen automatically upon user input via the onInput event', () { _.compile(''); + _.rootScope.apply(); Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); @@ -1348,6 +1350,36 @@ void main() { expect(input.classes.contains('custom-valid')).toBe(true); expect(input.classes.contains('custom-invalid')).toBe(false); }); + + it('should only validate twice during compilation and once upon scope digest', + (TestBed _, Scope scope) { + + scope.context['required'] = true; + _.compile(''); + + scope.context['pattern'] = '^[aeiou]+\$'; + scope.context['required'] = true; + + scope.apply(); + + var model = scope.context['i'].directive(NgModel); + var counter = model.validators.firstWhere((validator) => validator.name == 'counting'); + + expect(counter.count).toBe(2); //one for ngModel and one for all the other ones + expect(model.invalid).toBe(true); + + counter.count = 0; + scope.context['pattern'] = ''; + scope.context['required'] = false; + scope.apply(); + + expect(counter.count).toBe(1); + }); }); describe('converters', () { @@ -1496,3 +1528,20 @@ class MyCustomInputValidator extends NgValidator { return name != null && name == 'yes'; } } + +@NgDirective( + selector: '[counting-validator]') +class CountingValidator extends NgValidator { + + final String name = 'counting'; + int count = 0; + + CountingValidator(NgModel ngModel) { + ngModel.addValidator(this); + } + + bool isValid(String modelValue) { + count++; + return true; + } +} diff --git a/test/directive/ng_model_validators_spec.dart b/test/directive/ng_model_validators_spec.dart index 760692ad1..6ec7fda47 100644 --- a/test/directive/ng_model_validators_spec.dart +++ b/test/directive/ng_model_validators_spec.dart @@ -43,6 +43,8 @@ void main() { Probe probe = _.rootScope.context['i']; var model = probe.directive(NgModel); + _.rootScope.apply(); + expect(model.valid).toEqual(false); expect(model.invalid).toEqual(true); @@ -519,5 +521,80 @@ void main() { expect(model.invalid).toEqual(true); }); }); + + describe('when toggled it should properly validate', () { + var build, input, scope, model; + beforeEach(() { + scope = _.rootScope; + build = (attr, type) { + input = _.compile(''); + model = scope.context['p'].directive(NgModel); + }; + }); + + it('ng-required', () { + var input = build('ng-required', 'text'); + scope.apply(() { + scope.context['attr'] = true; + scope.context['value'] = ''; + }); + + expect(model.valid).toBe(false); + + scope.apply(() { + scope.context['attr'] = false; + }); + + expect(model.valid).toBe(true); + }); + + it('ng-pattern', () { + var input = build('ng-pattern', 'text'); + scope.apply(() { + scope.context['attr'] = '^\d+\$'; + scope.context['value'] = 'abc'; + }); + + expect(model.valid).toBe(false); + + scope.apply(() { + scope.context['attr'] = null; + }); + + expect(model.valid).toBe(true); + }); + + it('ng-minlength', () { + var input = build('ng-minlength', 'text'); + scope.apply(() { + scope.context['attr'] = '10'; + scope.context['value'] = 'abc'; + }); + + expect(model.valid).toBe(false); + + scope.apply(() { + scope.context['attr'] = null; + }); + + expect(model.valid).toBe(true); + }); + + it('ng-minlength', () { + var input = build('ng-maxlength', 'text'); + scope.apply(() { + scope.context['attr'] = '3'; + scope.context['value'] = 'abcd'; + }); + + expect(model.valid).toBe(false); + + scope.apply(() { + scope.context['attr'] = null; + }); + + expect(model.valid).toBe(true); + }); + }); }); }