diff --git a/example/pubspec.lock b/example/pubspec.lock
index f2692c5e6..8991b5523 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -18,7 +18,7 @@ packages:
barback:
description: barback
source: hosted
- version: "0.13.0"
+ version: "0.12.0"
browser:
description: browser
source: hosted
diff --git a/example/web/hello_world.html b/example/web/hello_world.html
index 1b4c681d9..705607d48 100644
--- a/example/web/hello_world.html
+++ b/example/web/hello_world.html
@@ -6,7 +6,7 @@
Hello {{ctrl.name}}!
-name:
+name:
diff --git a/lib/directive/module.dart b/lib/directive/module.dart
index 37c01c3a6..c934a2599 100644
--- a/lib/directive/module.dart
+++ b/lib/directive/module.dart
@@ -19,6 +19,8 @@ library angular.directive;
import 'package:di/di.dart';
import 'dart:html' as dom;
+import 'dart:convert' as convert;
+import 'dart:async' as async;
import 'package:intl/intl.dart';
import 'package:angular/core/annotation.dart';
import 'package:angular/core/module_internal.dart';
@@ -51,10 +53,11 @@ part 'ng_non_bindable.dart';
part 'ng_model_select.dart';
part 'ng_form.dart';
part 'ng_model_validators.dart';
+part 'ng_model_options.dart';
class DecoratorFormatter extends Module {
DecoratorFormatter() {
- bind(AHref, toValue: null);
+ bind(AHref, toValue: null);
bind(NgBaseCss); // The root injector should have an empty NgBaseCss
bind(NgBind, toValue: null);
bind(NgBindTemplate, toValue: null);
@@ -81,6 +84,7 @@ class DecoratorFormatter extends Module {
bind(ContentEditable, toValue: null);
bind(NgBindTypeForDateLike, toValue: null);
bind(NgModel, toValue: null);
+ bind(NgModelOptions, toValue: null);
bind(NgValue, toValue: null);
bind(NgTrueValue, toValue: new NgTrueValue());
bind(NgFalseValue, toValue: new NgFalseValue());
diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart
index 9eaf82e8b..34d6560af 100644
--- a/lib/directive/ng_model.dart
+++ b/lib/directive/ng_model.dart
@@ -148,8 +148,7 @@ class NgModel extends NgControl implements AttachAware {
onChange(changeRecord is CollectionChangeRecord
? changeRecord.iterable
: changeRecord);
- },
- collection: true);
+ }, collection: true);
} else if (_expression != null) {
_watch = _scope.watch(_expression, onChange);
}
@@ -294,26 +293,29 @@ class InputCheckbox {
final NgModel ngModel;
final NgTrueValue ngTrueValue;
final NgFalseValue ngFalseValue;
+ final NgModelOptions ngModelOptions;
final Scope scope;
InputCheckbox(dom.Element this.inputElement, this.ngModel,
- this.scope, this.ngTrueValue, this.ngFalseValue) {
+ this.scope, this.ngTrueValue, this.ngFalseValue, this.ngModelOptions) {
ngModel.render = (value) {
scope.rootScope.domWrite(() {
inputElement.checked = ngTrueValue.isValue(value);
});
};
inputElement
- ..onChange.listen((_) {
- ngModel.viewValue = inputElement.checked
- ? ngTrueValue.value : ngFalseValue.value;
- })
- ..onBlur.listen((e) {
+ ..onChange.listen((_) => ngModelOptions.executeChangeFunc(() {
+ ngModel.viewValue = inputElement.checked ? ngTrueValue.value : ngFalseValue.value;
+ }))
+ ..onBlur.listen((_) => ngModelOptions.executeBlurFunc(() {
ngModel.markAsTouched();
- });
+ }));
}
}
+
+
+
/**
* Usage:
*
@@ -337,15 +339,17 @@ class InputCheckbox {
class InputTextLike {
final dom.Element inputElement;
final NgModel ngModel;
+ final NgModelOptions ngModelOptions;
final Scope scope;
String _inputType;
+
get typedValue => (inputElement as dynamic).value;
void set typedValue(value) {
(inputElement as dynamic).value = (value == null) ? '' : value.toString();
}
- InputTextLike(this.inputElement, this.ngModel, this.scope) {
+ InputTextLike(this.inputElement, this.ngModel, this.scope, this.ngModelOptions) {
ngModel.render = (value) {
scope.rootScope.domWrite(() {
if (value == null) value = '';
@@ -353,21 +357,24 @@ class InputTextLike {
var currentValue = typedValue;
if (value != currentValue && !(value is num && currentValue is num &&
value.isNaN && currentValue.isNaN)) {
- typedValue = value;
+ typedValue = value;
}
});
};
+
inputElement
- ..onChange.listen(processValue)
- ..onInput.listen(processValue)
- ..onBlur.listen((e) {
+ ..onChange.listen((event) => ngModelOptions.executeChangeFunc(() => processValue(event)))
+ ..onInput.listen((event) => ngModelOptions.executeInputFunc(() => processValue(event)))
+ ..onBlur.listen((_) => ngModelOptions.executeBlurFunc(() => () {
ngModel.markAsTouched();
- });
+ }));
}
void processValue([_]) {
var value = typedValue;
+
if (value != ngModel.viewValue) ngModel.viewValue = value;
+
ngModel.validate();
}
}
@@ -394,6 +401,7 @@ class InputTextLike {
class InputNumberLike {
final dom.InputElement inputElement;
final NgModel ngModel;
+ final NgModelOptions ngModelOptions;
final Scope scope;
@@ -414,21 +422,20 @@ class InputNumberLike {
}
}
- InputNumberLike(dom.Element this.inputElement, this.ngModel, this.scope) {
+ InputNumberLike(dom.Element this.inputElement, this.ngModel, this.scope, this.ngModelOptions) {
ngModel.render = (value) {
scope.rootScope.domWrite(() {
- if (value != typedValue
- && (value == null || value is num && !value.isNaN)) {
+ if (value != typedValue && (value == null || value is num && !value.isNaN)) {
typedValue = value;
}
});
};
inputElement
- ..onChange.listen(relaxFnArgs(processValue))
- ..onInput.listen(relaxFnArgs(processValue))
- ..onBlur.listen((e) {
+ ..onChange.listen((event) => ngModelOptions.executeChangeFunc(() => processValue()))
+ ..onInput.listen((event) => ngModelOptions.executeInputFunc(() => processValue()))
+ ..onBlur.listen((event) => ngModelOptions.executeBlurFunc(() => () {
ngModel.markAsTouched();
- });
+ }));
}
void processValue() {
@@ -484,9 +491,12 @@ class NgBindTypeForDateLike {
dynamic get inputTypedValue {
switch (idlAttrKind) {
- case DATE: return inputValueAsDate;
- case NUMBER: return inputElement.valueAsNumber;
- default: return inputElement.value;
+ case DATE:
+ return inputValueAsDate;
+ case NUMBER:
+ return inputElement.valueAsNumber;
+ default:
+ return inputElement.value;
}
}
@@ -586,11 +596,12 @@ class InputDateLike {
toFactory: (Injector i) => new NgBindTypeForDateLike(i.get(dom.Element)));
final dom.InputElement inputElement;
final NgModel ngModel;
+ final NgModelOptions ngModelOptions;
final Scope scope;
NgBindTypeForDateLike ngBindType;
InputDateLike(dom.Element this.inputElement, this.ngModel, this.scope,
- this.ngBindType) {
+ this.ngBindType, this.ngModelOptions) {
if (inputElement.type == 'datetime-local') {
ngBindType.idlAttrKind = NgBindTypeForDateLike.NUMBER;
}
@@ -600,11 +611,11 @@ class InputDateLike {
});
};
inputElement
- ..onChange.listen(relaxFnArgs(processValue))
- ..onInput.listen(relaxFnArgs(processValue))
- ..onBlur.listen((e) {
+ ..onChange.listen((event) => ngModelOptions.executeChangeFunc(() => processValue()))
+ ..onInput.listen((event) => ngModelOptions.executeInputFunc(() => processValue()))
+ ..onBlur.listen((_) => ngModelOptions.executeBlurFunc(() => () {
ngModel.markAsTouched();
- });
+ }));
}
dynamic get typedValue => ngBindType.inputTypedValue;
@@ -680,7 +691,9 @@ class NgValue {
NgValue(this.element);
@NgOneWay('ng-value')
- void set value(val) { this._value = val; }
+ void set value(val) {
+ this._value = val;
+ }
dynamic get value => _value == null ? (element as dynamic).value : _value;
}
@@ -785,8 +798,8 @@ class InputRadio {
*/
@Decorator(selector: '[contenteditable][ng-model]')
class ContentEditable extends InputTextLike {
- ContentEditable(dom.Element inputElement, NgModel ngModel, Scope scope)
- : super(inputElement, ngModel, scope);
+ ContentEditable(dom.Element inputElement, NgModel ngModel, Scope scope, NgModelOptions modelOptions)
+ : super(inputElement, ngModel, scope, modelOptions);
// The implementation is identical to InputTextLike but use innerHtml instead of value
String get typedValue => (inputElement as dynamic).innerHtml;
diff --git a/lib/directive/ng_model_options.dart b/lib/directive/ng_model_options.dart
new file mode 100644
index 000000000..15f136ccd
--- /dev/null
+++ b/lib/directive/ng_model_options.dart
@@ -0,0 +1,62 @@
+part of angular.directive;
+
+@Decorator(selector: 'input[ng-model-options]')
+class NgModelOptions {
+ int _debounceDefaultValue = 0;
+ int _debounceBlurValue;
+ int _debounceChangeValue;
+ int _debounceInputValue;
+
+ async.Timer _blurTimer;
+ async.Timer _changeTimer;
+ async.Timer _inputTimer;
+
+ static const String _DEBOUNCE_DEFAULT_KEY = "default";
+ static const String _DEBOUNCE_BLUR_KEY = "blur";
+ static const String _DEBOUNCE_CHANGE_KEY = "change";
+ static const String _DEBOUNCE_INPUT_KEY = "input";
+
+ NgModelOptions(NodeAttrs attrs) {
+ var jsonFormattedOptions = attrs["ng-model-options"].replaceFirst("debounce", "'debounce'")
+ .replaceAll("'", "\"");
+ Map options = convert.JSON.decode(jsonFormattedOptions);
+
+ if(options["debounce"] is int){
+ _debounceDefaultValue = options["debounce"];
+ }else{
+ if (options["debounce"].containsKey(_DEBOUNCE_DEFAULT_KEY)){
+ _debounceDefaultValue = options["debounce"][_DEBOUNCE_DEFAULT_KEY];
+ }
+ _debounceBlurValue = options["debounce"][_DEBOUNCE_BLUR_KEY];
+ _debounceChangeValue = options["debounce"][_DEBOUNCE_CHANGE_KEY];
+ _debounceInputValue = options["debounce"][_DEBOUNCE_INPUT_KEY];
+ }
+ }
+
+
+ void executeBlurFunc(func()) {
+ var delay = _debounceBlurValue == null ? _debounceDefaultValue : _debounceBlurValue;
+ _blurTimer = _runFuncDebounced(delay, func, _blurTimer);
+ }
+
+ void executeChangeFunc(func()) {
+ var delay = _debounceChangeValue == null ? _debounceDefaultValue : _debounceChangeValue;
+ _changeTimer = _runFuncDebounced(delay, func, _changeTimer);
+ }
+
+ void executeInputFunc(func()) {
+ var delay = _debounceInputValue == null ? _debounceDefaultValue : _debounceInputValue;
+ _inputTimer = _runFuncDebounced(delay, func, _inputTimer);
+ }
+
+ async.Timer _runFuncDebounced(int delay, func(), async.Timer timer){
+ if (timer != null && timer.isActive) timer.cancel();
+
+ if(delay == 0){
+ func();
+ return null;
+ } else {
+ return new async.Timer(new Duration(milliseconds: delay), func);
+ }
+ }
+}
diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart
index 63bc86b5c..6ac1fc1be 100644
--- a/test/directive/ng_model_spec.dart
+++ b/test/directive/ng_model_spec.dart
@@ -86,16 +86,18 @@ void main() {
it('should write to input only if the value is different',
(Injector i, Animate animate) {
+ NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
+
var scope = _.rootScope;
var element = new dom.InputElement();
var ngElement = new NgElement(element, scope, animate);
-
- NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
+ var ngModelOptions = new NgModelOptions(nodeAttrs);
+
nodeAttrs['ng-model'] = 'model';
var model = new NgModel(scope, ngElement, i.createChild([new Module()]),
nodeAttrs, new Animate());
dom.querySelector('body').append(element);
- var input = new InputTextLike(element, model, scope);
+ var input = new InputTextLike(element, model, scope, ngModelOptions);
element
..value = 'abc'
@@ -364,16 +366,18 @@ void main() {
it('should write to input only if value is different',
(Injector i, Animate animate) {
+ NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
+
var scope = _.rootScope;
var element = new dom.InputElement();
var ngElement = new NgElement(element, scope, animate);
+ var ngModelOptions = new NgModelOptions(nodeAttrs);
- NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
nodeAttrs['ng-model'] = 'model';
var model = new NgModel(scope, ngElement, i.createChild([new Module()]),
nodeAttrs, new Animate());
dom.querySelector('body').append(element);
- var input = new InputTextLike(element, model, scope);
+ var input = new InputTextLike(element, model, scope, ngModelOptions);
element
..value = 'abc'
@@ -454,16 +458,18 @@ void main() {
it('should write to input only if value is different',
(Injector i, Animate animate) {
+ NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
+
var scope = _.rootScope;
var element = new dom.InputElement();
var ngElement = new NgElement(element, scope, animate);
+ var ngModelOptions = new NgModelOptions(nodeAttrs);
- NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
nodeAttrs['ng-model'] = 'model';
var model = new NgModel(scope, ngElement, i.createChild([new Module()]),
nodeAttrs, new Animate());
dom.querySelector('body').append(element);
- var input = new InputTextLike(element, model, scope);
+ var input = new InputTextLike(element, model, scope, ngModelOptions);
element
..value = 'abc'
@@ -552,16 +558,18 @@ void main() {
it('should write to input only if value is different',
(Injector i, Animate animate) {
+ NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
+
var scope = _.rootScope;
var element = new dom.InputElement();
var ngElement = new NgElement(element, scope, animate);
+ var ngModelOptions = new NgModelOptions(nodeAttrs);
- NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
nodeAttrs['ng-model'] = 'model';
var model = new NgModel(scope, ngElement, i.createChild([new Module()]),
nodeAttrs, new Animate());
dom.querySelector('body').append(element);
- var input = new InputTextLike(element, model, scope);
+ var input = new InputTextLike(element, model, scope, ngModelOptions);
element
..value = 'abc'
@@ -761,16 +769,18 @@ void main() {
xit('should write to input only if value is different',
(Injector i, Animate animate) {
+ NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
+
var scope = _.rootScope;
var element = new dom.TextAreaElement();
var ngElement = new NgElement(element, scope, animate);
+ var ngModelOptions = new NgModelOptions(nodeAttrs);
- NodeAttrs nodeAttrs = new NodeAttrs(new DivElement());
nodeAttrs['ng-model'] = 'model';
var model = new NgModel(scope, ngElement, i.createChild([new Module()]),
nodeAttrs, new Animate());
dom.querySelector('body').append(element);
- var input = new InputTextLike(element, model, scope);
+ var input = new InputTextLike(element, model, scope, ngModelOptions);
element
..value = 'abc'