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

Commit

Permalink
fix(ngModel): process input type=number according to convention, usin…
Browse files Browse the repository at this point in the history
…g valueAsNumber

Closes #574, Closes #575, Closes #576. 

Previous patches of relevance: #527, #415

Closes #577
  • Loading branch information
chalin authored and mhevery committed Feb 19, 2014
1 parent a630487 commit cf0160b
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 10 deletions.
38 changes: 30 additions & 8 deletions lib/directive/ng_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,17 @@ class InputTextLikeDirective {
*
* <input type="number|range" ng-model="myModel">
*
* 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]')
Expand All @@ -225,17 +230,34 @@ 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))
..onInput.listen(relaxFnArgs(processValue));
}

processValue() {
var value = num.parse(inputElement.value, (_) => null);
num value = typedValue;
if (value != ngModel.viewValue) {
ngModel.dirty = true;
scope.$apply(() => ngModel.viewValue = value);
Expand Down
62 changes: 60 additions & 2 deletions test/directive/ng_model_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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('<input type="number" ng-model="$modelFieldName">');
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('<input type="number" ng-model="$modelFieldName">');
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('<input type="number" ng-model="model">');
_.rootScope.$digest();
Expand Down Expand Up @@ -142,15 +200,15 @@ 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('<input type="number" ng-model="model" probe="p">');
Probe probe = _.rootScope.p;
var ngModel = probe.directive(NgModel);
InputElement inputElement = probe.element;

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(() {
Expand Down

0 comments on commit cf0160b

Please sign in to comment.