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/core/annotation_src.dart b/lib/core/annotation_src.dart
index 98664729a..643339ca9 100644
--- a/lib/core/annotation_src.dart
+++ b/lib/core/annotation_src.dart
@@ -547,11 +547,8 @@ abstract class DetachAware {
}
/**
- * Use the @[Formatter] class annotation to register a new formatter.
- *
- * A formatter is a pure function that performs a transformation on input data from an expression.
- * For more on formatters in Angular, see the documentation for the
- * [angular:formatter](#angular-formatter) library.
+ * Use @[Formatter] annotation to register a new formatter. A formatter is a class
+ * with a [call] method (a callable function).
*
* Usage:
*
diff --git a/lib/directive/module.dart b/lib/directive/module.dart
index 37c01c3a6..c0ef5ea98 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,6 +53,7 @@ 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() {
@@ -81,6 +84,7 @@ class DecoratorFormatter extends Module {
bind(ContentEditable, toValue: null);
bind(NgBindTypeForDateLike, toValue: null);
bind(NgModel, toValue: null);
+ bind(NgModelOptions, toValue: new NgModelOptions());
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..b3c9e5564 100644
--- a/lib/directive/ng_model.dart
+++ b/lib/directive/ng_model.dart
@@ -294,26 +294,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 +340,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 +358,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 +402,7 @@ class InputTextLike {
class InputNumberLike {
final dom.InputElement inputElement;
final NgModel ngModel;
+ final NgModelOptions ngModelOptions;
final Scope scope;
@@ -414,7 +423,7 @@ 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
@@ -424,11 +433,11 @@ class InputNumberLike {
});
};
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();
- });
+ }));
}
void processValue() {
@@ -586,11 +595,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 +610,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 +690,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;
}
@@ -767,7 +779,7 @@ class InputRadio {
..onClick.listen((_) {
if (radioButtonElement.checked) ngModel.viewValue = ngValue.value;
})
- ..onBlur.listen((e) {
+ ..onBlur.listen((event) {
ngModel.markAsTouched();
});
}
@@ -785,8 +797,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..dc6e734ab
--- /dev/null
+++ b/lib/directive/ng_model_options.dart
@@ -0,0 +1,62 @@
+part of angular.directive;
+
+@Decorator(
+ selector: 'input[ng-model-options]',
+ map: const {'ng-model-options': '=>options'})
+class NgModelOptions {
+ 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";
+
+ int _debounceDefaultValue = 0;
+ int _debounceBlurValue;
+ int _debounceChangeValue;
+ int _debounceInputValue;
+
+ async.Timer _blurTimer;
+ async.Timer _changeTimer;
+ async.Timer _inputTimer;
+
+ NgModelOptions();
+
+ void set options(options) {
+ if (options["debounce"] is int){
+ _debounceDefaultValue = options["debounce"];
+ } else {
+ Map debounceOptions = options["debounce"];
+ if (debounceOptions.containsKey(_DEBOUNCE_DEFAULT_KEY)){
+ _debounceDefaultValue = debounceOptions[_DEBOUNCE_DEFAULT_KEY];
+ }
+ _debounceBlurValue = debounceOptions[_DEBOUNCE_BLUR_KEY];
+ _debounceChangeValue = debounceOptions[_DEBOUNCE_CHANGE_KEY];
+ _debounceInputValue = debounceOptions[_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/angular_spec.dart b/test/angular_spec.dart
index 8efc3aa69..f302eb20e 100644
--- a/test/angular_spec.dart
+++ b/test/angular_spec.dart
@@ -182,6 +182,7 @@ main() {
"angular.directive.NgIf",
"angular.directive.NgInclude",
"angular.directive.NgModel",
+ "angular.directive.NgModelOptions",
"angular.directive.NgModelConverter",
"angular.directive.NgModelEmailValidator",
"angular.directive.NgModelMaxLengthValidator",
diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart
index 63bc86b5c..20b87665b 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);
+ var ngModelOptions = new NgModelOptions();
- 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'
@@ -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 = 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 = 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 = 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 = 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'