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:
Sean Perkins
2023-09-19 12:46:28 -04:00
committed by GitHub
parent dad7e66cf6
commit 28f2ec9c62
47 changed files with 1626 additions and 902 deletions

View File

@@ -26,6 +26,21 @@ export const routes: Routes = [
{ path: 'tab-three', loadComponent: () => import('../tabs/tab3.component').then(c => c.TabThreeComponent) }
]
},
{
path: 'value-accessors',
children: [
{ path: 'checkbox', loadComponent: () => import('../value-accessors/checkbox/checkbox.component').then(c => c.CheckboxComponent) },
{ path: 'datetime', loadComponent: () => import('../value-accessors/datetime/datetime.component').then(c => c.DatetimeComponent) },
{ path: 'input', loadComponent: () => import('../value-accessors/input/input.component').then(c => c.InputComponent) },
{ path: 'radio-group', loadComponent: () => import('../value-accessors/radio-group/radio-group.component').then(c => c.RadioGroupComponent) },
{ path: 'range', loadComponent: () => import('../value-accessors/range/range.component').then(c => c.RangeComponent) },
{ path: 'searchbar', loadComponent: () => import('../value-accessors/searchbar/searchbar.component').then(c => c.SearchbarComponent) },
{ path: 'segment', loadComponent: () => import('../value-accessors/segment/segment.component').then(c => c.SegmentComponent) },
{ path: 'textarea', loadComponent: () => import('../value-accessors/textarea/textarea.component').then(c => c.TextareaComponent) },
{ path: 'toggle', loadComponent: () => import('../value-accessors/toggle/toggle.component').then(c => c.ToggleComponent) },
{ path: '**', redirectTo: 'checkbox' }
]
}
]
},
];

View File

@@ -0,0 +1,11 @@
<div>
<h1>IonCheckbox Value Accessors</h1>
<p>
This test checks the form integrations with ion-checkbox to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-checkbox formControlName="checkbox"></ion-checkbox>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonCheckbox } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-checkbox',
templateUrl: 'checkbox.component.html',
standalone: true,
imports: [
IonCheckbox,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class CheckboxComponent {
form = this.fb.group({
checkbox: [false, Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,11 @@
<div>
<h1>IonDatetime Value Accessors</h1>
<p>
This test checks the form integrations with ion-datetime to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-datetime formControlName="datetime"></ion-datetime>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonDatetime } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-datetime',
templateUrl: 'datetime.component.html',
standalone: true,
imports: [
IonDatetime,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class DatetimeComponent {
form = this.fb.group({
datetime: ['2023-05-10T04:00:00', Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,11 @@
<div>
<h1>IonInput Value Accessors</h1>
<p>
This test checks the form integrations with ion-input to make sure values are correctly assigned to the form group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-input label="String" formControlName="inputString"></ion-input>
<ion-input label="Number" type="number" formControlName="inputNumber"></ion-input>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,26 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonInput } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-input',
templateUrl: 'input.component.html',
standalone: true,
imports: [
IonInput,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent,
]
})
export class InputComponent {
form = this.fb.group({
inputString: ['', Validators.required],
inputNumber: ['', Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,14 @@
<div>
<h1>IonRadioGroup Value Accessors</h1>
<p>
This test checks the form integrations with ion-radio-group to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-radio-group formControlName="radioGroup">
<ion-radio value="1">One</ion-radio>
<ion-radio value="2">Two</ion-radio>
</ion-radio-group>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,26 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonRadioGroup, IonRadio } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-radio-group',
templateUrl: 'radio-group.component.html',
standalone: true,
imports: [
IonRadioGroup,
IonRadio,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class RadioGroupComponent {
form = this.fb.group({
radioGroup: ['1', Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,9 @@
<div>
<h1>IonRange Value Accessors</h1>
<p>
This test checks the form integrations with ion-range to make sure values are correctly assigned to the form group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-range formControlName="range"></ion-range>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonRange } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-range',
templateUrl: 'range.component.html',
standalone: true,
imports: [
IonRange,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class RangeComponent {
form = this.fb.group({
range: [0, Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,10 @@
<div>
<h1>IonSearchbar Value Accessors</h1>
<p>
This test checks the form integrations with ion-searchbar to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-searchbar label="String" formControlName="searchbar"></ion-searchbar>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonSearchbar } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-searchbar',
templateUrl: 'searchbar.component.html',
standalone: true,
imports: [
IonSearchbar,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class SearchbarComponent {
form = this.fb.group({
searchbar: ['', Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,18 @@
<div>
<h1>IonSegment Value Accessors</h1>
<p>
This test checks the form integrations with ion-segment to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-segment formControlName="segment">
<ion-segment-button value="Paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button value="Free">
<ion-label>Free</ion-label>
</ion-segment-button>
</ion-segment>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,27 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonSegment, IonSegmentButton, IonLabel } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-segment',
templateUrl: 'segment.component.html',
standalone: true,
imports: [
IonSegment,
IonSegmentButton,
IonLabel,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class SegmentComponent {
form = this.fb.group({
segment: ['Paid', Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,11 @@
<div>
<h1>IonTextarea Value Accessors</h1>
<p>
This test checks the form integrations with ion-textarea to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-textarea label="String" formControlName="textarea"></ion-textarea>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonTextarea } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-textarea',
templateUrl: 'textarea.component.html',
standalone: true,
imports: [
IonTextarea,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class TextareaComponent {
form = this.fb.group({
textarea: ['', Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,10 @@
<div>
<h1>IonToggle Value Accessors</h1>
<p>
This test checks the form integrations with ion-toggle to make sure values are correctly assigned to the form group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-toggle formControlName="toggle"></ion-toggle>
</app-value-accessor-test>
</div>

View File

@@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { IonToggle } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
@Component({
selector: 'app-toggle',
templateUrl: 'toggle.component.html',
standalone: true,
imports: [
IonToggle,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class ToggleComponent {
form = this.fb.group({
toggle: [false, Validators.required],
});
constructor(private fb: FormBuilder) { }
}

View File

@@ -0,0 +1,16 @@
<form [formGroup]="formGroup">
<div>
<ng-content></ng-content>
</div>
<input type="submit" value="Submit" [disabled]="!formGroup.valid" />
<h3>Form Value</h3>
<pre id="formValue">{{ formGroup.value | json }}</pre>
<h3>Form Validity</h3>
<ul id="formValidity">
<li>Form valid: {{ formGroup.valid }}</li>
<li *ngFor="let control of formGroup.controls | keyvalue">{{ control.key }} valid: {{ control.value.valid }}</li>
</ul>
</form>

View File

@@ -0,0 +1,27 @@
import { CommonModule, JsonPipe, KeyValuePipe } from "@angular/common";
import { Component, Input } from "@angular/core";
import { FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
@Component({
selector: 'app-value-accessor-test',
templateUrl: 'value-accessor-test.component.html',
standalone: true,
imports: [
ReactiveFormsModule,
FormsModule,
JsonPipe,
KeyValuePipe,
/**
* NgFor directive is not available until Angular 15.
* We import the CommonModule for now.
*
* TODO: FW-5197 Replace with NgFor when dropping Angular 14 support.
*/
CommonModule
]
})
export class ValueAccessorTestComponent {
@Input() formGroup!: FormGroup;
}