From cf0160b8c316a39ac9d0fcce843c6f764429a1d4 Mon Sep 17 00:00:00 2001 From: Patrice Chalin Date: Sun, 16 Feb 2014 11:25:42 -0600 Subject: [PATCH] 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 --- lib/directive/ng_model.dart | 38 +++++++++++++++---- test/directive/ng_model_spec.dart | 62 ++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart index 239b2e68b..1ac6f9dce 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -211,12 +211,17 @@ class InputTextLikeDirective { * * * - * This creates a two-way binding between a number-based input element - * so long as the ng-model attribute is present on the input element. Whenever - * the value of the input element changes then the matching model property on the - * scope will be updated as well as the other way around (when the scope property - * is updated). - * + * Model: + * + * num myModel; + * + * This creates a two-way binding between the input and the named model property + * (e.g., myModel in the example above). When processing the input, its value is + * read as a [num], via the [dom.InputElement.valueAsNumber] field. If the input + * text does not represent a number, then the model is appropriately set to + * [double.NAN]. Setting the model property to [null] will clear the input. + * Setting the model to [double.NAN] will have no effect (input will be left + * unchanged). */ @NgDirective(selector: 'input[type=number][ng-model]') @NgDirective(selector: 'input[type=range][ng-model]') @@ -225,9 +230,26 @@ class InputNumberLikeDirective { final NgModel ngModel; final Scope scope; + num get typedValue => inputElement.valueAsNumber; + void set typedValue(num value) { + // [chalin, 2014-02-16] This post + // http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2010-January/024829.html + // suggests that setting `valueAsNumber` to null should clear the field, but + // it does not. [TODO: put BUG/ISSUE number here]. We implement a + // workaround by setting `value`. Clean-up once the bug is fixed. + if (value == null) { + inputElement.value = null; + } else { + inputElement.valueAsNumber = value; + } + } + InputNumberLikeDirective(dom.Element this.inputElement, this.ngModel, this.scope) { ngModel.render = (value) { - inputElement.value = value == null ? '' : value.toString(); + if (value != typedValue + && (value == null || value is num && !value.isNaN)) { + typedValue = value; + } }; inputElement ..onChange.listen(relaxFnArgs(processValue)) @@ -235,7 +257,7 @@ class InputNumberLikeDirective { } processValue() { - var value = num.parse(inputElement.value, (_) => null); + num value = typedValue; if (value != ngModel.viewValue) { ngModel.dirty = true; scope.$apply(() => ngModel.viewValue = value); diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart index 9240ad1c1..ce1ea6106 100644 --- a/test/directive/ng_model_spec.dart +++ b/test/directive/ng_model_spec.dart @@ -109,7 +109,65 @@ describe('ng-model', () { })); }); + /* An input of type number can only have assigned to its [value] field + * a string that represents a valid number. Any attempts to assign + * any other string will have no effect on the [value] field. + * + * This function simulates typing the given string value into the input + * field regardless of whether it represents a valid number or not. It + * has the side-effect of setting the window focus to the input. + */ + void setNumberInputValue(InputElement input, String value) { + input..focus() + ..dispatchEvent(new TextEvent('textInput', data: value)); + } + describe('type="number" like', () { + + it('should leave input unchanged when text does not represent a valid number', inject((Injector i) { + var modelFieldName = 'modelForNumFromInvalid1'; + var element = _.compile(''); + dom.querySelector('body').append(element); + + // Subcase: text not representing a valid number. + var piX = '3.141x'; + setNumberInputValue(element, piX); + // Because the text is not a valid number, the element value is empty. + expect(element.value).toEqual(''); + // But the selection can confirm that the text is there: + element.selectionStart = 0; + element.selectionEnd = piX.length; + expect(dom.window.getSelection().toString()).toEqual(piX); + // When the input is invalid, the model is [double.NAN]: + _.triggerEvent(element, 'change'); + expect(_.rootScope[modelFieldName].isNaN).toBeTruthy(); + + // Subcase: text representing a valid number (as if the user had erased + // the trailing 'x'). + num pi = 3.14159; + setNumberInputValue(element, pi.toString()); + _.triggerEvent(element, 'change'); + expect(element.value).toEqual(pi.toString()); + expect(_.rootScope[modelFieldName]).toEqual(pi); + })); + + it('should not reformat user input to equivalent numeric representation', inject((Injector i) { + var modelFieldName = 'modelForNumFromInvalid2'; + var element = _.compile(''); + dom.querySelector('body').append(element); + + var ten = '1e1'; + setNumberInputValue(element, ten); + _.triggerEvent(element, 'change'); + expect(_.rootScope[modelFieldName]).toEqual(10); + // Ensure that the input text is literally the same + element.selectionStart = 0; + // Set the selectionEnd to one beyond ten.length in + // case angular added some extra text. + element.selectionEnd = ten.length + 1; + expect(dom.window.getSelection().toString()).toEqual(ten); + })); + it('should update input value from model', inject(() { _.compile(''); _.rootScope.$digest(); @@ -142,7 +200,7 @@ describe('ng-model', () { expect(_.rootScope.model).toEqual(43); })); - it('should update model to null from a blank input value', inject(() { + it('should update model to NaN from a blank input value', inject(() { _.compile(''); Probe probe = _.rootScope.p; var ngModel = probe.directive(NgModel); @@ -150,7 +208,7 @@ describe('ng-model', () { inputElement.value = ''; _.triggerEvent(inputElement, 'change'); - expect(_.rootScope.model).toBeNull(); + expect(_.rootScope.model.isNaN).toBeTruthy(); })); it('should update model from the input value for range inputs', inject(() {