Skip to content

Commit 1acf9bd

Browse files
jrote1Diana Salsbury
authored and
Diana Salsbury
committed
feat(ng-model): Added ng-model-options
Added support for the debounce part of ng-model-options Closes dart-archive#969 Closes dart-archive#974
1 parent a59f246 commit 1acf9bd

File tree

7 files changed

+129
-43
lines changed

7 files changed

+129
-43
lines changed

example/web/hello_world.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<body hello-world-controller>
77

88
<h3>Hello {{ctrl.name}}!</h3>
9-
name: <input type="text" ng-model="ctrl.name">
9+
name: <input type="text" ng-model="ctrl.name" ng-model-options="{ debounce: {'default': 500, 'blur': 0} }">
1010

1111
<script type="application/dart" src="hello_world.dart"></script>
1212
<script src="packages/browser/dart.js"></script>

lib/core/annotation_src.dart

+2-5
Original file line numberDiff line numberDiff line change
@@ -547,11 +547,8 @@ abstract class DetachAware {
547547
}
548548

549549
/**
550-
* Use the @[Formatter] class annotation to register a new formatter.
551-
*
552-
* A formatter is a pure function that performs a transformation on input data from an expression.
553-
* For more on formatters in Angular, see the documentation for the
554-
* [angular:formatter](#angular-formatter) library.
550+
* Use @[Formatter] annotation to register a new formatter. A formatter is a class
551+
* with a [call] method (a callable function).
555552
*
556553
* Usage:
557554
*

lib/directive/module.dart

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ library angular.directive;
1919

2020
import 'package:di/di.dart';
2121
import 'dart:html' as dom;
22+
import 'dart:convert' as convert;
23+
import 'dart:async' as async;
2224
import 'package:intl/intl.dart';
2325
import 'package:angular/core/annotation.dart';
2426
import 'package:angular/core/module_internal.dart';
@@ -51,6 +53,7 @@ part 'ng_non_bindable.dart';
5153
part 'ng_model_select.dart';
5254
part 'ng_form.dart';
5355
part 'ng_model_validators.dart';
56+
part 'ng_model_options.dart';
5457

5558
class DecoratorFormatter extends Module {
5659
DecoratorFormatter() {
@@ -81,6 +84,7 @@ class DecoratorFormatter extends Module {
8184
bind(ContentEditable, toValue: null);
8285
bind(NgBindTypeForDateLike, toValue: null);
8386
bind(NgModel, toValue: null);
87+
bind(NgModelOptions, toValue: new NgModelOptions());
8488
bind(NgValue, toValue: null);
8589
bind(NgTrueValue, toValue: new NgTrueValue());
8690
bind(NgFalseValue, toValue: new NgFalseValue());

lib/directive/ng_model.dart

+39-27
Original file line numberDiff line numberDiff line change
@@ -294,26 +294,29 @@ class InputCheckbox {
294294
final NgModel ngModel;
295295
final NgTrueValue ngTrueValue;
296296
final NgFalseValue ngFalseValue;
297+
final NgModelOptions ngModelOptions;
297298
final Scope scope;
298299

299300
InputCheckbox(dom.Element this.inputElement, this.ngModel,
300-
this.scope, this.ngTrueValue, this.ngFalseValue) {
301+
this.scope, this.ngTrueValue, this.ngFalseValue, this.ngModelOptions) {
301302
ngModel.render = (value) {
302303
scope.rootScope.domWrite(() {
303304
inputElement.checked = ngTrueValue.isValue(value);
304305
});
305306
};
306307
inputElement
307-
..onChange.listen((_) {
308-
ngModel.viewValue = inputElement.checked
309-
? ngTrueValue.value : ngFalseValue.value;
310-
})
311-
..onBlur.listen((e) {
308+
..onChange.listen((_) => ngModelOptions.executeChangeFunc(() {
309+
ngModel.viewValue = inputElement.checked ? ngTrueValue.value : ngFalseValue.value;
310+
}))
311+
..onBlur.listen((_) => ngModelOptions.executeBlurFunc(() {
312312
ngModel.markAsTouched();
313-
});
313+
}));
314314
}
315315
}
316316

317+
318+
319+
317320
/**
318321
* Usage:
319322
*
@@ -337,37 +340,42 @@ class InputCheckbox {
337340
class InputTextLike {
338341
final dom.Element inputElement;
339342
final NgModel ngModel;
343+
final NgModelOptions ngModelOptions;
340344
final Scope scope;
341345
String _inputType;
342346

347+
343348
get typedValue => (inputElement as dynamic).value;
344349
void set typedValue(value) {
345350
(inputElement as dynamic).value = (value == null) ? '' : value.toString();
346351
}
347352

348-
InputTextLike(this.inputElement, this.ngModel, this.scope) {
353+
InputTextLike(this.inputElement, this.ngModel, this.scope, this.ngModelOptions) {
349354
ngModel.render = (value) {
350355
scope.rootScope.domWrite(() {
351356
if (value == null) value = '';
352357

353358
var currentValue = typedValue;
354359
if (value != currentValue && !(value is num && currentValue is num &&
355360
value.isNaN && currentValue.isNaN)) {
356-
typedValue = value;
361+
typedValue = value;
357362
}
358363
});
359364
};
365+
360366
inputElement
361-
..onChange.listen(processValue)
362-
..onInput.listen(processValue)
363-
..onBlur.listen((e) {
367+
..onChange.listen((event) => ngModelOptions.executeChangeFunc(() => processValue(event)))
368+
..onInput.listen((event) => ngModelOptions.executeInputFunc(() => processValue(event)))
369+
..onBlur.listen((_) => ngModelOptions.executeBlurFunc(() {
364370
ngModel.markAsTouched();
365-
});
371+
}));
366372
}
367373

368374
void processValue([_]) {
369375
var value = typedValue;
376+
370377
if (value != ngModel.viewValue) ngModel.viewValue = value;
378+
371379
ngModel.validate();
372380
}
373381
}
@@ -394,6 +402,7 @@ class InputTextLike {
394402
class InputNumberLike {
395403
final dom.InputElement inputElement;
396404
final NgModel ngModel;
405+
final NgModelOptions ngModelOptions;
397406
final Scope scope;
398407

399408

@@ -414,7 +423,7 @@ class InputNumberLike {
414423
}
415424
}
416425

417-
InputNumberLike(dom.Element this.inputElement, this.ngModel, this.scope) {
426+
InputNumberLike(dom.Element this.inputElement, this.ngModel, this.scope, this.ngModelOptions) {
418427
ngModel.render = (value) {
419428
scope.rootScope.domWrite(() {
420429
if (value != typedValue
@@ -424,11 +433,11 @@ class InputNumberLike {
424433
});
425434
};
426435
inputElement
427-
..onChange.listen(relaxFnArgs(processValue))
428-
..onInput.listen(relaxFnArgs(processValue))
429-
..onBlur.listen((e) {
436+
..onChange.listen((event) => ngModelOptions.executeChangeFunc(() => processValue()))
437+
..onInput.listen((event) => ngModelOptions.executeInputFunc(() => processValue()))
438+
..onBlur.listen((_) => ngModelOptions.executeBlurFunc(() {
430439
ngModel.markAsTouched();
431-
});
440+
}));
432441
}
433442

434443
void processValue() {
@@ -586,11 +595,12 @@ class InputDateLike {
586595
toFactory: (Injector i) => new NgBindTypeForDateLike(i.get(dom.Element)));
587596
final dom.InputElement inputElement;
588597
final NgModel ngModel;
598+
final NgModelOptions ngModelOptions;
589599
final Scope scope;
590600
NgBindTypeForDateLike ngBindType;
591601

592602
InputDateLike(dom.Element this.inputElement, this.ngModel, this.scope,
593-
this.ngBindType) {
603+
this.ngBindType, this.ngModelOptions) {
594604
if (inputElement.type == 'datetime-local') {
595605
ngBindType.idlAttrKind = NgBindTypeForDateLike.NUMBER;
596606
}
@@ -600,11 +610,11 @@ class InputDateLike {
600610
});
601611
};
602612
inputElement
603-
..onChange.listen(relaxFnArgs(processValue))
604-
..onInput.listen(relaxFnArgs(processValue))
605-
..onBlur.listen((e) {
613+
..onChange.listen((event) => ngModelOptions.executeChangeFunc(() => processValue()))
614+
..onInput.listen((event) => ngModelOptions.executeInputFunc(() => processValue()))
615+
..onBlur.listen((_) => ngModelOptions.executeBlurFunc(() {
606616
ngModel.markAsTouched();
607-
});
617+
}));
608618
}
609619

610620
dynamic get typedValue => ngBindType.inputTypedValue;
@@ -680,7 +690,9 @@ class NgValue {
680690
NgValue(this.element);
681691

682692
@NgOneWay('ng-value')
683-
void set value(val) { this._value = val; }
693+
void set value(val) {
694+
this._value = val;
695+
}
684696
dynamic get value => _value == null ? (element as dynamic).value : _value;
685697
}
686698

@@ -767,7 +779,7 @@ class InputRadio {
767779
..onClick.listen((_) {
768780
if (radioButtonElement.checked) ngModel.viewValue = ngValue.value;
769781
})
770-
..onBlur.listen((e) {
782+
..onBlur.listen((event) {
771783
ngModel.markAsTouched();
772784
});
773785
}
@@ -785,8 +797,8 @@ class InputRadio {
785797
*/
786798
@Decorator(selector: '[contenteditable][ng-model]')
787799
class ContentEditable extends InputTextLike {
788-
ContentEditable(dom.Element inputElement, NgModel ngModel, Scope scope)
789-
: super(inputElement, ngModel, scope);
800+
ContentEditable(dom.Element inputElement, NgModel ngModel, Scope scope, NgModelOptions modelOptions)
801+
: super(inputElement, ngModel, scope, modelOptions);
790802

791803
// The implementation is identical to InputTextLike but use innerHtml instead of value
792804
String get typedValue => (inputElement as dynamic).innerHtml;

lib/directive/ng_model_options.dart

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
part of angular.directive;
2+
3+
@Decorator(
4+
selector: 'input[ng-model-options]',
5+
map: const {'ng-model-options': '=>options'})
6+
class NgModelOptions {
7+
static const String _DEBOUNCE_DEFAULT_KEY = "default";
8+
static const String _DEBOUNCE_BLUR_KEY = "blur";
9+
static const String _DEBOUNCE_CHANGE_KEY = "change";
10+
static const String _DEBOUNCE_INPUT_KEY = "input";
11+
12+
int _debounceDefaultValue = 0;
13+
int _debounceBlurValue;
14+
int _debounceChangeValue;
15+
int _debounceInputValue;
16+
17+
async.Timer _blurTimer;
18+
async.Timer _changeTimer;
19+
async.Timer _inputTimer;
20+
21+
NgModelOptions();
22+
23+
void set options(options) {
24+
if (options["debounce"] is int){
25+
_debounceDefaultValue = options["debounce"];
26+
} else {
27+
Map debounceOptions = options["debounce"];
28+
if (debounceOptions.containsKey(_DEBOUNCE_DEFAULT_KEY)){
29+
_debounceDefaultValue = debounceOptions[_DEBOUNCE_DEFAULT_KEY];
30+
}
31+
_debounceBlurValue = debounceOptions[_DEBOUNCE_BLUR_KEY];
32+
_debounceChangeValue = debounceOptions[_DEBOUNCE_CHANGE_KEY];
33+
_debounceInputValue = debounceOptions[_DEBOUNCE_INPUT_KEY];
34+
}
35+
}
36+
37+
void executeBlurFunc(func()) {
38+
var delay = _debounceBlurValue == null ? _debounceDefaultValue : _debounceBlurValue;
39+
_blurTimer = _runFuncDebounced(delay, func, _blurTimer);
40+
}
41+
42+
void executeChangeFunc(func()) {
43+
var delay = _debounceChangeValue == null ? _debounceDefaultValue : _debounceChangeValue;
44+
_changeTimer = _runFuncDebounced(delay, func, _changeTimer);
45+
}
46+
47+
void executeInputFunc(func()) {
48+
var delay = _debounceInputValue == null ? _debounceDefaultValue : _debounceInputValue;
49+
_inputTimer = _runFuncDebounced(delay, func, _inputTimer);
50+
}
51+
52+
async.Timer _runFuncDebounced(int delay, func(), async.Timer timer){
53+
if (timer != null && timer.isActive) timer.cancel();
54+
55+
if (delay == 0){
56+
func();
57+
return null;
58+
} else {
59+
return new async.Timer(new Duration(milliseconds: delay), func);
60+
}
61+
}
62+
}

test/angular_spec.dart

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ main() {
182182
"angular.directive.NgIf",
183183
"angular.directive.NgInclude",
184184
"angular.directive.NgModel",
185+
"angular.directive.NgModelOptions",
185186
"angular.directive.NgModelConverter",
186187
"angular.directive.NgModelEmailValidator",
187188
"angular.directive.NgModelMaxLengthValidator",

0 commit comments

Comments
 (0)