From e95b481a53191582bca635f322ad07eadbd62d64 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 9 Jun 2020 11:54:40 -0400 Subject: [PATCH] fix(angular): patch FormControl methods to properly sync Ionic form classes (#21429) Co-authored-by: Mark Levy --- .../boolean-value-accessor.ts | 6 +-- .../numeric-value-accesssor.ts | 6 +-- .../radio-value-accessor.ts | 6 +-- .../select-value-accessor.ts | 6 +-- .../text-value-accessor.ts | 6 +-- .../control-value-accessors/value-accessor.ts | 49 +++++++++++++++++-- .../test/test-app/e2e/src/form.e2e-spec.ts | 16 +++++- .../test-app/src/app/form/form.component.html | 4 +- .../test-app/src/app/form/form.component.ts | 5 ++ 9 files changed, 83 insertions(+), 21 deletions(-) diff --git a/angular/src/directives/control-value-accessors/boolean-value-accessor.ts b/angular/src/directives/control-value-accessors/boolean-value-accessor.ts index 74d1d5066ad..50605e2de04 100644 --- a/angular/src/directives/control-value-accessors/boolean-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/boolean-value-accessor.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, HostListener } from '@angular/core'; +import { Directive, ElementRef, HostListener, Injector } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor, setIonicClasses } from './value-accessor'; @@ -16,8 +16,8 @@ import { ValueAccessor, setIonicClasses } from './value-accessor'; }) export class BooleanValueAccessor extends ValueAccessor { - constructor(el: ElementRef) { - super(el); + constructor(injector: Injector, el: ElementRef) { + super(injector, el); } writeValue(value: any) { diff --git a/angular/src/directives/control-value-accessors/numeric-value-accesssor.ts b/angular/src/directives/control-value-accessors/numeric-value-accesssor.ts index 7f667eb73a0..4f3f60ad57d 100644 --- a/angular/src/directives/control-value-accessors/numeric-value-accesssor.ts +++ b/angular/src/directives/control-value-accessors/numeric-value-accesssor.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, HostListener } from '@angular/core'; +import { Directive, ElementRef, HostListener, Injector } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from './value-accessor'; @@ -16,8 +16,8 @@ import { ValueAccessor } from './value-accessor'; }) export class NumericValueAccessor extends ValueAccessor { - constructor(el: ElementRef) { - super(el); + constructor(injector: Injector, el: ElementRef) { + super(injector, el); } @HostListener('ionChange', ['$event.target']) diff --git a/angular/src/directives/control-value-accessors/radio-value-accessor.ts b/angular/src/directives/control-value-accessors/radio-value-accessor.ts index ae68034ba3f..465569d36f1 100644 --- a/angular/src/directives/control-value-accessors/radio-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/radio-value-accessor.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, HostListener } from '@angular/core'; +import { Directive, ElementRef, HostListener, Injector } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from './value-accessor'; @@ -16,8 +16,8 @@ import { ValueAccessor } from './value-accessor'; }) export class RadioValueAccessor extends ValueAccessor { - constructor(el: ElementRef) { - super(el); + constructor(injector: Injector, el: ElementRef) { + super(injector, el); } @HostListener('ionSelect', ['$event.target']) diff --git a/angular/src/directives/control-value-accessors/select-value-accessor.ts b/angular/src/directives/control-value-accessors/select-value-accessor.ts index fddc1644858..d63f78bbbcd 100644 --- a/angular/src/directives/control-value-accessors/select-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/select-value-accessor.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, HostListener } from '@angular/core'; +import { Directive, ElementRef, HostListener, Injector } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from './value-accessor'; @@ -16,8 +16,8 @@ import { ValueAccessor } from './value-accessor'; }) export class SelectValueAccessor extends ValueAccessor { - constructor(el: ElementRef) { - super(el); + constructor(injector: Injector, el: ElementRef) { + super(injector, el); } @HostListener('ionChange', ['$event.target']) diff --git a/angular/src/directives/control-value-accessors/text-value-accessor.ts b/angular/src/directives/control-value-accessors/text-value-accessor.ts index 7981a60510a..5882f3f73d4 100644 --- a/angular/src/directives/control-value-accessors/text-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/text-value-accessor.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, HostListener } from '@angular/core'; +import { Directive, ElementRef, HostListener, Injector } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from './value-accessor'; @@ -16,8 +16,8 @@ import { ValueAccessor } from './value-accessor'; }) export class TextValueAccessor extends ValueAccessor { - constructor(el: ElementRef) { - super(el); + constructor(injector: Injector, el: ElementRef) { + super(injector, el); } @HostListener('ionChange', ['$event.target']) diff --git a/angular/src/directives/control-value-accessors/value-accessor.ts b/angular/src/directives/control-value-accessors/value-accessor.ts index ca159d301c9..e7f3effaa1f 100644 --- a/angular/src/directives/control-value-accessors/value-accessor.ts +++ b/angular/src/directives/control-value-accessors/value-accessor.ts @@ -1,15 +1,17 @@ -import { ElementRef, HostListener } from '@angular/core'; -import { ControlValueAccessor } from '@angular/forms'; +import { AfterViewInit, ElementRef, HostListener, Injector, OnDestroy, Type } from '@angular/core'; +import { ControlValueAccessor, NgControl } from '@angular/forms'; +import { Subscription } from 'rxjs'; import { raf } from '../../util/util'; -export class ValueAccessor implements ControlValueAccessor { +export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDestroy { private onChange: (value: any) => void = () => {/**/}; private onTouched: () => void = () => {/**/}; protected lastValue: any; + private statusChanges?: Subscription; - constructor(protected el: ElementRef) {} + constructor(protected injector: Injector, protected el: ElementRef) {} writeValue(value: any) { /** @@ -52,6 +54,45 @@ export class ValueAccessor implements ControlValueAccessor { setDisabledState(isDisabled: boolean) { this.el.nativeElement.disabled = isDisabled; } + + ngOnDestroy() { + if (this.statusChanges) { + this.statusChanges.unsubscribe(); + } + } + + ngAfterViewInit() { + const ngControl = this.injector.get(NgControl as Type); + + // Listen for changes in validity, disabled, or pending states + if (ngControl.statusChanges) { + this.statusChanges = ngControl.statusChanges.subscribe(() => setIonicClasses(this.el)); + } + + /** + * TODO Remove this in favor of https://github.com/angular/angular/issues/10887 + * whenever it is implemented. Currently, Ionic's form status classes + * do not react to changes when developers manually call + * Angular form control methods such as markAsTouched. + * This results in Ionic's form status classes being out + * of sync with the ng form status classes. + * This patches the methods to manually sync + * the classes until this feature is implemented in Angular. + */ + const formControl = ngControl.control; + if (formControl) { + const methodsToPatch = ['markAsTouched', 'markAllAsTouched', 'markAsUntouched', 'markAsDirty', 'markAsPristine']; + methodsToPatch.forEach(method => { + if (formControl[method]) { + const oldFn = formControl[method].bind(formControl); + formControl[method] = (...params) => { + oldFn(...params); + setIonicClasses(this.el); + }; + } + }); + } + } } export const setIonicClasses = (element: ElementRef) => { diff --git a/angular/test/test-app/e2e/src/form.e2e-spec.ts b/angular/test/test-app/e2e/src/form.e2e-spec.ts index 3c26bd9d7ec..7a1540c512a 100644 --- a/angular/test/test-app/e2e/src/form.e2e-spec.ts +++ b/angular/test/test-app/e2e/src/form.e2e-spec.ts @@ -1,5 +1,5 @@ import { browser, element, by } from 'protractor'; -import { handleErrorMessages, setProperty, getText, waitTime } from './utils'; +import { handleErrorMessages, getProperty, setProperty, getText, waitTime } from './utils'; describe('form', () => { @@ -7,6 +7,20 @@ describe('form', () => { return handleErrorMessages(); }); + describe('status updates', () => { + beforeEach(async () => { + await browser.get('/form'); + await waitTime(30); + }); + + it('should update Ionic form classes when calling form methods programatically', async () => { + await element(by.css('form #input-touched')).click(); + await waitTime(100); + const classList = (await getProperty('#touched-input-test', 'classList')) as string[]; + expect(classList.includes('ion-touched')).toEqual(true); + }); + }); + describe('change', () => { beforeEach(async () => { await browser.get('/form'); diff --git a/angular/test/test-app/src/app/form/form.component.html b/angular/test/test-app/src/app/form/form.component.html index 09b1bd234b3..0de47dae90a 100644 --- a/angular/test/test-app/src/app/form/form.component.html +++ b/angular/test/test-app/src/app/form/form.component.html @@ -35,9 +35,11 @@ Input (required) - + + Set Input Touched + Input diff --git a/angular/test/test-app/src/app/form/form.component.ts b/angular/test/test-app/src/app/form/form.component.ts index 2d5058e4902..6f9345330b4 100644 --- a/angular/test/test-app/src/app/form/form.component.ts +++ b/angular/test/test-app/src/app/form/form.component.ts @@ -25,6 +25,11 @@ export class FormComponent { }); } + setTouched() { + const formControl = this.profileForm.get('input'); + formControl.markAsTouched(); + } + onSubmit(_ev) { this.submitted = 'true'; }