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'