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

Commit

Permalink
fix(NgModel): ensure DOM value changes are only applied during scope.…
Browse files Browse the repository at this point in the history
…domWrite
  • Loading branch information
matsko authored and mhevery committed Mar 13, 2014
1 parent bed9fe1 commit 419e918
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 22 deletions.
34 changes: 21 additions & 13 deletions lib/directive/ng_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ class InputCheckboxDirective {
InputCheckboxDirective(dom.Element this.inputElement, this.ngModel,
this.scope, this.ngTrueValue, this.ngFalseValue) {
ngModel.render = (value) {
inputElement.checked = ngTrueValue.isValue(inputElement, value);
scope.rootScope.domWrite(() {
inputElement.checked = ngTrueValue.isValue(inputElement, value);
});
};
inputElement.onChange.listen((value) {
ngModel.viewValue = inputElement.checked
Expand Down Expand Up @@ -242,13 +244,15 @@ class InputTextLikeDirective {

InputTextLikeDirective(this.inputElement, this.ngModel, this.scope) {
ngModel.render = (value) {
if (value == null) value = '';

var currentValue = typedValue;
if (value != currentValue && !(value is num && currentValue is num &&
value.isNaN && currentValue.isNaN)) {
typedValue = value;
}
scope.rootScope.domWrite(() {
if (value == null) value = '';

var currentValue = typedValue;
if (value != currentValue && !(value is num && currentValue is num &&
value.isNaN && currentValue.isNaN)) {
typedValue = value;
}
});
};
inputElement
..onChange.listen(processValue)
Expand Down Expand Up @@ -309,10 +313,12 @@ class InputNumberLikeDirective {

InputNumberLikeDirective(dom.Element this.inputElement, this.ngModel, this.scope) {
ngModel.render = (value) {
if (value != typedValue
&& (value == null || value is num && !value.isNaN)) {
typedValue = value;
}
scope.rootScope.domWrite(() {
if (value != typedValue
&& (value == null || value is num && !value.isNaN)) {
typedValue = value;
}
});
};
inputElement
..onChange.listen(relaxFnArgs(processValue))
Expand Down Expand Up @@ -447,7 +453,9 @@ class InputRadioDirective {
attrs["name"] = _uidCounter.next();
}
ngModel.render = (value) {
radioButtonElement.checked = (value == ngValue.readValue(radioButtonElement));
scope.rootScope.domWrite(() {
radioButtonElement.checked = (value == ngValue.readValue(radioButtonElement));
});
};
radioButtonElement.onClick.listen((_) {
if (radioButtonElement.checked) {
Expand Down
206 changes: 197 additions & 9 deletions test/directive/ng_model_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ void main() {
expect(ngModel.valid).toBe(false);
}));

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

var scope = _.rootScope;
Expand All @@ -111,20 +111,40 @@ void main() {
..selectionStart = 1
..selectionEnd = 2;

model.render('abc');
scope.apply(() {
scope.context['model'] = 'abc';
});

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

model.render('xyz');
scope.apply(() {
scope.context['model'] = 'xyz';
});

// Value updated. selectionStart/End changed.
expect(element.value).toEqual('xyz');
expect(element.selectionStart).toEqual(3);
expect(element.selectionEnd).toEqual(3);
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<input type="text" ng-model="model" probe="p">');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
InputElement inputElement = probe.element;

ngModel.render('xyz');
scope.context['model'] = 'xyz';

expect(inputElement.value).not.toEqual('xyz');

scope.apply();

expect(inputElement.value).toEqual('xyz');
}));
});

/* This function simulates typing the given text into the input
Expand Down Expand Up @@ -254,6 +274,22 @@ void main() {
_.rootScope.apply('model = null');
expect((_.rootElement as dom.InputElement).value).toEqual('');
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<input type="number" ng-model="model" probe="p">');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
InputElement inputElement = probe.element;

ngModel.render(123);
scope.context['model'] = 123;

expect(inputElement.value).not.toEqual('123');

scope.apply();

expect(inputElement.value).toEqual('123');
}));

});

Expand Down Expand Up @@ -313,18 +349,38 @@ void main() {
..selectionStart = 1
..selectionEnd = 2;

model.render('abc');
scope.apply(() {
scope.context['model'] = 'abc';
});

expect(element.value).toEqual('abc');
expect(element.selectionStart).toEqual(1);
expect(element.selectionEnd).toEqual(2);

model.render('xyz');
scope.apply(() {
scope.context['model'] = 'xyz';
});

expect(element.value).toEqual('xyz');
expect(element.selectionStart).toEqual(3);
expect(element.selectionEnd).toEqual(3);
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<input type="password" ng-model="model" probe="p">');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
InputElement inputElement = probe.element;

ngModel.render('xyz');
scope.context['model'] = 'xyz';

expect(inputElement.value).not.toEqual('xyz');

scope.apply();

expect(inputElement.value).toEqual('xyz');
}));
});

describe('type="search"', () {
Expand Down Expand Up @@ -382,20 +438,40 @@ void main() {
..selectionStart = 1
..selectionEnd = 2;

model.render('abc');
scope.apply(() {
scope.context['model'] = 'abc';
});

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

model.render('xyz');
scope.apply(() {
scope.context['model'] = 'xyz';
});

// Value updated. selectionStart/End changed.
expect(element.value).toEqual('xyz');
expect(element.selectionStart).toEqual(3);
expect(element.selectionEnd).toEqual(3);
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<input type="search" ng-model="model" probe="p">');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
InputElement inputElement = probe.element;

ngModel.render('xyz');
scope.context['model'] = 'xyz';

expect(inputElement.value).not.toEqual('xyz');

scope.apply();

expect(inputElement.value).toEqual('xyz');
}));
});

describe('no type attribute', () {
Expand Down Expand Up @@ -459,18 +535,38 @@ void main() {
..selectionStart = 1
..selectionEnd = 2;

model.render('abc');
scope.apply(() {
scope.context['model'] = 'abc';
});

expect(element.value).toEqual('abc');
expect(element.selectionStart).toEqual(1);
expect(element.selectionEnd).toEqual(2);

model.render('xyz');
scope.apply(() {
scope.context['model'] = 'xyz';
});

expect(element.value).toEqual('xyz');
expect(element.selectionStart).toEqual(3);
expect(element.selectionEnd).toEqual(3);
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<input ng-model="model" probe="p">');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
InputElement inputElement = probe.element;

ngModel.render('xyz');
scope.context['model'] = 'xyz';

expect(inputElement.value).not.toEqual('xyz');

scope.apply();

expect(inputElement.value).toEqual('xyz');
}));
});

describe('type="checkbox"', () {
Expand Down Expand Up @@ -557,6 +653,22 @@ void main() {
_.triggerEvent(element, 'change');
expect(scope.context['model']).toBe(false);
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<input type="checkbox" ng-model="model" probe="p">');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
InputElement inputElement = probe.element;

ngModel.render('xyz');
scope.context['model'] = true;

expect(inputElement.checked).toBe(false);

scope.apply();

expect(inputElement.checked).toBe(true);
}));
});

describe('textarea', () {
Expand Down Expand Up @@ -631,6 +743,22 @@ void main() {
expect(element.selectionStart).toEqual(0);
expect(element.selectionEnd).toEqual(0);
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<textarea ng-model="model" probe="p"></textarea>');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
TextAreaElement inputElement = probe.element;

ngModel.render('xyz');
scope.context['model'] = 'xyz';

expect(inputElement.value).not.toEqual('xyz');

scope.apply();

expect(inputElement.value).toEqual('xyz');
}));
});

describe('type="radio"', () {
Expand Down Expand Up @@ -772,6 +900,34 @@ void main() {
expect(input1.classes.contains("ng-pristine")).toBe(false);
expect(input1.classes.contains("ng-pristine")).toBe(false);
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
var element = _.compile(
'<div>' +
' <input type="radio" id="on" ng-model="model" probe="i" value="on" />' +
' <input type="radio" id="off" ng-model="model" probe="j" value="off" />' +
'</div>'
);

Probe probe1 = _.rootScope.context['i'];
var ngModel1 = probe1.directive(NgModel);
InputElement inputElement1 = probe1.element;

Probe probe2 = _.rootScope.context['j'];
var ngModel2 = probe2.directive(NgModel);
InputElement inputElement2 = probe2.element;

ngModel1.render('on');
scope.context['model'] = 'on';

expect(inputElement1.checked).toBe(false);
expect(inputElement2.checked).toBe(false);

scope.apply();

expect(inputElement1.checked).toBe(true);
expect(inputElement2.checked).toBe(false);
}));
});

describe('type="search"', () {
Expand Down Expand Up @@ -810,6 +966,22 @@ void main() {
input.processValue();
expect(_.rootScope.context['model']).toEqual('123');
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<input type="search" ng-model="model" probe="p">');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
InputElement inputElement = probe.element;

ngModel.render('xyz');
scope.context['model'] = 'xyz';

expect(inputElement.value).not.toEqual('xyz');

scope.apply();

expect(inputElement.value).toEqual('xyz');
}));
});

describe('contenteditable', () {
Expand All @@ -836,6 +1008,22 @@ void main() {
input.processValue();
expect(_.rootScope.context['model']).toEqual('def');
}));

it('should only render the input value upon the next digest', inject((Scope scope) {
_.compile('<div contenteditable ng-model="model" probe="p"></div>');
Probe probe = _.rootScope.context['p'];
var ngModel = probe.directive(NgModel);
Element element = probe.element;

ngModel.render('xyz');
scope.context['model'] = 'xyz';

expect(element.innerHtml).not.toEqual('xyz');

scope.apply();

expect(element.innerHtml).toEqual('xyz');
}));
});

describe('pristine / dirty', () {
Expand Down

0 comments on commit 419e918

Please sign in to comment.