mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(angular): standalone form controls can participate in forms (#28125)
Issue number: N/A --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Ionic standalone form controls cannot participate in Angular forms. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Ionic form controls can participate in Angular forms by importing the standalone component - Applies to: `ion-input`, `ion-textarea`, `ion-searchbar`, `ion-toggle`, `ion-checkbox`, `ion-segment`, `ion-radio`, `ion-radio-group`, `ion-datetime` and `ion-range`. - Refactors `ValueAccessor` from `@ionic/angular` to `@ionic/angular/common` - Refactors `raf` utility from `@ionic/angular` to `@ionic/angular/common` ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> ---------
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import { NgZone } from '@angular/core';
|
||||
import type { Config, IonicWindow } from '@ionic/angular/common';
|
||||
import { raf } from '@ionic/angular/common';
|
||||
import { setupConfig } from '@ionic/core';
|
||||
import { applyPolyfills, defineCustomElements } from '@ionic/core/loader';
|
||||
|
||||
import { raf } from './util/util';
|
||||
|
||||
// TODO(FW-2827): types
|
||||
|
||||
export const appInitialize = (config: Config, doc: Document, zone: NgZone) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Directive, HostListener, ElementRef, Injector } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor, setIonicClasses } from './value-accessor';
|
||||
import { ValueAccessor, setIonicClasses } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-checkbox,ion-toggle',
|
||||
@@ -19,8 +18,8 @@ export class BooleanValueAccessorDirective extends ValueAccessor {
|
||||
}
|
||||
|
||||
writeValue(value: boolean): void {
|
||||
this.el.nativeElement.checked = this.lastValue = value;
|
||||
setIonicClasses(this.el);
|
||||
this.elementRef.nativeElement.checked = this.lastValue = value;
|
||||
setIonicClasses(this.elementRef);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Directive, HostListener, ElementRef, Injector } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor } from './value-accessor';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-input[type=number]',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ElementRef, Injector, Directive, HostListener } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor } from './value-accessor';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
/* tslint:disable-next-line:directive-selector */
|
||||
@@ -19,9 +18,12 @@ export class RadioValueAccessorDirective extends ValueAccessor {
|
||||
super(injector, el);
|
||||
}
|
||||
|
||||
// TODO(FW-2827): type (HTMLIonRadioElement and HTMLElement are both missing `checked`)
|
||||
@HostListener('ionSelect', ['$event.target'])
|
||||
_handleIonSelect(el: any): void {
|
||||
/**
|
||||
* The `el` type is any to access the `checked` state property
|
||||
* that is not exposed on the type interface.
|
||||
*/
|
||||
this.handleValueChange(el, el.checked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ElementRef, Injector, Directive, HostListener } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor } from './value-accessor';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
/* tslint:disable-next-line:directive-selector */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ElementRef, Injector, Directive, HostListener } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor } from './value-accessor';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-input:not([type=number]),ion-textarea,ion-searchbar',
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { AfterViewInit, ElementRef, Injector, OnDestroy, Directive, HostListener } from '@angular/core';
|
||||
import { ControlValueAccessor, NgControl } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { raf } from '../../util/util';
|
||||
|
||||
// TODO(FW-2827): types
|
||||
|
||||
@Directive()
|
||||
export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDestroy {
|
||||
private onChange: (value: any) => void = () => {
|
||||
/**/
|
||||
};
|
||||
private onTouched: () => void = () => {
|
||||
/**/
|
||||
};
|
||||
protected lastValue: any;
|
||||
private statusChanges?: Subscription;
|
||||
|
||||
constructor(protected injector: Injector, protected el: ElementRef) {}
|
||||
|
||||
writeValue(value: any): void {
|
||||
this.el.nativeElement.value = this.lastValue = value;
|
||||
setIonicClasses(this.el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the ControlValueAccessor of a change in the value of the control.
|
||||
*
|
||||
* This is called by each of the ValueAccessor directives when we want to update
|
||||
* the status and validity of the form control. For example with text components this
|
||||
* is called when the ionInput event is fired. For select components this is called
|
||||
* when the ionChange event is fired.
|
||||
*
|
||||
* This also updates the Ionic form status classes on the element.
|
||||
*
|
||||
* @param el The component element.
|
||||
* @param value The new value of the control.
|
||||
*/
|
||||
handleValueChange(el: HTMLElement, value: any): void {
|
||||
if (el === this.el.nativeElement) {
|
||||
if (value !== this.lastValue) {
|
||||
this.lastValue = value;
|
||||
this.onChange(value);
|
||||
}
|
||||
setIonicClasses(this.el);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('ionBlur', ['$event.target'])
|
||||
_handleBlurEvent(el: any): void {
|
||||
if (el === this.el.nativeElement) {
|
||||
this.onTouched();
|
||||
setIonicClasses(this.el);
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: any) => void): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.el.nativeElement.disabled = isDisabled;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.statusChanges) {
|
||||
this.statusChanges.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
let ngControl;
|
||||
try {
|
||||
ngControl = this.injector.get<NgControl>(NgControl);
|
||||
} catch {
|
||||
/* No FormControl or ngModel binding */
|
||||
}
|
||||
|
||||
if (!ngControl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for changes in validity, disabled, or pending states
|
||||
if (ngControl.statusChanges) {
|
||||
this.statusChanges = ngControl.statusChanges.subscribe(() => setIonicClasses(this.el));
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO FW-2787: Remove this in favor of https://github.com/angular/angular/issues/10887
|
||||
* whenever it is implemented.
|
||||
*/
|
||||
const formControl = ngControl.control as any;
|
||||
if (formControl) {
|
||||
const methodsToPatch = ['markAsTouched', 'markAllAsTouched', 'markAsUntouched', 'markAsDirty', 'markAsPristine'];
|
||||
methodsToPatch.forEach((method) => {
|
||||
if (typeof formControl[method] !== 'undefined') {
|
||||
const oldFn = formControl[method].bind(formControl);
|
||||
formControl[method] = (...params: any[]) => {
|
||||
oldFn(...params);
|
||||
setIonicClasses(this.el);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setIonicClasses = (element: ElementRef): void => {
|
||||
raf(() => {
|
||||
const input = element.nativeElement as HTMLInputElement;
|
||||
const hasValue = input.value != null && input.value.toString().length > 0;
|
||||
const classes = getClasses(input);
|
||||
setClasses(input, classes);
|
||||
const item = input.closest('ion-item');
|
||||
if (item) {
|
||||
if (hasValue) {
|
||||
setClasses(item, [...classes, 'item-has-value']);
|
||||
} else {
|
||||
setClasses(item, classes);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getClasses = (element: HTMLElement) => {
|
||||
const classList = element.classList;
|
||||
const classes = [];
|
||||
for (let i = 0; i < classList.length; i++) {
|
||||
const item = classList.item(i);
|
||||
if (item !== null && startsWith(item, 'ng-')) {
|
||||
classes.push(`ion-${item.substring(3)}`);
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
};
|
||||
|
||||
const setClasses = (element: HTMLElement, classes: string[]) => {
|
||||
const classList = element.classList;
|
||||
classList.remove('ion-valid', 'ion-invalid', 'ion-touched', 'ion-untouched', 'ion-dirty', 'ion-pristine');
|
||||
classList.add(...classes);
|
||||
};
|
||||
|
||||
const startsWith = (input: string, search: string): boolean => {
|
||||
return input.substring(0, search.length) === search;
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
declare const __zone_symbol__requestAnimationFrame: any;
|
||||
declare const requestAnimationFrame: any;
|
||||
|
||||
export const raf = (h: any): any => {
|
||||
if (typeof __zone_symbol__requestAnimationFrame === 'function') {
|
||||
return __zone_symbol__requestAnimationFrame(h);
|
||||
}
|
||||
if (typeof requestAnimationFrame === 'function') {
|
||||
return requestAnimationFrame(h);
|
||||
}
|
||||
return setTimeout(h);
|
||||
};
|
||||
Reference in New Issue
Block a user