- {errorText}
+
+ {isInvalid ? errorText : null}
,
];
}
private getHintTextID(): string | undefined {
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
+ if (isInvalid && errorText) {
return errorTextId;
}
@@ -864,7 +907,7 @@ export class Input implements ComponentInterface {
onCompositionstart={this.onCompositionStart}
onCompositionend={this.onCompositionEnd}
aria-describedby={this.getHintTextID()}
- aria-invalid={this.getHintTextID() === this.errorTextId}
+ aria-invalid={this.isInvalid ? 'true' : undefined}
{...this.inheritedAttributes}
/>
{this.clearInput && !readonly && !disabled && (
diff --git a/core/src/components/input/test/validation/index.html b/core/src/components/input/test/validation/index.html
new file mode 100644
index 00000000000..556ba15545f
--- /dev/null
+++ b/core/src/components/input/test/validation/index.html
@@ -0,0 +1,292 @@
+
+
+
+
+
Input - Validation
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Input - Validation Test
+
+
+
+
+
+
Screen Reader Testing Instructions:
+
+ - Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
+ - Tab through the form fields
+ - When you tab away from an empty required field, the error should be announced immediately
+ - The error text should be announced BEFORE the next field is announced
+ - Test in Chrome, Safari, and Firefox to verify consistent behavior
+
+
+
+
+
+
Required Email Field
+
+
+
+
+
Required Name Field
+
+
+
+
+
Phone Number (Pattern Validation)
+
+
+
+
+
Password (Min Length)
+
+
+
+
+
Age (Number Range)
+
+
+
+
+
Optional Field (No Validation)
+
+
+
+
+
+ Submit Form
+ Reset Form
+
+
+
+
+
+
+
diff --git a/core/src/components/textarea/test/validation/index.html b/core/src/components/textarea/test/validation/index.html
new file mode 100644
index 00000000000..3c832e7979e
--- /dev/null
+++ b/core/src/components/textarea/test/validation/index.html
@@ -0,0 +1,293 @@
+
+
+
+
+
Textarea - Validation
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Textarea - Validation Test
+
+
+
+
+
+
Screen Reader Testing Instructions:
+
+ - Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
+ - Tab through the form fields
+ - When you tab away from an empty required field, the error should be announced immediately
+ - The error text should be announced BEFORE the next field is announced
+ - Test in Chrome, Safari, and Firefox to verify consistent behavior
+
+
+
+
+
+
Required Description (Min Length)
+
+
+
+
+
Required Comments
+
+
+
+
+
Bio (Max Length)
+
+
+
+
+
Address (Pattern Validation)
+
+
+
+
+
Review (Min/Max Length)
+
+
+
+
+
Optional Notes
+
+
+
+
+
+ Submit Form
+ Reset Form
+
+
+
+
+
+
+
diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx
index 64ff00c9225..83c1b91c2e4 100644
--- a/core/src/components/textarea/textarea.tsx
+++ b/core/src/components/textarea/textarea.tsx
@@ -81,6 +81,13 @@ export class Textarea implements ComponentInterface {
*/
@State() hasFocus = false;
+ /**
+ * Track validation state for proper aria-live announcements
+ */
+ @State() isInvalid = false;
+
+ private validationObserver?: MutationObserver;
+
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -328,6 +335,16 @@ export class Textarea implements ComponentInterface {
}
}
+ /**
+ * Checks if the textarea is in an invalid state based on Ionic validation classes
+ */
+ private checkValidationState(): boolean {
+ const hasIonTouched = this.el.classList.contains('ion-touched');
+ const hasIonInvalid = this.el.classList.contains('ion-invalid');
+
+ return hasIonTouched && hasIonInvalid;
+ }
+
connectedCallback() {
const { el } = this;
this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this));
@@ -336,6 +353,27 @@ export class Textarea implements ComponentInterface {
() => this.notchSpacerEl,
() => this.labelSlot
);
+
+ // Watch for class changes to update validation state
+ if (Build.isBrowser && typeof MutationObserver !== 'undefined') {
+ this.validationObserver = new MutationObserver(() => {
+ const newIsInvalid = this.checkValidationState();
+ if (this.isInvalid !== newIsInvalid) {
+ this.isInvalid = newIsInvalid;
+ // Force a re-render to update aria-describedby immediately
+ forceUpdate(this);
+ }
+ });
+
+ this.validationObserver.observe(el, {
+ attributes: true,
+ attributeFilter: ['class'],
+ });
+ }
+
+ // Always set initial state
+ this.isInvalid = this.checkValidationState();
+
this.debounceChanged();
if (Build.isBrowser) {
document.dispatchEvent(
@@ -364,6 +402,12 @@ export class Textarea implements ComponentInterface {
this.notchController.destroy();
this.notchController = undefined;
}
+
+ // Clean up validation observer to prevent memory leaks
+ if (this.validationObserver) {
+ this.validationObserver.disconnect();
+ this.validationObserver = undefined;
+ }
}
componentWillLoad() {
@@ -628,22 +672,22 @@ export class Textarea implements ComponentInterface {
* Renders the helper text or error text values
*/
private renderHintText() {
- const { helperText, errorText, helperTextId, errorTextId } = this;
+ const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
return [
-
- {helperText}
+
+ {!isInvalid ? helperText : null}
,
-
- {errorText}
+
+ {isInvalid ? errorText : null}
,
];
}
private getHintTextID(): string | undefined {
- const { el, helperText, errorText, helperTextId, errorTextId } = this;
+ const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this;
- if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
+ if (isInvalid && errorText) {
return errorTextId;
}
@@ -777,7 +821,7 @@ export class Textarea implements ComponentInterface {
onFocus={this.onFocus}
onKeyDown={this.onKeyDown}
aria-describedby={this.getHintTextID()}
- aria-invalid={this.getHintTextID() === this.errorTextId}
+ aria-invalid={this.isInvalid ? 'true' : undefined}
{...this.inheritedAttributes}
>
{value}
diff --git a/packages/angular/test/base/src/app/lazy/app-lazy/app.module.ts b/packages/angular/test/base/src/app/lazy/app-lazy/app.module.ts
index caf27670d2d..ac0ebd501fb 100644
--- a/packages/angular/test/base/src/app/lazy/app-lazy/app.module.ts
+++ b/packages/angular/test/base/src/app/lazy/app-lazy/app.module.ts
@@ -28,6 +28,7 @@ import { AlertComponent } from '../alert/alert.component';
import { AccordionComponent } from '../accordion/accordion.component';
import { AccordionModalComponent } from '../accordion/accordion-modal/accordion-modal.component';
import { TabsBasicComponent } from '../tabs-basic/tabs-basic.component';
+import { TemplateFormComponent } from '../template-form/template-form.component';
@NgModule({
declarations: [
@@ -53,7 +54,8 @@ import { TabsBasicComponent } from '../tabs-basic/tabs-basic.component';
AlertComponent,
AccordionComponent,
AccordionModalComponent,
- TabsBasicComponent
+ TabsBasicComponent,
+ TemplateFormComponent
],
imports: [
CommonModule,
diff --git a/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts b/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts
index 0e15ea2867d..1a46992f92c 100644
--- a/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts
+++ b/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts
@@ -19,6 +19,7 @@ import { NavigationPage3Component } from '../navigation-page3/navigation-page3.c
import { AlertComponent } from '../alert/alert.component';
import { AccordionComponent } from '../accordion/accordion.component';
import { TabsBasicComponent } from '../tabs-basic/tabs-basic.component';
+import { TemplateFormComponent } from '../template-form/template-form.component';
export const routes: Routes = [
{
@@ -33,6 +34,7 @@ export const routes: Routes = [
{ path: 'textarea', loadChildren: () => import('../textarea/textarea.module').then(m => m.TextareaModule) },
{ path: 'searchbar', loadChildren: () => import('../searchbar/searchbar.module').then(m => m.SearchbarModule) },
{ path: 'form', component: FormComponent },
+ { path: 'template-form', component: TemplateFormComponent },
{ path: 'modals', component: ModalComponent },
{ path: 'modal-inline', loadChildren: () => import('../modal-inline').then(m => m.ModalInlineModule) },
{ path: 'view-child', component: ViewChildComponent },
diff --git a/packages/angular/test/base/src/app/lazy/home-page/home-page.component.html b/packages/angular/test/base/src/app/lazy/home-page/home-page.component.html
index 80418148c5e..136a0119d34 100644
--- a/packages/angular/test/base/src/app/lazy/home-page/home-page.component.html
+++ b/packages/angular/test/base/src/app/lazy/home-page/home-page.component.html
@@ -25,6 +25,11 @@
Form Test
+
+
+ Template-Driven Form Test
+
+
Modals Test
diff --git a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html
new file mode 100644
index 00000000000..d33aa4ae1e5
--- /dev/null
+++ b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html
@@ -0,0 +1,116 @@
+
+
+ Template-Driven Form Validation Test
+
+
+
+
+
+
+
+
Instructions to reproduce issue:
+
+ - Click in the "Required Input" field
+ - Click outside without entering text
+ - The field should show as touched and invalid
+ - The error text should appear below the input
+ - For screen readers, the validation state should be announced
+
+
Note: With template-driven forms, Angular applies validation classes to the wrapper element, not directly to ion-input/ion-textarea.
+
+
diff --git a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts
new file mode 100644
index 00000000000..1ecdaa5e5d0
--- /dev/null
+++ b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts
@@ -0,0 +1,26 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-template-form',
+ templateUrl: './template-form.component.html',
+ standalone: false
+})
+export class TemplateFormComponent {
+ inputValue = '';
+ textareaValue = '';
+ minLengthValue = '';
+
+ // Track if form has been submitted
+ submitted = false;
+
+ onSubmit(form: any) {
+ this.submitted = true;
+ console.log('Form submitted:', form.value);
+ console.log('Form valid:', form.valid);
+ }
+
+ resetForm(form: any) {
+ form.reset();
+ this.submitted = false;
+ }
+}
diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts
index ae6ee66193c..fafb69c62ad 100644
--- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts
+++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts
@@ -40,6 +40,14 @@ export const routes: Routes = [
]
},
{ path: 'tabs-basic', loadComponent: () => import('../tabs-basic/tabs-basic.component').then(c => c.TabsBasicComponent) },
+ {
+ path: 'validation',
+ children: [
+ { path: 'input-validation', loadComponent: () => import('../validation/input-validation/input-validation.component').then(c => c.InputValidationComponent) },
+ { path: 'textarea-validation', loadComponent: () => import('../validation/textarea-validation/textarea-validation.component').then(c => c.TextareaValidationComponent) },
+ { path: '**', redirectTo: 'input-validation' }
+ ]
+ },
{
path: 'value-accessors',
children: [
diff --git a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html
index 163e438d42c..7900bdfb64e 100644
--- a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html
+++ b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html
@@ -107,6 +107,22 @@
+
+
+ Validation Tests
+
+
+
+ Input Validation Test
+
+
+
+
+ Textarea Validation Test
+
+
+
+
Value Accessors
diff --git a/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.html
new file mode 100644
index 00000000000..d3c085b084b
--- /dev/null
+++ b/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.html
@@ -0,0 +1,131 @@
+
+
+ Input - Validation Test
+
+
+
+
+
+
Screen Reader Testing Instructions:
+
+ - Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
+ - Tab through the form fields
+ - When you tab away from an empty required field, the error should be announced immediately
+ - The error text should be announced BEFORE the next field is announced
+ - Test in Chrome, Safari, and Firefox to verify consistent behavior
+
+
+
+
+
+
+ Submit Form
+ Reset Form
+
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.scss
new file mode 100644
index 00000000000..add228ccab1
--- /dev/null
+++ b/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.scss
@@ -0,0 +1,36 @@
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ grid-row-gap: 20px;
+ grid-column-gap: 20px;
+}
+
+h2 {
+ font-size: 12px;
+ font-weight: normal;
+ color: var(--ion-color-step-600);
+ margin-top: 10px;
+ margin-bottom: 5px;
+}
+
+.validation-info {
+ margin: 20px;
+ padding: 10px;
+ background: var(--ion-color-light);
+ border-radius: 4px;
+}
+
+.validation-info h2 {
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 10px;
+}
+
+.validation-info ol {
+ margin: 0;
+ padding-left: 20px;
+}
+
+.validation-info li {
+ margin-bottom: 5px;
+}
diff --git a/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.ts
new file mode 100644
index 00000000000..f7d67754cd9
--- /dev/null
+++ b/packages/angular/test/base/src/app/standalone/validation/input-validation/input-validation.component.ts
@@ -0,0 +1,125 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import {
+ FormBuilder,
+ ReactiveFormsModule,
+ Validators
+} from '@angular/forms';
+import {
+ IonButton,
+ IonContent,
+ IonHeader,
+ IonInput,
+ IonTitle,
+ IonToolbar
+} from '@ionic/angular/standalone';
+
+@Component({
+ selector: 'app-input-validation',
+ templateUrl: './input-validation.component.html',
+ styleUrls: ['./input-validation.component.scss'],
+ standalone: true,
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ IonInput,
+ IonButton,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent
+ ]
+})
+export class InputValidationComponent {
+ // Track which fields have been touched (using Set like vanilla test)
+ touchedFields = new Set();
+
+ // Field metadata for labels and error messages
+ fieldMetadata = {
+ email: {
+ label: 'Email',
+ helperText: "We'll never share your email",
+ errorText: 'Please enter a valid email address'
+ },
+ name: {
+ label: 'Full Name',
+ helperText: 'First and last name',
+ errorText: 'Name is required'
+ },
+ phone: {
+ label: 'Phone',
+ helperText: 'Format: (555) 555-5555',
+ errorText: 'Please enter a valid phone number'
+ },
+ password: {
+ label: 'Password',
+ helperText: 'At least 8 characters',
+ errorText: 'Password must be at least 8 characters'
+ },
+ age: {
+ label: 'Age',
+ helperText: 'Must be 18 or older',
+ errorText: 'Please enter a valid age (18-120)'
+ },
+ optional: {
+ label: 'Optional Info',
+ helperText: 'You can skip this field',
+ errorText: ''
+ }
+ };
+
+ form = this.fb.group({
+ email: ['', [Validators.required, Validators.email]],
+ name: ['', Validators.required],
+ phone: ['', [Validators.required, Validators.pattern(/^\(\d{3}\) \d{3}-\d{4}$/)]],
+ password: ['', [Validators.required, Validators.minLength(8)]],
+ age: ['', [Validators.required, Validators.min(18), Validators.max(120)]],
+ optional: ['']
+ });
+
+ constructor(private fb: FormBuilder) {}
+
+ // Check if a field is invalid
+ isInvalid(fieldName: string): boolean {
+ const control = this.form.get(fieldName);
+ return !!(control && control.invalid && control.touched);
+ }
+
+ // Check if a field is valid
+ isValid(fieldName: string): boolean {
+ const control = this.form.get(fieldName);
+ return !!(control && control.valid && control.touched);
+ }
+
+
+ // Check if form is valid (excluding optional field)
+ isFormValid(): boolean {
+ const requiredFields = ['email', 'name', 'phone', 'password', 'age'];
+ return requiredFields.every(field => {
+ const control = this.form.get(field);
+ return control && control.valid;
+ });
+ }
+
+ // Submit form
+ onSubmit(): void {
+ if (this.isFormValid()) {
+ alert('Form submitted successfully!');
+ }
+ }
+
+ // Reset form
+ onReset(): void {
+ // Reset form values
+ this.form.reset();
+
+ // Clear touched fields
+ this.touchedFields.clear();
+
+ // Remove validation classes from all inputs
+ const inputs = document.querySelectorAll('ion-input');
+ inputs.forEach(input => {
+ input.classList.remove('ion-valid', 'ion-invalid', 'ion-touched');
+ });
+ }
+}
diff --git a/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.html
new file mode 100644
index 00000000000..8aa8f506b61
--- /dev/null
+++ b/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.html
@@ -0,0 +1,133 @@
+
+
+ Textarea - Validation Test
+
+
+
+
+
+
Screen Reader Testing Instructions:
+
+ - Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
+ - Tab through the form fields
+ - When you tab away from an empty required field, the error should be announced immediately
+ - The error text should be announced BEFORE the next field is announced
+ - Test in Chrome, Safari, and Firefox to verify consistent behavior
+
+
+
+
+
+
+ Submit Form
+ Reset Form
+
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.scss
new file mode 100644
index 00000000000..8c0400b3756
--- /dev/null
+++ b/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.scss
@@ -0,0 +1,44 @@
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ grid-row-gap: 20px;
+ grid-column-gap: 20px;
+}
+
+h2 {
+ font-size: 12px;
+ font-weight: normal;
+ color: var(--ion-color-step-600);
+ margin-top: 10px;
+ margin-bottom: 5px;
+}
+
+.aria-live-region {
+ position: absolute;
+ left: -10000px;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+}
+
+.validation-info {
+ margin: 20px;
+ padding: 10px;
+ background: var(--ion-color-light);
+ border-radius: 4px;
+}
+
+.validation-info h2 {
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 10px;
+}
+
+.validation-info ol {
+ margin: 0;
+ padding-left: 20px;
+}
+
+.validation-info li {
+ margin-bottom: 5px;
+}
diff --git a/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.ts
new file mode 100644
index 00000000000..3756ddefcd0
--- /dev/null
+++ b/packages/angular/test/base/src/app/standalone/validation/textarea-validation/textarea-validation.component.ts
@@ -0,0 +1,144 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import {
+ AbstractControl,
+ FormBuilder,
+ ReactiveFormsModule,
+ ValidationErrors,
+ Validators
+} from '@angular/forms';
+import {
+ IonButton,
+ IonContent,
+ IonHeader,
+ IonTextarea,
+ IonTitle,
+ IonToolbar
+} from '@ionic/angular/standalone';
+
+// Custom validator for address (must be at least 10 chars and contain a digit)
+function addressValidator(control: AbstractControl): ValidationErrors | null {
+ const value = control.value;
+ if (!value || value.length < 10) {
+ return { invalidAddress: true };
+ }
+ // Check if it contains at least one number (for street/zip)
+ return /\d/.test(value) ? null : { invalidAddress: true };
+}
+
+@Component({
+ selector: 'app-textarea-validation',
+ templateUrl: './textarea-validation.component.html',
+ styleUrls: ['./textarea-validation.component.scss'],
+ standalone: true,
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ IonTextarea,
+ IonButton,
+ IonHeader,
+ IonToolbar,
+ IonTitle,
+ IonContent
+ ]
+})
+export class TextareaValidationComponent {
+ // Track which fields have been touched (using Set like vanilla test)
+ touchedFields = new Set();
+
+ // Field metadata for labels and error messages
+ fieldMetadata = {
+ description: {
+ label: 'Description',
+ helperText: 'At least 20 characters',
+ errorText: 'Description must be at least 20 characters',
+ rows: 4
+ },
+ comments: {
+ label: 'Comments',
+ helperText: 'Please provide your feedback',
+ errorText: 'Comments are required',
+ rows: 4
+ },
+ bio: {
+ label: 'Bio',
+ helperText: 'Maximum 200 characters',
+ errorText: 'Bio is required',
+ rows: 4,
+ counter: true
+ },
+ address: {
+ label: 'Address',
+ helperText: 'Include street, city, state, and zip',
+ errorText: 'Please enter a complete address',
+ rows: 3
+ },
+ review: {
+ label: 'Product Review',
+ helperText: 'Between 50-500 characters',
+ errorText: 'Review must be between 50-500 characters',
+ rows: 5,
+ counter: true
+ },
+ notes: {
+ label: 'Additional Notes',
+ helperText: 'This field is optional',
+ errorText: '',
+ rows: 3
+ }
+ };
+
+ form = this.fb.group({
+ description: ['', [Validators.required, Validators.minLength(20)]],
+ comments: ['', Validators.required],
+ bio: ['', [Validators.required, Validators.maxLength(200)]],
+ address: ['', [Validators.required, addressValidator]],
+ review: ['', [Validators.required, Validators.minLength(50), Validators.maxLength(500)]],
+ notes: ['']
+ });
+
+ constructor(private fb: FormBuilder) {}
+
+ // Check if a field is invalid
+ isInvalid(fieldName: string): boolean {
+ const control = this.form.get(fieldName);
+ return !!(control && control.invalid && control.touched);
+ }
+
+ // Check if a field is valid
+ isValid(fieldName: string): boolean {
+ const control = this.form.get(fieldName);
+ return !!(control && control.valid && control.touched);
+ }
+
+ // Check if form is valid (excluding optional field)
+ isFormValid(): boolean {
+ const requiredFields = ['description', 'comments', 'bio', 'address', 'review'];
+ return requiredFields.every(field => {
+ const control = this.form.get(field);
+ return control && control.valid;
+ });
+ }
+
+ // Submit form
+ onSubmit(): void {
+ if (this.isFormValid()) {
+ alert('Form submitted successfully!');
+ }
+ }
+
+ // Reset form
+ onReset(): void {
+ // Reset form values
+ this.form.reset();
+
+ // Clear touched fields
+ this.touchedFields.clear();
+
+ // Remove validation classes from all textareas
+ const textareas = document.querySelectorAll('ion-textarea');
+ textareas.forEach(textarea => {
+ textarea.classList.remove('ion-valid', 'ion-invalid', 'ion-touched');
+ });
+ }
+}