Skip to content

Commit 0622f3a

Browse files
matskomhevery
authored andcommitted
fix(forms): ensure models are validated when validator attributes change
1 parent 1954e9e commit 0622f3a

File tree

5 files changed

+206
-28
lines changed

5 files changed

+206
-28
lines changed

lib/directive/ng_model.dart

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class NgModel extends NgControl implements NgAttachAware {
3131
String _exp;
3232
final _validators = <NgValidator>[];
3333
bool _alwaysProcessViewValue;
34+
bool _toBeValidated = false;
3435

3536
NgModelConverter _converter;
3637
Watch _removeWatch;
@@ -89,6 +90,16 @@ class NgModel extends NgControl implements NgAttachAware {
8990
addInfoState(this, NgControl.NG_DIRTY);
9091
}
9192

93+
void validateLater() {
94+
if (_toBeValidated) return;
95+
_toBeValidated = true;
96+
_scope.rootScope.runAsync(() {
97+
if (_toBeValidated) {
98+
validate();
99+
}
100+
});
101+
}
102+
92103
get converter => _converter;
93104
set converter(NgModelConverter c) {
94105
_converter = c;
@@ -136,7 +147,6 @@ class NgModel extends NgControl implements NgAttachAware {
136147
_scope.rootScope.runAsync(() {
137148
_modelValue = boundExpression();
138149
_originalValue = modelValue;
139-
validate();
140150
processViewValue(_modelValue);
141151
});
142152
}
@@ -184,13 +194,15 @@ class NgModel extends NgControl implements NgAttachAware {
184194
* Executes a validation on the form against each of the validation present on the model.
185195
*/
186196
void validate() {
197+
_toBeValidated = false;
187198
if (validators.isNotEmpty) {
188199
validators.forEach((validator) {
189200
validator.isValid(modelValue) == false
190201
? this.addError(validator.name)
191202
: this.removeError(validator.name);
192203
});
193204
}
205+
194206
invalid
195207
? addInfo(NgControl.NG_INVALID)
196208
: removeInfo(NgControl.NG_INVALID);
@@ -201,15 +213,15 @@ class NgModel extends NgControl implements NgAttachAware {
201213
*/
202214
void addValidator(NgValidator v) {
203215
validators.add(v);
204-
validate();
216+
validateLater();
205217
}
206218

207219
/**
208220
* De-registers a validator from the model.
209221
*/
210222
void removeValidator(NgValidator v) {
211223
validators.remove(v);
212-
validate();
224+
validateLater();
213225
}
214226
}
215227

lib/directive/ng_model_validators.dart

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ abstract class NgValidator {
1414
selector: '[ng-model][ng-required]',
1515
map: const {'ng-required': '=>required'})
1616
class NgModelRequiredValidator implements NgValidator {
17-
bool _required = true;
1817

1918
final String name = 'ng-required';
19+
bool _required = true;
20+
final NgModel _ngModel;
2021

21-
NgModelRequiredValidator(NgModel ngModel) {
22-
ngModel.addValidator(this);
22+
NgModelRequiredValidator(NgModel this._ngModel) {
23+
_ngModel.addValidator(this);
2324
}
2425

2526
bool isValid(modelValue) {
@@ -35,6 +36,7 @@ class NgModelRequiredValidator implements NgValidator {
3536

3637
set required(value) {
3738
_required = value == null ? false : value;
39+
_ngModel.validateLater();
3840
}
3941
}
4042

@@ -81,6 +83,7 @@ class NgModelEmailValidator implements NgValidator {
8183
@NgDirective(selector: 'input[type=number][ng-model]')
8284
@NgDirective(selector: 'input[type=range][ng-model]')
8385
class NgModelNumberValidator implements NgValidator {
86+
8487
final String name = 'ng-number';
8588

8689
NgModelNumberValidator(NgModel ngModel) {
@@ -115,11 +118,12 @@ class NgModelNumberValidator implements NgValidator {
115118
map: const {'ng-max': '=>max'})
116119
class NgModelMaxNumberValidator implements NgValidator {
117120

118-
double _max;
119121
final String name = 'ng-max';
122+
double _max;
123+
final NgModel _ngModel;
120124

121-
NgModelMaxNumberValidator(NgModel ngModel) {
122-
ngModel.addValidator(this);
125+
NgModelMaxNumberValidator(NgModel this._ngModel) {
126+
_ngModel.addValidator(this);
123127
}
124128

125129
@NgAttr('max')
@@ -128,6 +132,7 @@ class NgModelMaxNumberValidator implements NgValidator {
128132
try {
129133
num parsedValue = double.parse(value);
130134
_max = parsedValue.isNaN ? _max : parsedValue;
135+
_ngModel.validateLater();
131136
} catch(e) {};
132137
}
133138

@@ -161,11 +166,12 @@ class NgModelMaxNumberValidator implements NgValidator {
161166
map: const {'ng-min': '=>min'})
162167
class NgModelMinNumberValidator implements NgValidator {
163168

164-
double _min;
165169
final String name = 'ng-min';
170+
double _min;
171+
final NgModel _ngModel;
166172

167-
NgModelMinNumberValidator(NgModel ngModel) {
168-
ngModel.addValidator(this);
173+
NgModelMinNumberValidator(NgModel this._ngModel) {
174+
_ngModel.addValidator(this);
169175
}
170176

171177
@NgAttr('min')
@@ -174,6 +180,7 @@ class NgModelMinNumberValidator implements NgValidator {
174180
try {
175181
num parsedValue = double.parse(value);
176182
_min = parsedValue.isNaN ? _min : parsedValue;
183+
_ngModel.validateLater();
177184
} catch(e) {};
178185
}
179186

@@ -203,12 +210,13 @@ class NgModelMinNumberValidator implements NgValidator {
203210
selector: '[ng-model][ng-pattern]',
204211
map: const {'ng-pattern': '=>pattern'})
205212
class NgModelPatternValidator implements NgValidator {
206-
RegExp _pattern;
207213

208214
final String name = 'ng-pattern';
215+
RegExp _pattern;
216+
final NgModel _ngModel;
209217

210-
NgModelPatternValidator(NgModel ngModel) {
211-
ngModel.addValidator(this);
218+
NgModelPatternValidator(NgModel this._ngModel) {
219+
_ngModel.addValidator(this);
212220
}
213221

214222
bool isValid(modelValue) {
@@ -218,8 +226,10 @@ class NgModelPatternValidator implements NgValidator {
218226
}
219227

220228
@NgAttr('pattern')
221-
set pattern(val) =>
222-
_pattern = val != null && val.length > 0 ? new RegExp(val) : null;
229+
void set pattern(val) {
230+
_pattern = val != null && val.length > 0 ? new RegExp(val) : null;
231+
_ngModel.validateLater();
232+
}
223233
}
224234

225235
/**
@@ -232,12 +242,13 @@ class NgModelPatternValidator implements NgValidator {
232242
selector: '[ng-model][ng-minlength]',
233243
map: const {'ng-minlength': '=>minlength'})
234244
class NgModelMinLengthValidator implements NgValidator {
235-
int _minlength;
236245

237246
final String name = 'ng-minlength';
247+
int _minlength;
248+
final NgModel _ngModel;
238249

239-
NgModelMinLengthValidator(NgModel ngModel) {
240-
ngModel.addValidator(this);
250+
NgModelMinLengthValidator(NgModel this._ngModel) {
251+
_ngModel.addValidator(this);
241252
}
242253

243254
bool isValid(modelValue) {
@@ -247,8 +258,10 @@ class NgModelMinLengthValidator implements NgValidator {
247258
}
248259

249260
@NgAttr('minlength')
250-
set minlength(value) =>
251-
_minlength = value == null ? 0 : int.parse(value.toString());
261+
void set minlength(value) {
262+
_minlength = value == null ? 0 : int.parse(value.toString());
263+
_ngModel.validateLater();
264+
}
252265
}
253266

254267
/**
@@ -261,18 +274,21 @@ class NgModelMinLengthValidator implements NgValidator {
261274
selector: '[ng-model][ng-maxlength]',
262275
map: const {'ng-maxlength': '=>maxlength'})
263276
class NgModelMaxLengthValidator implements NgValidator {
264-
int _maxlength = 0;
265277

266278
final String name = 'ng-maxlength';
279+
int _maxlength = 0;
280+
final NgModel _ngModel;
267281

268-
NgModelMaxLengthValidator(NgModel ngModel) {
269-
ngModel.addValidator(this);
282+
NgModelMaxLengthValidator(NgModel this._ngModel) {
283+
_ngModel.addValidator(this);
270284
}
271285

272286
bool isValid(modelValue) =>
273287
_maxlength == 0 || (modelValue == null ? 0 : modelValue.length) <= _maxlength;
274288

275289
@NgAttr('maxlength')
276-
set maxlength(value) =>
277-
_maxlength = value == null ? 0 : int.parse(value.toString());
290+
void set maxlength(value) {
291+
_maxlength = value == null ? 0 : int.parse(value.toString());
292+
_ngModel.validateLater();
293+
}
278294
}

test/directive/ng_form_spec.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ void main() {
514514
_.compile('<form name="superForm">' +
515515
' <input type="text" ng-model="myModel" probe="i" required />' +
516516
'</form>');
517+
scope.apply();
517518

518519
NgForm form = _.rootScope.context['superForm'];
519520
Probe probe = _.rootScope.context['i'];
@@ -600,6 +601,29 @@ void main() {
600601
expect(form.classes.contains('ng-required-invalid')).toBe(false);
601602
});
602603

604+
it('should re-validate itself when validators are toggled on and off',
605+
(TestBed _, Scope scope) {
606+
607+
scope.context['required'] = true;
608+
_.compile('<form name="myForm">' +
609+
'<input type="text" ng-model="model" ng-required="required" probe="i">' +
610+
'</form>');
611+
scope.apply();
612+
613+
var form = scope.context['myForm'];
614+
var model = scope.context['i'].directive(NgModel);
615+
616+
expect(form.invalid).toBe(true);
617+
expect(model.invalid).toBe(true);
618+
619+
scope.context['required'] = false;
620+
scope.apply();
621+
622+
expect(form.valid).toBe(true);
623+
expect(model.valid).toBe(true);
624+
});
625+
626+
603627
describe('custom validators', () {
604628
beforeEachModule((Module module) {
605629
module.type(MyCustomFormValidator);

test/directive/ng_model_spec.dart

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ void main() {
1010
beforeEachModule((Module module) {
1111
module
1212
..type(ControllerWithNoLove)
13-
..type(MyCustomInputValidator);
13+
..type(MyCustomInputValidator)
14+
..type(CountingValidator);
1415
});
1516

1617
beforeEach((TestBed tb) => _ = tb);
@@ -1128,6 +1129,7 @@ void main() {
11281129

11291130
it('should happen automatically upon user input via the onInput event', () {
11301131
_.compile('<input type="text" ng-model="model" probe="i" required>');
1132+
_.rootScope.apply();
11311133

11321134
Probe probe = _.rootScope.context['i'];
11331135
var model = probe.directive(NgModel);
@@ -1348,6 +1350,36 @@ void main() {
13481350
expect(input.classes.contains('custom-valid')).toBe(true);
13491351
expect(input.classes.contains('custom-invalid')).toBe(false);
13501352
});
1353+
1354+
it('should only validate twice during compilation and once upon scope digest',
1355+
(TestBed _, Scope scope) {
1356+
1357+
scope.context['required'] = true;
1358+
_.compile('<input type="text" '
1359+
'ng-model="model" '
1360+
'ng-required="required" '
1361+
'ng-pattern="pattern" '
1362+
'counting-validator '
1363+
'probe="i">');
1364+
1365+
scope.context['pattern'] = '^[aeiou]+\$';
1366+
scope.context['required'] = true;
1367+
1368+
scope.apply();
1369+
1370+
var model = scope.context['i'].directive(NgModel);
1371+
var counter = model.validators.firstWhere((validator) => validator.name == 'counting');
1372+
1373+
expect(counter.count).toBe(2); //one for ngModel and one for all the other ones
1374+
expect(model.invalid).toBe(true);
1375+
1376+
counter.count = 0;
1377+
scope.context['pattern'] = '';
1378+
scope.context['required'] = false;
1379+
scope.apply();
1380+
1381+
expect(counter.count).toBe(1);
1382+
});
13511383
});
13521384

13531385
describe('converters', () {
@@ -1496,3 +1528,20 @@ class MyCustomInputValidator extends NgValidator {
14961528
return name != null && name == 'yes';
14971529
}
14981530
}
1531+
1532+
@NgDirective(
1533+
selector: '[counting-validator]')
1534+
class CountingValidator extends NgValidator {
1535+
1536+
final String name = 'counting';
1537+
int count = 0;
1538+
1539+
CountingValidator(NgModel ngModel) {
1540+
ngModel.addValidator(this);
1541+
}
1542+
1543+
bool isValid(String modelValue) {
1544+
count++;
1545+
return true;
1546+
}
1547+
}

0 commit comments

Comments
 (0)