Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit 419e918

Browse files
matskomhevery
authored andcommitted
fix(NgModel): ensure DOM value changes are only applied during scope.domWrite
1 parent bed9fe1 commit 419e918

File tree

2 files changed

+218
-22
lines changed

2 files changed

+218
-22
lines changed

lib/directive/ng_model.dart

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ class InputCheckboxDirective {
200200
InputCheckboxDirective(dom.Element this.inputElement, this.ngModel,
201201
this.scope, this.ngTrueValue, this.ngFalseValue) {
202202
ngModel.render = (value) {
203-
inputElement.checked = ngTrueValue.isValue(inputElement, value);
203+
scope.rootScope.domWrite(() {
204+
inputElement.checked = ngTrueValue.isValue(inputElement, value);
205+
});
204206
};
205207
inputElement.onChange.listen((value) {
206208
ngModel.viewValue = inputElement.checked
@@ -242,13 +244,15 @@ class InputTextLikeDirective {
242244

243245
InputTextLikeDirective(this.inputElement, this.ngModel, this.scope) {
244246
ngModel.render = (value) {
245-
if (value == null) value = '';
246-
247-
var currentValue = typedValue;
248-
if (value != currentValue && !(value is num && currentValue is num &&
249-
value.isNaN && currentValue.isNaN)) {
250-
typedValue = value;
251-
}
247+
scope.rootScope.domWrite(() {
248+
if (value == null) value = '';
249+
250+
var currentValue = typedValue;
251+
if (value != currentValue && !(value is num && currentValue is num &&
252+
value.isNaN && currentValue.isNaN)) {
253+
typedValue = value;
254+
}
255+
});
252256
};
253257
inputElement
254258
..onChange.listen(processValue)
@@ -309,10 +313,12 @@ class InputNumberLikeDirective {
309313

310314
InputNumberLikeDirective(dom.Element this.inputElement, this.ngModel, this.scope) {
311315
ngModel.render = (value) {
312-
if (value != typedValue
313-
&& (value == null || value is num && !value.isNaN)) {
314-
typedValue = value;
315-
}
316+
scope.rootScope.domWrite(() {
317+
if (value != typedValue
318+
&& (value == null || value is num && !value.isNaN)) {
319+
typedValue = value;
320+
}
321+
});
316322
};
317323
inputElement
318324
..onChange.listen(relaxFnArgs(processValue))
@@ -447,7 +453,9 @@ class InputRadioDirective {
447453
attrs["name"] = _uidCounter.next();
448454
}
449455
ngModel.render = (value) {
450-
radioButtonElement.checked = (value == ngValue.readValue(radioButtonElement));
456+
scope.rootScope.domWrite(() {
457+
radioButtonElement.checked = (value == ngValue.readValue(radioButtonElement));
458+
});
451459
};
452460
radioButtonElement.onClick.listen((_) {
453461
if (radioButtonElement.checked) {

test/directive/ng_model_spec.dart

Lines changed: 197 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ void main() {
9393
expect(ngModel.valid).toBe(false);
9494
}));
9595

96-
it('should write to input only if value is different',
96+
it('should write to input only if the value is different',
9797
inject((Injector i, AstParser parser, NgAnimate animate) {
9898

9999
var scope = _.rootScope;
@@ -111,20 +111,40 @@ void main() {
111111
..selectionStart = 1
112112
..selectionEnd = 2;
113113

114-
model.render('abc');
114+
scope.apply(() {
115+
scope.context['model'] = 'abc';
116+
});
115117

116118
expect(element.value).toEqual('abc');
117119
// No update. selectionStart/End is unchanged.
118120
expect(element.selectionStart).toEqual(1);
119121
expect(element.selectionEnd).toEqual(2);
120122

121-
model.render('xyz');
123+
scope.apply(() {
124+
scope.context['model'] = 'xyz';
125+
});
122126

123127
// Value updated. selectionStart/End changed.
124128
expect(element.value).toEqual('xyz');
125129
expect(element.selectionStart).toEqual(3);
126130
expect(element.selectionEnd).toEqual(3);
127131
}));
132+
133+
it('should only render the input value upon the next digest', inject((Scope scope) {
134+
_.compile('<input type="text" ng-model="model" probe="p">');
135+
Probe probe = _.rootScope.context['p'];
136+
var ngModel = probe.directive(NgModel);
137+
InputElement inputElement = probe.element;
138+
139+
ngModel.render('xyz');
140+
scope.context['model'] = 'xyz';
141+
142+
expect(inputElement.value).not.toEqual('xyz');
143+
144+
scope.apply();
145+
146+
expect(inputElement.value).toEqual('xyz');
147+
}));
128148
});
129149

130150
/* This function simulates typing the given text into the input
@@ -254,6 +274,22 @@ void main() {
254274
_.rootScope.apply('model = null');
255275
expect((_.rootElement as dom.InputElement).value).toEqual('');
256276
}));
277+
278+
it('should only render the input value upon the next digest', inject((Scope scope) {
279+
_.compile('<input type="number" ng-model="model" probe="p">');
280+
Probe probe = _.rootScope.context['p'];
281+
var ngModel = probe.directive(NgModel);
282+
InputElement inputElement = probe.element;
283+
284+
ngModel.render(123);
285+
scope.context['model'] = 123;
286+
287+
expect(inputElement.value).not.toEqual('123');
288+
289+
scope.apply();
290+
291+
expect(inputElement.value).toEqual('123');
292+
}));
257293

258294
});
259295

@@ -313,18 +349,38 @@ void main() {
313349
..selectionStart = 1
314350
..selectionEnd = 2;
315351

316-
model.render('abc');
352+
scope.apply(() {
353+
scope.context['model'] = 'abc';
354+
});
317355

318356
expect(element.value).toEqual('abc');
319357
expect(element.selectionStart).toEqual(1);
320358
expect(element.selectionEnd).toEqual(2);
321359

322-
model.render('xyz');
360+
scope.apply(() {
361+
scope.context['model'] = 'xyz';
362+
});
323363

324364
expect(element.value).toEqual('xyz');
325365
expect(element.selectionStart).toEqual(3);
326366
expect(element.selectionEnd).toEqual(3);
327367
}));
368+
369+
it('should only render the input value upon the next digest', inject((Scope scope) {
370+
_.compile('<input type="password" ng-model="model" probe="p">');
371+
Probe probe = _.rootScope.context['p'];
372+
var ngModel = probe.directive(NgModel);
373+
InputElement inputElement = probe.element;
374+
375+
ngModel.render('xyz');
376+
scope.context['model'] = 'xyz';
377+
378+
expect(inputElement.value).not.toEqual('xyz');
379+
380+
scope.apply();
381+
382+
expect(inputElement.value).toEqual('xyz');
383+
}));
328384
});
329385

330386
describe('type="search"', () {
@@ -382,20 +438,40 @@ void main() {
382438
..selectionStart = 1
383439
..selectionEnd = 2;
384440

385-
model.render('abc');
441+
scope.apply(() {
442+
scope.context['model'] = 'abc';
443+
});
386444

387445
expect(element.value).toEqual('abc');
388446
// No update. selectionStart/End is unchanged.
389447
expect(element.selectionStart).toEqual(1);
390448
expect(element.selectionEnd).toEqual(2);
391449

392-
model.render('xyz');
450+
scope.apply(() {
451+
scope.context['model'] = 'xyz';
452+
});
393453

394454
// Value updated. selectionStart/End changed.
395455
expect(element.value).toEqual('xyz');
396456
expect(element.selectionStart).toEqual(3);
397457
expect(element.selectionEnd).toEqual(3);
398458
}));
459+
460+
it('should only render the input value upon the next digest', inject((Scope scope) {
461+
_.compile('<input type="search" ng-model="model" probe="p">');
462+
Probe probe = _.rootScope.context['p'];
463+
var ngModel = probe.directive(NgModel);
464+
InputElement inputElement = probe.element;
465+
466+
ngModel.render('xyz');
467+
scope.context['model'] = 'xyz';
468+
469+
expect(inputElement.value).not.toEqual('xyz');
470+
471+
scope.apply();
472+
473+
expect(inputElement.value).toEqual('xyz');
474+
}));
399475
});
400476

401477
describe('no type attribute', () {
@@ -459,18 +535,38 @@ void main() {
459535
..selectionStart = 1
460536
..selectionEnd = 2;
461537

462-
model.render('abc');
538+
scope.apply(() {
539+
scope.context['model'] = 'abc';
540+
});
463541

464542
expect(element.value).toEqual('abc');
465543
expect(element.selectionStart).toEqual(1);
466544
expect(element.selectionEnd).toEqual(2);
467545

468-
model.render('xyz');
546+
scope.apply(() {
547+
scope.context['model'] = 'xyz';
548+
});
469549

470550
expect(element.value).toEqual('xyz');
471551
expect(element.selectionStart).toEqual(3);
472552
expect(element.selectionEnd).toEqual(3);
473553
}));
554+
555+
it('should only render the input value upon the next digest', inject((Scope scope) {
556+
_.compile('<input ng-model="model" probe="p">');
557+
Probe probe = _.rootScope.context['p'];
558+
var ngModel = probe.directive(NgModel);
559+
InputElement inputElement = probe.element;
560+
561+
ngModel.render('xyz');
562+
scope.context['model'] = 'xyz';
563+
564+
expect(inputElement.value).not.toEqual('xyz');
565+
566+
scope.apply();
567+
568+
expect(inputElement.value).toEqual('xyz');
569+
}));
474570
});
475571

476572
describe('type="checkbox"', () {
@@ -557,6 +653,22 @@ void main() {
557653
_.triggerEvent(element, 'change');
558654
expect(scope.context['model']).toBe(false);
559655
}));
656+
657+
it('should only render the input value upon the next digest', inject((Scope scope) {
658+
_.compile('<input type="checkbox" ng-model="model" probe="p">');
659+
Probe probe = _.rootScope.context['p'];
660+
var ngModel = probe.directive(NgModel);
661+
InputElement inputElement = probe.element;
662+
663+
ngModel.render('xyz');
664+
scope.context['model'] = true;
665+
666+
expect(inputElement.checked).toBe(false);
667+
668+
scope.apply();
669+
670+
expect(inputElement.checked).toBe(true);
671+
}));
560672
});
561673

562674
describe('textarea', () {
@@ -631,6 +743,22 @@ void main() {
631743
expect(element.selectionStart).toEqual(0);
632744
expect(element.selectionEnd).toEqual(0);
633745
}));
746+
747+
it('should only render the input value upon the next digest', inject((Scope scope) {
748+
_.compile('<textarea ng-model="model" probe="p"></textarea>');
749+
Probe probe = _.rootScope.context['p'];
750+
var ngModel = probe.directive(NgModel);
751+
TextAreaElement inputElement = probe.element;
752+
753+
ngModel.render('xyz');
754+
scope.context['model'] = 'xyz';
755+
756+
expect(inputElement.value).not.toEqual('xyz');
757+
758+
scope.apply();
759+
760+
expect(inputElement.value).toEqual('xyz');
761+
}));
634762
});
635763

636764
describe('type="radio"', () {
@@ -772,6 +900,34 @@ void main() {
772900
expect(input1.classes.contains("ng-pristine")).toBe(false);
773901
expect(input1.classes.contains("ng-pristine")).toBe(false);
774902
}));
903+
904+
it('should only render the input value upon the next digest', inject((Scope scope) {
905+
var element = _.compile(
906+
'<div>' +
907+
' <input type="radio" id="on" ng-model="model" probe="i" value="on" />' +
908+
' <input type="radio" id="off" ng-model="model" probe="j" value="off" />' +
909+
'</div>'
910+
);
911+
912+
Probe probe1 = _.rootScope.context['i'];
913+
var ngModel1 = probe1.directive(NgModel);
914+
InputElement inputElement1 = probe1.element;
915+
916+
Probe probe2 = _.rootScope.context['j'];
917+
var ngModel2 = probe2.directive(NgModel);
918+
InputElement inputElement2 = probe2.element;
919+
920+
ngModel1.render('on');
921+
scope.context['model'] = 'on';
922+
923+
expect(inputElement1.checked).toBe(false);
924+
expect(inputElement2.checked).toBe(false);
925+
926+
scope.apply();
927+
928+
expect(inputElement1.checked).toBe(true);
929+
expect(inputElement2.checked).toBe(false);
930+
}));
775931
});
776932

777933
describe('type="search"', () {
@@ -810,6 +966,22 @@ void main() {
810966
input.processValue();
811967
expect(_.rootScope.context['model']).toEqual('123');
812968
}));
969+
970+
it('should only render the input value upon the next digest', inject((Scope scope) {
971+
_.compile('<input type="search" ng-model="model" probe="p">');
972+
Probe probe = _.rootScope.context['p'];
973+
var ngModel = probe.directive(NgModel);
974+
InputElement inputElement = probe.element;
975+
976+
ngModel.render('xyz');
977+
scope.context['model'] = 'xyz';
978+
979+
expect(inputElement.value).not.toEqual('xyz');
980+
981+
scope.apply();
982+
983+
expect(inputElement.value).toEqual('xyz');
984+
}));
813985
});
814986

815987
describe('contenteditable', () {
@@ -836,6 +1008,22 @@ void main() {
8361008
input.processValue();
8371009
expect(_.rootScope.context['model']).toEqual('def');
8381010
}));
1011+
1012+
it('should only render the input value upon the next digest', inject((Scope scope) {
1013+
_.compile('<div contenteditable ng-model="model" probe="p"></div>');
1014+
Probe probe = _.rootScope.context['p'];
1015+
var ngModel = probe.directive(NgModel);
1016+
Element element = probe.element;
1017+
1018+
ngModel.render('xyz');
1019+
scope.context['model'] = 'xyz';
1020+
1021+
expect(element.innerHtml).not.toEqual('xyz');
1022+
1023+
scope.apply();
1024+
1025+
expect(element.innerHtml).toEqual('xyz');
1026+
}));
8391027
});
8401028

8411029
describe('pristine / dirty', () {

0 commit comments

Comments
 (0)