mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(angular): add custom injector support for modal and popover controllers (#30899)
Issue number: resolves #30638 --------- <!-- 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? When using `ModalController.create()` or `PopoverController.create()` in Angular, components rendered inside overlays cannot access non-global services or tokens from the component tree. For example, route-scoped services or Angular's Dir directive for bidirectional text support are not accessible from within a modal, requiring complex workarounds with wrapper components. ## What is the new behavior? `ModalController.create()` and `PopoverController.create()` now accept an optional injector property that allows passing a custom Angular Injector. This enables overlay components to access services and tokens that are not available in the root injector, such as route-scoped services or the Dir directive from Angular CDK. ```typescript const customInjector = Injector.create({ providers: [{ provide: MyService, useValue: myServiceInstance }], parent: this.injector, }); ``` ```typescript const modal = await this.modalController.create({ component: MyModalComponent, injector: customInjector, }); ``` ## Does this introduce a breaking change? - [ ] Yes - [X] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Current dev build: ``` 8.7.17-dev.11769628168.11eca7cd ```
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Modal: Custom Injector', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/standalone/modal-custom-injector');
|
||||
});
|
||||
|
||||
test('should inject custom service via custom injector', async ({ page }) => {
|
||||
await page.locator('ion-button#open-modal-with-custom-injector').click();
|
||||
|
||||
await expect(page.locator('ion-modal')).toBeVisible();
|
||||
|
||||
const serviceValue = page.locator('#service-value');
|
||||
await expect(serviceValue).toHaveText('Service Value: custom-injector-value');
|
||||
|
||||
await page.locator('#close-modal').click();
|
||||
await expect(page.locator('ion-modal')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should fail without custom injector when service is not globally provided', async ({ page }) => {
|
||||
page.on('dialog', async (dialog) => {
|
||||
expect(dialog.message()).toContain('TestService not available');
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
await page.locator('ion-button#open-modal-without-custom-injector').click();
|
||||
|
||||
await page.waitForEvent('dialog');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Popover: Custom Injector', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/standalone/popover-custom-injector');
|
||||
});
|
||||
|
||||
test('should inject custom service via custom injector', async ({ page }) => {
|
||||
await page.locator('ion-button#open-popover-with-custom-injector').click();
|
||||
|
||||
await expect(page.locator('ion-popover')).toBeVisible();
|
||||
|
||||
const serviceValue = page.locator('#service-value');
|
||||
await expect(serviceValue).toHaveText('Service Value: custom-injector-value');
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,8 @@ export const routes: Routes = [
|
||||
]
|
||||
},
|
||||
{ path: 'programmatic-modal', loadComponent: () => import('../programmatic-modal/programmatic-modal.component').then(c => c.ProgrammaticModalComponent) },
|
||||
{ path: 'modal-custom-injector', loadComponent: () => import('../modal-custom-injector/modal-custom-injector.component').then(c => c.ModalCustomInjectorComponent) },
|
||||
{ path: 'popover-custom-injector', loadComponent: () => import('../popover-custom-injector/popover-custom-injector.component').then(c => c.PopoverCustomInjectorComponent) },
|
||||
{ path: 'router-outlet', loadComponent: () => import('../router-outlet/router-outlet.component').then(c => c.RouterOutletComponent) },
|
||||
{ path: 'back-button', loadComponent: () => import('../back-button/back-button.component').then(c => c.BackButtonComponent) },
|
||||
{ path: 'router-link', loadComponent: () => import('../router-link/router-link.component').then(c => c.RouterLinkComponent) },
|
||||
|
||||
@@ -110,6 +110,11 @@
|
||||
Programmatic Modal Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/modal-custom-injector">
|
||||
<ion-label>
|
||||
Modal Custom Injector Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/overlay-controllers">
|
||||
<ion-label>
|
||||
Overlay Controllers Test
|
||||
@@ -120,6 +125,11 @@
|
||||
Popover Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/popover-custom-injector">
|
||||
<ion-label>
|
||||
Popover Custom Injector Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Component, inject, Injector } from '@angular/core';
|
||||
import { IonContent, IonHeader, IonTitle, IonToolbar, IonButton, ModalController } from '@ionic/angular/standalone';
|
||||
import { ModalCustomInjectorModalComponent } from './modal/modal.component';
|
||||
import { TestService } from './test.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal-custom-injector',
|
||||
template: `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal Custom Injector Test</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<ion-button id="open-modal-with-custom-injector" (click)="openWithCustomInjector()">
|
||||
Open Modal with Custom Injector
|
||||
</ion-button>
|
||||
<ion-button id="open-modal-without-custom-injector" (click)="openWithoutCustomInjector()">
|
||||
Open Modal without Custom Injector
|
||||
</ion-button>
|
||||
</ion-content>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [IonContent, IonHeader, IonTitle, IonToolbar, IonButton]
|
||||
})
|
||||
export class ModalCustomInjectorComponent {
|
||||
private modalController = inject(ModalController);
|
||||
private injector = inject(Injector);
|
||||
|
||||
async openWithCustomInjector() {
|
||||
const testService = new TestService();
|
||||
testService.setValue('custom-injector-value');
|
||||
|
||||
const customInjector = Injector.create({
|
||||
providers: [{ provide: TestService, useValue: testService }],
|
||||
parent: this.injector,
|
||||
});
|
||||
|
||||
const modal = await this.modalController.create({
|
||||
component: ModalCustomInjectorModalComponent,
|
||||
injector: customInjector,
|
||||
});
|
||||
|
||||
await modal.present();
|
||||
}
|
||||
|
||||
async openWithoutCustomInjector() {
|
||||
try {
|
||||
const modal = await this.modalController.create({
|
||||
component: ModalCustomInjectorModalComponent,
|
||||
});
|
||||
await modal.present();
|
||||
} catch (e) {
|
||||
alert('Error: TestService not available without custom injector');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { IonContent, IonHeader, IonTitle, IonToolbar, IonButton, IonButtons } from '@ionic/angular/standalone';
|
||||
import { TestService } from '../test.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal-custom-injector-modal',
|
||||
template: `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal with Custom Injector</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button id="close-modal" (click)="dismiss()">Close</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<p id="service-value">Service Value: {{ serviceValue }}</p>
|
||||
</ion-content>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [IonContent, IonHeader, IonTitle, IonToolbar, IonButton, IonButtons]
|
||||
})
|
||||
export class ModalCustomInjectorModalComponent implements OnInit {
|
||||
private testService = inject(TestService);
|
||||
serviceValue = '';
|
||||
modal: HTMLIonModalElement | undefined;
|
||||
|
||||
ngOnInit() {
|
||||
this.serviceValue = this.testService.getValue();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.modal?.dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class TestService {
|
||||
private value = 'default-value';
|
||||
|
||||
setValue(value: string) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, inject, Injector } from '@angular/core';
|
||||
import { IonContent, IonHeader, IonTitle, IonToolbar, IonButton, PopoverController } from '@ionic/angular/standalone';
|
||||
import { PopoverCustomInjectorPopoverComponent } from './popover/popover.component';
|
||||
import { TestService } from './test.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-popover-custom-injector',
|
||||
template: `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Popover Custom Injector Test</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<ion-button id="open-popover-with-custom-injector" (click)="openWithCustomInjector($event)">
|
||||
Open Popover with Custom Injector
|
||||
</ion-button>
|
||||
</ion-content>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [IonContent, IonHeader, IonTitle, IonToolbar, IonButton]
|
||||
})
|
||||
export class PopoverCustomInjectorComponent {
|
||||
private popoverController = inject(PopoverController);
|
||||
private injector = inject(Injector);
|
||||
|
||||
async openWithCustomInjector(event: Event) {
|
||||
const testService = new TestService();
|
||||
testService.setValue('custom-injector-value');
|
||||
|
||||
const customInjector = Injector.create({
|
||||
providers: [{ provide: TestService, useValue: testService }],
|
||||
parent: this.injector,
|
||||
});
|
||||
|
||||
const popover = await this.popoverController.create({
|
||||
component: PopoverCustomInjectorPopoverComponent,
|
||||
event: event,
|
||||
injector: customInjector,
|
||||
});
|
||||
|
||||
await popover.present();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { IonContent } from '@ionic/angular/standalone';
|
||||
import { TestService } from '../test.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-popover-custom-injector-popover',
|
||||
template: `
|
||||
<ion-content>
|
||||
<p id="service-value">Service Value: {{ serviceValue }}</p>
|
||||
</ion-content>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [IonContent]
|
||||
})
|
||||
export class PopoverCustomInjectorPopoverComponent implements OnInit {
|
||||
private testService = inject(TestService);
|
||||
serviceValue = '';
|
||||
|
||||
ngOnInit() {
|
||||
this.serviceValue = this.testService.getValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class TestService {
|
||||
private value = 'default-value';
|
||||
|
||||
setValue(value: string) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user