diff --git a/angular_forms/CHANGELOG.md b/angular_forms/CHANGELOG.md index 9575c902a9..83bed761fc 100644 --- a/angular_forms/CHANGELOG.md +++ b/angular_forms/CHANGELOG.md @@ -1,5 +1,9 @@ ## 2.0.0-alpha+3 +### New Features + +* Add `ngDisabled` input to all Control directives. + ### Breaking Changes * `NgControlGroup` can no longer be injected directly. It can still be diff --git a/angular_forms/lib/src/directives/abstract_control_directive.dart b/angular_forms/lib/src/directives/abstract_control_directive.dart index 4454a917a1..fa5ad54c05 100644 --- a/angular_forms/lib/src/directives/abstract_control_directive.dart +++ b/angular_forms/lib/src/directives/abstract_control_directive.dart @@ -12,6 +12,10 @@ abstract class AbstractControlDirective { bool get valid => control?.valid; + bool get disabled => control?.disabled; + + bool get enabled => control?.enabled; + Map get errors => control?.errors; bool get pristine => control?.pristine; @@ -23,4 +27,9 @@ abstract class AbstractControlDirective { bool get untouched => control?.untouched; List get path => null; + + void toggleDisabled(bool isDisabled) { + if (isDisabled && !control.disabled) control.markAsDisabled(); + if (!isDisabled && !control.enabled) control.markAsEnabled(); + } } diff --git a/angular_forms/lib/src/directives/checkbox_value_accessor.dart b/angular_forms/lib/src/directives/checkbox_value_accessor.dart index a8bffce988..fe1059e6f9 100644 --- a/angular_forms/lib/src/directives/checkbox_value_accessor.dart +++ b/angular_forms/lib/src/directives/checkbox_value_accessor.dart @@ -42,5 +42,7 @@ class CheckboxControlValueAccessor extends Object } @override - void onDisabledChanged(bool isDisabled) {} + void onDisabledChanged(bool isDisabled) { + _element.disabled = isDisabled; + } } diff --git a/angular_forms/lib/src/directives/default_value_accessor.dart b/angular_forms/lib/src/directives/default_value_accessor.dart index 0acfa36452..c7754f20d4 100644 --- a/angular_forms/lib/src/directives/default_value_accessor.dart +++ b/angular_forms/lib/src/directives/default_value_accessor.dart @@ -2,6 +2,8 @@ import 'dart:html'; import 'dart:js_util' as js_util; import 'package:angular/angular.dart'; +import 'package:angular_forms/src/directives/shared.dart' + show setElementDisabled; import 'control_value_accessor.dart'; @@ -43,5 +45,7 @@ class DefaultValueAccessor extends Object } @override - void onDisabledChanged(bool isDisabled) {} + void onDisabledChanged(bool isDisabled) { + setElementDisabled(_element, isDisabled); + } } diff --git a/angular_forms/lib/src/directives/ng_control_group.dart b/angular_forms/lib/src/directives/ng_control_group.dart index b175c586c6..204c7fb7ff 100644 --- a/angular_forms/lib/src/directives/ng_control_group.dart +++ b/angular_forms/lib/src/directives/ng_control_group.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:angular/angular.dart'; import '../model.dart' show ControlGroup; @@ -62,6 +64,9 @@ class NgControlGroup extends ControlContainer implements OnInit, OnDestroy { final ValidatorFn validator; final ControlContainer _parent; + bool _isDisabled = false; + bool _disabledChanged = false; + NgControlGroup(@SkipSelf() this._parent, @Optional() @Self() @Inject(NG_VALIDATORS) List validators) : validator = composeValidators(validators); @@ -72,9 +77,26 @@ class NgControlGroup extends ControlContainer implements OnInit, OnDestroy { super.name = value; } + @Input('ngDisabled') + set disabled(bool isDisabled) { + _isDisabled = isDisabled; + if (control != null) { + _disabledChanged = false; + toggleDisabled(isDisabled); + } else { + _disabledChanged = true; + } + } + @override void ngOnInit() { formDirective.addControlGroup(this); + if (_disabledChanged) { + scheduleMicrotask(() { + _disabledChanged = false; + toggleDisabled(_isDisabled); + }); + } } @override diff --git a/angular_forms/lib/src/directives/ng_control_name.dart b/angular_forms/lib/src/directives/ng_control_name.dart index e5ab3916a9..c05c070730 100644 --- a/angular_forms/lib/src/directives/ng_control_name.dart +++ b/angular_forms/lib/src/directives/ng_control_name.dart @@ -74,6 +74,7 @@ import 'shared.dart' show controlPath; class NgControlName extends NgControl implements AfterChanges, OnDestroy { final ControlContainer _parent; final _update = new StreamController.broadcast(); + bool _modelChanged = false; dynamic _model; @Input('ngModel') @@ -86,6 +87,9 @@ class NgControlName extends NgControl implements AfterChanges, OnDestroy { dynamic viewModel; var _added = false; + bool _isDisabled = false; + bool _disabledChanged = false; + NgControlName( @SkipSelf() this._parent, @@ -105,6 +109,12 @@ class NgControlName extends NgControl implements AfterChanges, OnDestroy { super.name = value; } + @Input('ngDisabled') + set disabled(bool isDisabled) { + _isDisabled = isDisabled; + _disabledChanged = true; + } + @Output('ngModelChange') Stream get update => _update.stream; @@ -121,6 +131,12 @@ class NgControlName extends NgControl implements AfterChanges, OnDestroy { formDirective.updateModel(this, _model); } } + if (_disabledChanged) { + scheduleMicrotask(() { + _disabledChanged = false; + toggleDisabled(_isDisabled); + }); + } } @override diff --git a/angular_forms/lib/src/directives/ng_form.dart b/angular_forms/lib/src/directives/ng_form.dart index 47e6a109ce..c8ab5afb57 100644 --- a/angular_forms/lib/src/directives/ng_form.dart +++ b/angular_forms/lib/src/directives/ng_form.dart @@ -81,6 +81,11 @@ class NgForm extends AbstractForm { form = new ControlGroup({}, composeValidators(validators)); } + @Input('ngDisabled') + set disabled(bool isDisabled) { + toggleDisabled(isDisabled); + } + Map get controls => form.controls; @override diff --git a/angular_forms/lib/src/directives/ng_model.dart b/angular_forms/lib/src/directives/ng_model.dart index fe70b43a34..a8e28ae1b5 100644 --- a/angular_forms/lib/src/directives/ng_model.dart +++ b/angular_forms/lib/src/directives/ng_model.dart @@ -91,6 +91,13 @@ class NgModel extends NgControl _init(valueAccessors); } + @Input('ngDisabled') + set disabled(bool isDisabled) { + setState(() { + toggleDisabled(isDisabled); + }); + } + // This function prevents constructor inlining for smaller code size since // NgModel is constructed for majority of form components. void _init(List valueAccessors) { diff --git a/angular_forms/lib/src/directives/number_value_accessor.dart b/angular_forms/lib/src/directives/number_value_accessor.dart index c96e7754ac..d264ad0bda 100644 --- a/angular_forms/lib/src/directives/number_value_accessor.dart +++ b/angular_forms/lib/src/directives/number_value_accessor.dart @@ -41,5 +41,7 @@ class NumberValueAccessor extends Object } @override - void onDisabledChanged(bool isDisabled) {} + void onDisabledChanged(bool isDisabled) { + _element.disabled = isDisabled; + } } diff --git a/angular_forms/lib/src/directives/radio_control_value_accessor.dart b/angular_forms/lib/src/directives/radio_control_value_accessor.dart index c28a54e60e..e001d413f6 100644 --- a/angular_forms/lib/src/directives/radio_control_value_accessor.dart +++ b/angular_forms/lib/src/directives/radio_control_value_accessor.dart @@ -2,6 +2,8 @@ import 'dart:html'; import 'dart:js_util' as js_util; import 'package:angular/angular.dart'; +import 'package:angular_forms/src/directives/shared.dart' + show setElementDisabled; import 'control_value_accessor.dart' show ChangeHandler, ControlValueAccessor, NG_VALUE_ACCESSOR, TouchHandler; @@ -116,5 +118,7 @@ class RadioControlValueAccessor extends Object } @override - void onDisabledChanged(bool isDisabled) {} + void onDisabledChanged(bool isDisabled) { + setElementDisabled(_element, isDisabled); + } } diff --git a/angular_forms/lib/src/directives/select_control_value_accessor.dart b/angular_forms/lib/src/directives/select_control_value_accessor.dart index a44df80a21..018c878007 100644 --- a/angular_forms/lib/src/directives/select_control_value_accessor.dart +++ b/angular_forms/lib/src/directives/select_control_value_accessor.dart @@ -61,7 +61,9 @@ class SelectControlValueAccessor extends Object } @override - void onDisabledChanged(bool isDisabled) {} + void onDisabledChanged(bool isDisabled) { + _element.disabled = isDisabled; + } String _registerOption() => (_idCounter++).toString(); diff --git a/angular_forms/lib/src/directives/shared.dart b/angular_forms/lib/src/directives/shared.dart index 8ab0f56457..f4ed5dc1f3 100644 --- a/angular_forms/lib/src/directives/shared.dart +++ b/angular_forms/lib/src/directives/shared.dart @@ -1,3 +1,6 @@ +import 'dart:html'; +import 'dart:js_util' as js_util; + import '../model.dart' show Control, ControlGroup; import '../validators.dart' show Validators; import 'abstract_control_directive.dart' show AbstractControlDirective; @@ -35,6 +38,7 @@ void setUpControl(Control control, NgControl dir) { // model -> view control.registerOnChange( (dynamic newValue) => dir.valueAccessor?.writeValue(newValue)); + control.disabledChanges.listen(dir.valueAccessor?.onDisabledChanged); // touched dir.valueAccessor.registerOnTouched(() => control.markAsTouched()); } @@ -86,3 +90,7 @@ ControlValueAccessor selectValueAccessor( _throwError(null, 'No valid value accessor for'); return null; } + +void setElementDisabled(HtmlElement element, bool isDisabled) { + js_util.setProperty(element, 'disabled', isDisabled); +} diff --git a/angular_forms/lib/src/model.dart b/angular_forms/lib/src/model.dart index 05d0964c63..a7ecd424e5 100644 --- a/angular_forms/lib/src/model.dart +++ b/angular_forms/lib/src/model.dart @@ -39,6 +39,7 @@ abstract class AbstractControl { T _value; final _valueChanges = new StreamController.broadcast(); final _statusChanges = new StreamController.broadcast(); + final _disabledChanges = new StreamController.broadcast(); String _status; Map _errors; bool _pristine = true; @@ -77,6 +78,8 @@ abstract class AbstractControl { Stream get statusChanges => _statusChanges.stream; + Stream get disabledChanges => _disabledChanges.stream; + bool get pending => _status == PENDING; void markAsTouched() { @@ -126,6 +129,7 @@ abstract class AbstractControl { if (emitEvent) _emitEvent(); _updateAncestors(updateParent: updateParent, emitEvent: emitEvent); + _disabledChanges.add(true); } /// Enables the control. This means the control will be included in @@ -143,6 +147,7 @@ abstract class AbstractControl { (c) => c.markAsEnabled(updateParent: false, emitEvent: emitEvent)); updateValueAndValidity(onlySelf: true, emitEvent: emitEvent); _updateAncestors(updateParent: updateParent, emitEvent: emitEvent); + _disabledChanges.add(false); } void _updateAncestors({bool updateParent, bool emitEvent}) { diff --git a/angular_forms/test/model_test.dart b/angular_forms/test/model_test.dart index 5ec535f21a..c79d6dd03b 100644 --- a/angular_forms/test/model_test.dart +++ b/angular_forms/test/model_test.dart @@ -379,6 +379,15 @@ void main() { expect(control.disabled, false); }); + test('should update nested children', () { + var childControl = new Control(); + group.addControl('nested', new ControlGroup({'child': childControl})); + group.markAsDisabled(); + expect(childControl.disabled, true); + group.markAsEnabled(); + expect(childControl.disabled, false); + }); + test('should handle empty ControlGroup', () { var emptyGroup = new ControlGroup({}); expect(emptyGroup.disabled, false); diff --git a/angular_forms/test/ng_control_group_test.dart b/angular_forms/test/ng_control_group_test.dart index 95d7979078..8e3c04c211 100644 --- a/angular_forms/test/ng_control_group_test.dart +++ b/angular_forms/test/ng_control_group_test.dart @@ -1,3 +1,5 @@ +import 'dart:html'; + @TestOn('browser') import 'package:test/test.dart'; import 'package:angular/angular.dart'; @@ -31,6 +33,17 @@ void main() { expect(cmp.controlGroup.untouched, cmp.groupModel.untouched); }); }); + + test('should disable child controls', () async { + await fixture.update((cmp) { + cmp.disabled = true; + }); + expect(fixture.assertOnlyInstance.inputElement.disabled, true); + await fixture.update((cmp) { + cmp.disabled = false; + }); + expect(fixture.assertOnlyInstance.inputElement.disabled, false); + }); }); } @@ -42,8 +55,8 @@ void main() { ], template: '''
-
- +
+
''', @@ -52,6 +65,11 @@ class NgControlGroupTest { @ViewChild('controlGroup') NgControlGroup controlGroup; + @ViewChild('input') + InputElement inputElement; + + bool disabled = false; + ControlGroup formModel = FormBuilder.controlGroup({ 'group': FormBuilder.controlGroup({'login': new Control(null)}) }); diff --git a/angular_forms/test/ng_control_name_test.dart b/angular_forms/test/ng_control_name_test.dart index 95a2951e2e..5659eff135 100644 --- a/angular_forms/test/ng_control_name_test.dart +++ b/angular_forms/test/ng_control_name_test.dart @@ -1,3 +1,5 @@ +import 'dart:html'; + @TestOn('browser') import 'package:test/test.dart'; import 'package:angular/angular.dart'; @@ -31,6 +33,14 @@ void main() { expect(cmp.controlName.untouched, cmp.controlModel.untouched); }); }); + + test('should disabled element', () async { + expect(fixture.assertOnlyInstance.inputElement.disabled, false); + await fixture.update((cmp) => cmp.disabled = true); + expect(fixture.assertOnlyInstance.inputElement.disabled, true); + await fixture.update((cmp) => cmp.disabled = false); + expect(fixture.assertOnlyInstance.inputElement.disabled, false); + }); }); } @@ -41,7 +51,12 @@ void main() { ], template: '''
- +
''', ) @@ -49,9 +64,14 @@ class NgControlNameTest { @ViewChild('login') NgControlName controlName; + @ViewChild('input') + InputElement inputElement; + String loginValue; ControlGroup formModel = new ControlGroup({'login': new Control('login')}); + bool disabled = false; + Control get controlModel => formModel.controls['login']; } diff --git a/angular_forms/test/ng_form_control_test.dart b/angular_forms/test/ng_form_control_test.dart index c18016c218..13f3ee82a9 100644 --- a/angular_forms/test/ng_form_control_test.dart +++ b/angular_forms/test/ng_form_control_test.dart @@ -1,3 +1,5 @@ +import 'dart:html'; + @TestOn('browser') import 'package:test/test.dart'; import 'package:angular/angular.dart'; @@ -52,6 +54,14 @@ void main() { expect(cmp.loginControl.valid, false); }); }); + + test('should disable element', () async { + expect(fixture.assertOnlyInstance.inputElement.disabled, false); + await fixture.update((cmp) => cmp.loginControl.markAsDisabled()); + expect(fixture.assertOnlyInstance.inputElement.disabled, true); + await fixture.update((cmp) => cmp.loginControl.markAsEnabled()); + expect(fixture.assertOnlyInstance.inputElement.disabled, false); + }); }); } @@ -62,7 +72,7 @@ void main() { ], template: '''
- +
''', ) @@ -70,5 +80,8 @@ class NgFormControlTest { @ViewChild('login') NgFormControl formControl; + @ViewChild('input') + InputElement inputElement; + Control loginControl = new Control(null); } diff --git a/angular_forms/test/ng_form_test.dart b/angular_forms/test/ng_form_test.dart index d0e877bbfa..e1c4b08155 100644 --- a/angular_forms/test/ng_form_test.dart +++ b/angular_forms/test/ng_form_test.dart @@ -1,3 +1,5 @@ +import 'dart:html'; + @TestOn('browser') import 'package:test/test.dart'; import 'package:angular/angular.dart'; @@ -59,6 +61,18 @@ void main() { var f = new NgForm([formValidator]); expect(f.form.errors, {'custom': true}); }); + + test('should disable child elements', () async { + var readonlyComponent = fixture.assertOnlyInstance; + expect(readonlyComponent.inputElement.disabled, false); + expect(readonlyComponent.loginControlDir.disabled, false); + await fixture.update((cmp) => cmp.disabled = true); + expect(readonlyComponent.inputElement.disabled, true); + expect(readonlyComponent.loginControlDir.disabled, true); + await fixture.update((cmp) => cmp.disabled = false); + expect(readonlyComponent.inputElement.disabled, false); + expect(readonlyComponent.loginControlDir.disabled, false); + }); }); } @@ -70,9 +84,10 @@ void main() { NgIf, ], template: ''' -
+
+
''', @@ -84,6 +99,10 @@ class NgFormTest { @ViewChild('login') NgControlName loginControlDir; + @ViewChild('input') + InputElement inputElement; + + bool disabled = false; bool needsLogin = true; ControlGroup get formModel => form.form;