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'