Skip to content

Commit 552e96d

Browse files
committed
feat(angular): add custom injector support for modal and popover controllers
1 parent 7d64307 commit 552e96d

14 files changed

Lines changed: 187 additions & 26 deletions

File tree

packages/angular/common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { AngularDelegate, bindLifecycleEvents, IonModalToken } from './providers
99

1010
export type { IonicWindow } from './types/interfaces';
1111
export type { ViewDidEnter, ViewDidLeave, ViewWillEnter, ViewWillLeave } from './types/ionic-lifecycle-hooks';
12+
export type { AngularModalOptions, AngularPopoverOptions } from './types/overlay-options';
1213

1314
export { NavParams } from './directives/navigation/nav-params';
1415

packages/angular/common/src/providers/angular-delegate.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@ export class AngularDelegate {
3636
create(
3737
environmentInjector: EnvironmentInjector,
3838
injector: Injector,
39-
elementReferenceKey?: string
39+
elementReferenceKey?: string,
40+
customInjector?: Injector
4041
): AngularFrameworkDelegate {
4142
return new AngularFrameworkDelegate(
4243
environmentInjector,
4344
injector,
4445
this.applicationRef,
4546
this.zone,
4647
elementReferenceKey,
47-
this.config.useSetInputAPI ?? false
48+
this.config.useSetInputAPI ?? false,
49+
customInjector
4850
);
4951
}
5052
}
@@ -59,7 +61,8 @@ export class AngularFrameworkDelegate implements FrameworkDelegate {
5961
private applicationRef: ApplicationRef,
6062
private zone: NgZone,
6163
private elementReferenceKey?: string,
62-
private enableSignalsSupport?: boolean
64+
private enableSignalsSupport?: boolean,
65+
private customInjector?: Injector
6366
) {}
6467

6568
attachViewToDom(container: any, component: any, params?: any, cssClasses?: string[]): Promise<any> {
@@ -93,7 +96,8 @@ export class AngularFrameworkDelegate implements FrameworkDelegate {
9396
componentProps,
9497
cssClasses,
9598
this.elementReferenceKey,
96-
this.enableSignalsSupport
99+
this.enableSignalsSupport,
100+
this.customInjector
97101
);
98102
resolve(el);
99103
});
@@ -131,7 +135,8 @@ export const attachView = (
131135
params: any,
132136
cssClasses: string[] | undefined,
133137
elementReferenceKey: string | undefined,
134-
enableSignalsSupport: boolean | undefined
138+
enableSignalsSupport: boolean | undefined,
139+
customInjector?: Injector
135140
): any => {
136141
/**
137142
* Wraps the injector with a custom injector that
@@ -158,7 +163,7 @@ export const attachView = (
158163

159164
const childInjector = Injector.create({
160165
providers,
161-
parent: injector,
166+
parent: customInjector ?? injector,
162167
});
163168

164169
const componentRef = createComponent<any>(component, {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Injector } from '@angular/core';
2+
import type { ModalOptions, PopoverOptions } from '@ionic/core/components';
3+
4+
export interface AngularModalOptions extends ModalOptions {
5+
injector?: Injector;
6+
}
7+
8+
export interface AngularPopoverOptions extends PopoverOptions {
9+
injector?: Injector;
10+
}

packages/angular/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export {
3232
ViewDidEnter,
3333
ViewDidLeave,
3434
} from '@ionic/angular/common';
35+
export type { AngularModalOptions, AngularPopoverOptions } from '@ionic/angular/common';
3536
export { AlertController } from './providers/alert-controller';
3637
export { AnimationController } from './providers/animation-controller';
3738
export { ActionSheetController } from './providers/action-sheet-controller';
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Injector, Injectable, EnvironmentInjector, inject } from '@angular/core';
22
import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common';
3-
import type { ModalOptions } from '@ionic/core';
3+
import type { AngularModalOptions } from '@ionic/angular/common';
44
import { modalController } from '@ionic/core';
55

66
@Injectable()
7-
export class ModalController extends OverlayBaseController<ModalOptions, HTMLIonModalElement> {
7+
export class ModalController extends OverlayBaseController<AngularModalOptions, HTMLIonModalElement> {
88
private angularDelegate = inject(AngularDelegate);
99
private injector = inject(Injector);
1010
private environmentInjector = inject(EnvironmentInjector);
@@ -13,10 +13,11 @@ export class ModalController extends OverlayBaseController<ModalOptions, HTMLIon
1313
super(modalController);
1414
}
1515

16-
create(opts: ModalOptions): Promise<HTMLIonModalElement> {
16+
create(opts: AngularModalOptions): Promise<HTMLIonModalElement> {
17+
const { injector: customInjector, ...restOpts } = opts;
1718
return super.create({
18-
...opts,
19-
delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'modal'),
19+
...restOpts,
20+
delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'modal', customInjector),
2021
});
2122
}
2223
}
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Injector, inject, EnvironmentInjector } from '@angular/core';
22
import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common';
3-
import type { PopoverOptions } from '@ionic/core';
3+
import type { AngularPopoverOptions } from '@ionic/angular/common';
44
import { popoverController } from '@ionic/core';
55

6-
export class PopoverController extends OverlayBaseController<PopoverOptions, HTMLIonPopoverElement> {
6+
export class PopoverController extends OverlayBaseController<AngularPopoverOptions, HTMLIonPopoverElement> {
77
private angularDelegate = inject(AngularDelegate);
88
private injector = inject(Injector);
99
private environmentInjector = inject(EnvironmentInjector);
@@ -12,10 +12,11 @@ export class PopoverController extends OverlayBaseController<PopoverOptions, HTM
1212
super(popoverController);
1313
}
1414

15-
create(opts: PopoverOptions): Promise<HTMLIonPopoverElement> {
15+
create(opts: AngularPopoverOptions): Promise<HTMLIonPopoverElement> {
16+
const { injector: customInjector, ...restOpts } = opts;
1617
return super.create({
17-
...opts,
18-
delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'popover'),
18+
...restOpts,
19+
delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'popover', customInjector),
1920
});
2021
}
2122
}

packages/angular/standalone/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export {
2828
ViewWillLeave,
2929
ViewDidLeave,
3030
} from '@ionic/angular/common';
31+
export type { AngularModalOptions, AngularPopoverOptions } from '@ionic/angular/common';
3132
export { IonNav } from './navigation/nav';
3233
export {
3334
IonCheckbox,
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Injector, Injectable, EnvironmentInjector, inject } from '@angular/core';
22
import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common';
3-
import type { ModalOptions } from '@ionic/core/components';
3+
import type { AngularModalOptions } from '@ionic/angular/common';
44
import { modalController } from '@ionic/core/components';
55
import { defineCustomElement } from '@ionic/core/components/ion-modal.js';
66

77
@Injectable()
8-
export class ModalController extends OverlayBaseController<ModalOptions, HTMLIonModalElement> {
8+
export class ModalController extends OverlayBaseController<AngularModalOptions, HTMLIonModalElement> {
99
private angularDelegate = inject(AngularDelegate);
1010
private injector = inject(Injector);
1111
private environmentInjector = inject(EnvironmentInjector);
@@ -15,10 +15,11 @@ export class ModalController extends OverlayBaseController<ModalOptions, HTMLIon
1515
defineCustomElement();
1616
}
1717

18-
create(opts: ModalOptions): Promise<HTMLIonModalElement> {
18+
create(opts: AngularModalOptions): Promise<HTMLIonModalElement> {
19+
const { injector: customInjector, ...restOpts } = opts;
1920
return super.create({
20-
...opts,
21-
delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'modal'),
21+
...restOpts,
22+
delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'modal', customInjector),
2223
});
2324
}
2425
}
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Injector, inject, EnvironmentInjector } from '@angular/core';
22
import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common';
3-
import type { PopoverOptions } from '@ionic/core/components';
3+
import type { AngularPopoverOptions } from '@ionic/angular/common';
44
import { popoverController } from '@ionic/core/components';
55
import { defineCustomElement } from '@ionic/core/components/ion-popover.js';
66

7-
export class PopoverController extends OverlayBaseController<PopoverOptions, HTMLIonPopoverElement> {
7+
export class PopoverController extends OverlayBaseController<AngularPopoverOptions, HTMLIonPopoverElement> {
88
private angularDelegate = inject(AngularDelegate);
99
private injector = inject(Injector);
1010
private environmentInjector = inject(EnvironmentInjector);
@@ -14,10 +14,11 @@ export class PopoverController extends OverlayBaseController<PopoverOptions, HTM
1414
defineCustomElement();
1515
}
1616

17-
create(opts: PopoverOptions): Promise<HTMLIonPopoverElement> {
17+
create(opts: AngularPopoverOptions): Promise<HTMLIonPopoverElement> {
18+
const { injector: customInjector, ...restOpts } = opts;
1819
return super.create({
19-
...opts,
20-
delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'popover'),
20+
...restOpts,
21+
delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'popover', customInjector),
2122
});
2223
}
2324
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Modal: Custom Injector', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/standalone/modal-custom-injector');
6+
});
7+
8+
test('should inject custom service via custom injector', async ({ page }) => {
9+
await page.locator('ion-button#open-modal-with-custom-injector').click();
10+
11+
await expect(page.locator('ion-modal')).toBeVisible();
12+
13+
const serviceValue = page.locator('#service-value');
14+
await expect(serviceValue).toHaveText('Service Value: custom-injector-value');
15+
16+
await page.locator('#close-modal').click();
17+
await expect(page.locator('ion-modal')).not.toBeVisible();
18+
});
19+
20+
test('should fail without custom injector when service is not globally provided', async ({ page }) => {
21+
await page.locator('ion-button#open-modal-without-custom-injector').click();
22+
23+
await page.waitForTimeout(1000);
24+
25+
const errorMessage = page.locator('#error-message');
26+
await expect(errorMessage).toBeVisible();
27+
await expect(errorMessage).toHaveText('Error: TestService not available');
28+
});
29+
});

0 commit comments

Comments
 (0)