mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 01:52:19 +08:00
fix(angular): patch FormControl methods to properly sync Ionic form classes (#21429)
Co-authored-by: Mark Levy <MarkChrisLevy@users.noreply.github.com>
This commit is contained in:
@ -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 { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
import { ValueAccessor, setIonicClasses } from './value-accessor';
|
import { ValueAccessor, setIonicClasses } from './value-accessor';
|
||||||
@ -16,8 +16,8 @@ import { ValueAccessor, setIonicClasses } from './value-accessor';
|
|||||||
})
|
})
|
||||||
export class BooleanValueAccessor extends ValueAccessor {
|
export class BooleanValueAccessor extends ValueAccessor {
|
||||||
|
|
||||||
constructor(el: ElementRef) {
|
constructor(injector: Injector, el: ElementRef) {
|
||||||
super(el);
|
super(injector, el);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeValue(value: any) {
|
writeValue(value: any) {
|
||||||
|
@ -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 { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
import { ValueAccessor } from './value-accessor';
|
import { ValueAccessor } from './value-accessor';
|
||||||
@ -16,8 +16,8 @@ import { ValueAccessor } from './value-accessor';
|
|||||||
})
|
})
|
||||||
export class NumericValueAccessor extends ValueAccessor {
|
export class NumericValueAccessor extends ValueAccessor {
|
||||||
|
|
||||||
constructor(el: ElementRef) {
|
constructor(injector: Injector, el: ElementRef) {
|
||||||
super(el);
|
super(injector, el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('ionChange', ['$event.target'])
|
@HostListener('ionChange', ['$event.target'])
|
||||||
|
@ -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 { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
import { ValueAccessor } from './value-accessor';
|
import { ValueAccessor } from './value-accessor';
|
||||||
@ -16,8 +16,8 @@ import { ValueAccessor } from './value-accessor';
|
|||||||
})
|
})
|
||||||
export class RadioValueAccessor extends ValueAccessor {
|
export class RadioValueAccessor extends ValueAccessor {
|
||||||
|
|
||||||
constructor(el: ElementRef) {
|
constructor(injector: Injector, el: ElementRef) {
|
||||||
super(el);
|
super(injector, el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('ionSelect', ['$event.target'])
|
@HostListener('ionSelect', ['$event.target'])
|
||||||
|
@ -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 { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
import { ValueAccessor } from './value-accessor';
|
import { ValueAccessor } from './value-accessor';
|
||||||
@ -16,8 +16,8 @@ import { ValueAccessor } from './value-accessor';
|
|||||||
})
|
})
|
||||||
export class SelectValueAccessor extends ValueAccessor {
|
export class SelectValueAccessor extends ValueAccessor {
|
||||||
|
|
||||||
constructor(el: ElementRef) {
|
constructor(injector: Injector, el: ElementRef) {
|
||||||
super(el);
|
super(injector, el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('ionChange', ['$event.target'])
|
@HostListener('ionChange', ['$event.target'])
|
||||||
|
@ -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 { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
import { ValueAccessor } from './value-accessor';
|
import { ValueAccessor } from './value-accessor';
|
||||||
@ -16,8 +16,8 @@ import { ValueAccessor } from './value-accessor';
|
|||||||
})
|
})
|
||||||
export class TextValueAccessor extends ValueAccessor {
|
export class TextValueAccessor extends ValueAccessor {
|
||||||
|
|
||||||
constructor(el: ElementRef) {
|
constructor(injector: Injector, el: ElementRef) {
|
||||||
super(el);
|
super(injector, el);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('ionChange', ['$event.target'])
|
@HostListener('ionChange', ['$event.target'])
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { ElementRef, HostListener } from '@angular/core';
|
import { AfterViewInit, ElementRef, HostListener, Injector, OnDestroy, Type } from '@angular/core';
|
||||||
import { ControlValueAccessor } from '@angular/forms';
|
import { ControlValueAccessor, NgControl } from '@angular/forms';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { raf } from '../../util/util';
|
import { raf } from '../../util/util';
|
||||||
|
|
||||||
export class ValueAccessor implements ControlValueAccessor {
|
export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
private onChange: (value: any) => void = () => {/**/};
|
private onChange: (value: any) => void = () => {/**/};
|
||||||
private onTouched: () => void = () => {/**/};
|
private onTouched: () => void = () => {/**/};
|
||||||
protected lastValue: any;
|
protected lastValue: any;
|
||||||
|
private statusChanges?: Subscription;
|
||||||
|
|
||||||
constructor(protected el: ElementRef) {}
|
constructor(protected injector: Injector, protected el: ElementRef) {}
|
||||||
|
|
||||||
writeValue(value: any) {
|
writeValue(value: any) {
|
||||||
/**
|
/**
|
||||||
@ -52,6 +54,45 @@ export class ValueAccessor implements ControlValueAccessor {
|
|||||||
setDisabledState(isDisabled: boolean) {
|
setDisabledState(isDisabled: boolean) {
|
||||||
this.el.nativeElement.disabled = isDisabled;
|
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) => {
|
export const setIonicClasses = (element: ElementRef) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
import { browser, element, by } from 'protractor';
|
||||||
import { handleErrorMessages, setProperty, getText, waitTime } from './utils';
|
import { handleErrorMessages, getProperty, setProperty, getText, waitTime } from './utils';
|
||||||
|
|
||||||
describe('form', () => {
|
describe('form', () => {
|
||||||
|
|
||||||
@ -7,6 +7,20 @@ describe('form', () => {
|
|||||||
return handleErrorMessages();
|
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', () => {
|
describe('change', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await browser.get('/form');
|
await browser.get('/form');
|
||||||
|
@ -35,9 +35,11 @@
|
|||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>Input (required)</ion-label>
|
<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-item>
|
||||||
|
|
||||||
|
<ion-button id="input-touched" (click)="setTouched()">Set Input Touched</ion-button>
|
||||||
|
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label>Input</ion-label>
|
<ion-label>Input</ion-label>
|
||||||
<ion-input formControlName="input2"></ion-input>
|
<ion-input formControlName="input2"></ion-input>
|
||||||
|
@ -25,6 +25,11 @@ export class FormComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTouched() {
|
||||||
|
const formControl = this.profileForm.get('input');
|
||||||
|
formControl.markAsTouched();
|
||||||
|
}
|
||||||
|
|
||||||
onSubmit(_ev) {
|
onSubmit(_ev) {
|
||||||
this.submitted = 'true';
|
this.submitted = 'true';
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user