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

Commit cf0160b

Browse files
chalinmhevery
authored andcommitted
fix(ngModel): process input type=number according to convention, using valueAsNumber
Closes #574, Closes #575, Closes #576. Previous patches of relevance: #527, #415 Closes #577
1 parent a630487 commit cf0160b

File tree

2 files changed

+90
-10
lines changed

2 files changed

+90
-10
lines changed

lib/directive/ng_model.dart

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,17 @@ class InputTextLikeDirective {
211211
*
212212
* <input type="number|range" ng-model="myModel">
213213
*
214-
* This creates a two-way binding between a number-based input element
215-
* so long as the ng-model attribute is present on the input element. Whenever
216-
* the value of the input element changes then the matching model property on the
217-
* scope will be updated as well as the other way around (when the scope property
218-
* is updated).
219-
*
214+
* Model:
215+
*
216+
* num myModel;
217+
*
218+
* This creates a two-way binding between the input and the named model property
219+
* (e.g., myModel in the example above). When processing the input, its value is
220+
* read as a [num], via the [dom.InputElement.valueAsNumber] field. If the input
221+
* text does not represent a number, then the model is appropriately set to
222+
* [double.NAN]. Setting the model property to [null] will clear the input.
223+
* Setting the model to [double.NAN] will have no effect (input will be left
224+
* unchanged).
220225
*/
221226
@NgDirective(selector: 'input[type=number][ng-model]')
222227
@NgDirective(selector: 'input[type=range][ng-model]')
@@ -225,17 +230,34 @@ class InputNumberLikeDirective {
225230
final NgModel ngModel;
226231
final Scope scope;
227232

233+
num get typedValue => inputElement.valueAsNumber;
234+
void set typedValue(num value) {
235+
// [chalin, 2014-02-16] This post
236+
// http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2010-January/024829.html
237+
// suggests that setting `valueAsNumber` to null should clear the field, but
238+
// it does not. [TODO: put BUG/ISSUE number here]. We implement a
239+
// workaround by setting `value`. Clean-up once the bug is fixed.
240+
if (value == null) {
241+
inputElement.value = null;
242+
} else {
243+
inputElement.valueAsNumber = value;
244+
}
245+
}
246+
228247
InputNumberLikeDirective(dom.Element this.inputElement, this.ngModel, this.scope) {
229248
ngModel.render = (value) {
230-
inputElement.value = value == null ? '' : value.toString();
249+
if (value != typedValue
250+
&& (value == null || value is num && !value.isNaN)) {
251+
typedValue = value;
252+
}
231253
};
232254
inputElement
233255
..onChange.listen(relaxFnArgs(processValue))
234256
..onInput.listen(relaxFnArgs(processValue));
235257
}
236258

237259
processValue() {
238-
var value = num.parse(inputElement.value, (_) => null);
260+
num value = typedValue;
239261
if (value != ngModel.viewValue) {
240262
ngModel.dirty = true;
241263
scope.$apply(() => ngModel.viewValue = value);

test/directive/ng_model_spec.dart

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,65 @@ describe('ng-model', () {
109109
}));
110110
});
111111

112+
/* An input of type number can only have assigned to its [value] field
113+
* a string that represents a valid number. Any attempts to assign
114+
* any other string will have no effect on the [value] field.
115+
*
116+
* This function simulates typing the given string value into the input
117+
* field regardless of whether it represents a valid number or not. It
118+
* has the side-effect of setting the window focus to the input.
119+
*/
120+
void setNumberInputValue(InputElement input, String value) {
121+
input..focus()
122+
..dispatchEvent(new TextEvent('textInput', data: value));
123+
}
124+
112125
describe('type="number" like', () {
126+
127+
it('should leave input unchanged when text does not represent a valid number', inject((Injector i) {
128+
var modelFieldName = 'modelForNumFromInvalid1';
129+
var element = _.compile('<input type="number" ng-model="$modelFieldName">');
130+
dom.querySelector('body').append(element);
131+
132+
// Subcase: text not representing a valid number.
133+
var piX = '3.141x';
134+
setNumberInputValue(element, piX);
135+
// Because the text is not a valid number, the element value is empty.
136+
expect(element.value).toEqual('');
137+
// But the selection can confirm that the text is there:
138+
element.selectionStart = 0;
139+
element.selectionEnd = piX.length;
140+
expect(dom.window.getSelection().toString()).toEqual(piX);
141+
// When the input is invalid, the model is [double.NAN]:
142+
_.triggerEvent(element, 'change');
143+
expect(_.rootScope[modelFieldName].isNaN).toBeTruthy();
144+
145+
// Subcase: text representing a valid number (as if the user had erased
146+
// the trailing 'x').
147+
num pi = 3.14159;
148+
setNumberInputValue(element, pi.toString());
149+
_.triggerEvent(element, 'change');
150+
expect(element.value).toEqual(pi.toString());
151+
expect(_.rootScope[modelFieldName]).toEqual(pi);
152+
}));
153+
154+
it('should not reformat user input to equivalent numeric representation', inject((Injector i) {
155+
var modelFieldName = 'modelForNumFromInvalid2';
156+
var element = _.compile('<input type="number" ng-model="$modelFieldName">');
157+
dom.querySelector('body').append(element);
158+
159+
var ten = '1e1';
160+
setNumberInputValue(element, ten);
161+
_.triggerEvent(element, 'change');
162+
expect(_.rootScope[modelFieldName]).toEqual(10);
163+
// Ensure that the input text is literally the same
164+
element.selectionStart = 0;
165+
// Set the selectionEnd to one beyond ten.length in
166+
// case angular added some extra text.
167+
element.selectionEnd = ten.length + 1;
168+
expect(dom.window.getSelection().toString()).toEqual(ten);
169+
}));
170+
113171
it('should update input value from model', inject(() {
114172
_.compile('<input type="number" ng-model="model">');
115173
_.rootScope.$digest();
@@ -142,15 +200,15 @@ describe('ng-model', () {
142200
expect(_.rootScope.model).toEqual(43);
143201
}));
144202

145-
it('should update model to null from a blank input value', inject(() {
203+
it('should update model to NaN from a blank input value', inject(() {
146204
_.compile('<input type="number" ng-model="model" probe="p">');
147205
Probe probe = _.rootScope.p;
148206
var ngModel = probe.directive(NgModel);
149207
InputElement inputElement = probe.element;
150208

151209
inputElement.value = '';
152210
_.triggerEvent(inputElement, 'change');
153-
expect(_.rootScope.model).toBeNull();
211+
expect(_.rootScope.model.isNaN).toBeTruthy();
154212
}));
155213

156214
it('should update model from the input value for range inputs', inject(() {

0 commit comments

Comments
 (0)