Skip to content

Commit

Permalink
fix(angular): patch FormControl methods to properly sync Ionic form c…
Browse files Browse the repository at this point in the history
…lasses (#21429)

Co-authored-by: Mark Levy <[email protected]>
  • Loading branch information
liamdebeasi and MarkChrisLevy authored Jun 9, 2020
1 parent 698e526 commit e95b481
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'])
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'])
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'])
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'])
Expand Down
49 changes: 45 additions & 4 deletions angular/src/directives/control-value-accessors/value-accessor.ts
Original file line number Diff line number Diff line change
@@ -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) {
/**
Expand Down Expand Up @@ -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>(NgControl as Type<NgControl>);

// 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) => {
Expand Down
16 changes: 15 additions & 1 deletion angular/test/test-app/e2e/src/form.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { browser, element, by } from 'protractor';
import { handleErrorMessages, setProperty, getText, waitTime } from './utils';
import { handleErrorMessages, getProperty, setProperty, getText, waitTime } from './utils';

describe('form', () => {

afterEach(() => {
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');
Expand Down
4 changes: 3 additions & 1 deletion angular/test/test-app/src/app/form/form.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@

<ion-item>
<ion-label>Input (required)</ion-label>
<ion-input formControlName="input" class="required"></ion-input>
<ion-input formControlName="input" class="required" id="touched-input-test"></ion-input>
</ion-item>

<ion-button id="input-touched" (click)="setTouched()">Set Input Touched</ion-button>

<ion-item>
<ion-label>Input</ion-label>
<ion-input formControlName="input2"></ion-input>
Expand Down
5 changes: 5 additions & 0 deletions angular/test/test-app/src/app/form/form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export class FormComponent {
});
}

setTouched() {
const formControl = this.profileForm.get('input');
formControl.markAsTouched();
}

onSubmit(_ev) {
this.submitted = 'true';
}
Expand Down

0 comments on commit e95b481

Please sign in to comment.