diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index 447fb3e1c14..f93646302c7 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -9,6 +9,7 @@ export { AngularDelegate, bindLifecycleEvents, IonModalToken } from './providers export type { IonicWindow } from './types/interfaces'; export type { ViewDidEnter, ViewDidLeave, ViewWillEnter, ViewWillLeave } from './types/ionic-lifecycle-hooks'; +export type { AngularModalOptions, AngularPopoverOptions } from './types/overlay-options'; export { NavParams } from './directives/navigation/nav-params'; diff --git a/packages/angular/common/src/providers/angular-delegate.ts b/packages/angular/common/src/providers/angular-delegate.ts index dd4964ef1c1..bde802ab115 100644 --- a/packages/angular/common/src/providers/angular-delegate.ts +++ b/packages/angular/common/src/providers/angular-delegate.ts @@ -36,7 +36,8 @@ export class AngularDelegate { create( environmentInjector: EnvironmentInjector, injector: Injector, - elementReferenceKey?: string + elementReferenceKey?: string, + customInjector?: Injector ): AngularFrameworkDelegate { return new AngularFrameworkDelegate( environmentInjector, @@ -44,7 +45,8 @@ export class AngularDelegate { this.applicationRef, this.zone, elementReferenceKey, - this.config.useSetInputAPI ?? false + this.config.useSetInputAPI ?? false, + customInjector ); } } @@ -59,7 +61,8 @@ export class AngularFrameworkDelegate implements FrameworkDelegate { private applicationRef: ApplicationRef, private zone: NgZone, private elementReferenceKey?: string, - private enableSignalsSupport?: boolean + private enableSignalsSupport?: boolean, + private customInjector?: Injector ) {} attachViewToDom(container: any, component: any, params?: any, cssClasses?: string[]): Promise { @@ -93,7 +96,8 @@ export class AngularFrameworkDelegate implements FrameworkDelegate { componentProps, cssClasses, this.elementReferenceKey, - this.enableSignalsSupport + this.enableSignalsSupport, + this.customInjector ); resolve(el); }); @@ -131,7 +135,8 @@ export const attachView = ( params: any, cssClasses: string[] | undefined, elementReferenceKey: string | undefined, - enableSignalsSupport: boolean | undefined + enableSignalsSupport: boolean | undefined, + customInjector?: Injector ): any => { /** * Wraps the injector with a custom injector that @@ -158,7 +163,7 @@ export const attachView = ( const childInjector = Injector.create({ providers, - parent: injector, + parent: customInjector ?? injector, }); const componentRef = createComponent(component, { diff --git a/packages/angular/common/src/types/overlay-options.ts b/packages/angular/common/src/types/overlay-options.ts new file mode 100644 index 00000000000..e89add1d65e --- /dev/null +++ b/packages/angular/common/src/types/overlay-options.ts @@ -0,0 +1,10 @@ +import type { Injector } from '@angular/core'; +import type { ModalOptions, PopoverOptions } from '@ionic/core/components'; + +export interface AngularModalOptions extends ModalOptions { + injector?: Injector; +} + +export interface AngularPopoverOptions extends PopoverOptions { + injector?: Injector; +} diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index 3eefb6c46ee..761daebe41d 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -32,6 +32,7 @@ export { ViewDidEnter, ViewDidLeave, } from '@ionic/angular/common'; +export type { AngularModalOptions, AngularPopoverOptions } from '@ionic/angular/common'; export { AlertController } from './providers/alert-controller'; export { AnimationController } from './providers/animation-controller'; export { ActionSheetController } from './providers/action-sheet-controller'; diff --git a/packages/angular/src/providers/modal-controller.ts b/packages/angular/src/providers/modal-controller.ts index 51edf69cf36..127e0f71897 100644 --- a/packages/angular/src/providers/modal-controller.ts +++ b/packages/angular/src/providers/modal-controller.ts @@ -1,10 +1,10 @@ import { Injector, Injectable, EnvironmentInjector, inject } from '@angular/core'; import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common'; -import type { ModalOptions } from '@ionic/core'; +import type { AngularModalOptions } from '@ionic/angular/common'; import { modalController } from '@ionic/core'; @Injectable() -export class ModalController extends OverlayBaseController { +export class ModalController extends OverlayBaseController { private angularDelegate = inject(AngularDelegate); private injector = inject(Injector); private environmentInjector = inject(EnvironmentInjector); @@ -13,10 +13,11 @@ export class ModalController extends OverlayBaseController { + create(opts: AngularModalOptions): Promise { + const { injector: customInjector, ...restOpts } = opts; return super.create({ - ...opts, - delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'modal'), + ...restOpts, + delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'modal', customInjector), }); } } diff --git a/packages/angular/src/providers/popover-controller.ts b/packages/angular/src/providers/popover-controller.ts index 96c5c8d5a82..348a8e47ead 100644 --- a/packages/angular/src/providers/popover-controller.ts +++ b/packages/angular/src/providers/popover-controller.ts @@ -1,9 +1,9 @@ import { Injector, inject, EnvironmentInjector } from '@angular/core'; import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common'; -import type { PopoverOptions } from '@ionic/core'; +import type { AngularPopoverOptions } from '@ionic/angular/common'; import { popoverController } from '@ionic/core'; -export class PopoverController extends OverlayBaseController { +export class PopoverController extends OverlayBaseController { private angularDelegate = inject(AngularDelegate); private injector = inject(Injector); private environmentInjector = inject(EnvironmentInjector); @@ -12,10 +12,11 @@ export class PopoverController extends OverlayBaseController { + create(opts: AngularPopoverOptions): Promise { + const { injector: customInjector, ...restOpts } = opts; return super.create({ - ...opts, - delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'popover'), + ...restOpts, + delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'popover', customInjector), }); } } diff --git a/packages/angular/standalone/src/index.ts b/packages/angular/standalone/src/index.ts index 4dc766c1d41..89f95298878 100644 --- a/packages/angular/standalone/src/index.ts +++ b/packages/angular/standalone/src/index.ts @@ -28,6 +28,7 @@ export { ViewWillLeave, ViewDidLeave, } from '@ionic/angular/common'; +export type { AngularModalOptions, AngularPopoverOptions } from '@ionic/angular/common'; export { IonNav } from './navigation/nav'; export { IonCheckbox, diff --git a/packages/angular/standalone/src/providers/modal-controller.ts b/packages/angular/standalone/src/providers/modal-controller.ts index 07e406ec5d6..ad3329e2987 100644 --- a/packages/angular/standalone/src/providers/modal-controller.ts +++ b/packages/angular/standalone/src/providers/modal-controller.ts @@ -1,11 +1,11 @@ import { Injector, Injectable, EnvironmentInjector, inject } from '@angular/core'; import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common'; -import type { ModalOptions } from '@ionic/core/components'; +import type { AngularModalOptions } from '@ionic/angular/common'; import { modalController } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-modal.js'; @Injectable() -export class ModalController extends OverlayBaseController { +export class ModalController extends OverlayBaseController { private angularDelegate = inject(AngularDelegate); private injector = inject(Injector); private environmentInjector = inject(EnvironmentInjector); @@ -15,10 +15,11 @@ export class ModalController extends OverlayBaseController { + create(opts: AngularModalOptions): Promise { + const { injector: customInjector, ...restOpts } = opts; return super.create({ - ...opts, - delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'modal'), + ...restOpts, + delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'modal', customInjector), }); } } diff --git a/packages/angular/standalone/src/providers/popover-controller.ts b/packages/angular/standalone/src/providers/popover-controller.ts index 395b4dbdb70..f2b16393d0b 100644 --- a/packages/angular/standalone/src/providers/popover-controller.ts +++ b/packages/angular/standalone/src/providers/popover-controller.ts @@ -1,10 +1,10 @@ import { Injector, inject, EnvironmentInjector } from '@angular/core'; import { AngularDelegate, OverlayBaseController } from '@ionic/angular/common'; -import type { PopoverOptions } from '@ionic/core/components'; +import type { AngularPopoverOptions } from '@ionic/angular/common'; import { popoverController } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-popover.js'; -export class PopoverController extends OverlayBaseController { +export class PopoverController extends OverlayBaseController { private angularDelegate = inject(AngularDelegate); private injector = inject(Injector); private environmentInjector = inject(EnvironmentInjector); @@ -14,10 +14,11 @@ export class PopoverController extends OverlayBaseController { + create(opts: AngularPopoverOptions): Promise { + const { injector: customInjector, ...restOpts } = opts; return super.create({ - ...opts, - delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'popover'), + ...restOpts, + delegate: this.angularDelegate.create(this.environmentInjector, this.injector, 'popover', customInjector), }); } } diff --git a/packages/angular/test/base/e2e/src/standalone/modal-custom-injector.spec.ts b/packages/angular/test/base/e2e/src/standalone/modal-custom-injector.spec.ts new file mode 100644 index 00000000000..f06224042eb --- /dev/null +++ b/packages/angular/test/base/e2e/src/standalone/modal-custom-injector.spec.ts @@ -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'); + }); +}); diff --git a/packages/angular/test/base/e2e/src/standalone/popover-custom-injector.spec.ts b/packages/angular/test/base/e2e/src/standalone/popover-custom-injector.spec.ts new file mode 100644 index 00000000000..255e59575f0 --- /dev/null +++ b/packages/angular/test/base/e2e/src/standalone/popover-custom-injector.spec.ts @@ -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'); + }); +}); 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 667ef672e8b..197563dfb32 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 @@ -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) }, 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 6dbad643eb2..058cec23c09 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 @@ -110,6 +110,11 @@ Programmatic Modal Test + + + Modal Custom Injector Test + + Overlay Controllers Test @@ -120,6 +125,11 @@ Popover Test + + + Popover Custom Injector Test + + diff --git a/packages/angular/test/base/src/app/standalone/modal-custom-injector/modal-custom-injector.component.ts b/packages/angular/test/base/src/app/standalone/modal-custom-injector/modal-custom-injector.component.ts new file mode 100644 index 00000000000..e889f108293 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-custom-injector/modal-custom-injector.component.ts @@ -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: ` + + + Modal Custom Injector Test + + + + + Open Modal with Custom Injector + + + Open Modal without Custom Injector + + + `, + 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'); + } + } +} diff --git a/packages/angular/test/base/src/app/standalone/modal-custom-injector/modal/modal.component.ts b/packages/angular/test/base/src/app/standalone/modal-custom-injector/modal/modal.component.ts new file mode 100644 index 00000000000..8c97d4a89e4 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-custom-injector/modal/modal.component.ts @@ -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: ` + + + Modal with Custom Injector + + Close + + + + +

Service Value: {{ serviceValue }}

+
+ `, + 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(); + } +} diff --git a/packages/angular/test/base/src/app/standalone/modal-custom-injector/test.service.ts b/packages/angular/test/base/src/app/standalone/modal-custom-injector/test.service.ts new file mode 100644 index 00000000000..c5fe87d1a48 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-custom-injector/test.service.ts @@ -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; + } +} diff --git a/packages/angular/test/base/src/app/standalone/popover-custom-injector/popover-custom-injector.component.ts b/packages/angular/test/base/src/app/standalone/popover-custom-injector/popover-custom-injector.component.ts new file mode 100644 index 00000000000..66b74033ef1 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/popover-custom-injector/popover-custom-injector.component.ts @@ -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: ` + + + Popover Custom Injector Test + + + + + Open Popover with Custom Injector + + + `, + 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(); + } +} diff --git a/packages/angular/test/base/src/app/standalone/popover-custom-injector/popover/popover.component.ts b/packages/angular/test/base/src/app/standalone/popover-custom-injector/popover/popover.component.ts new file mode 100644 index 00000000000..2be85458c5a --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/popover-custom-injector/popover/popover.component.ts @@ -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: ` + +

Service Value: {{ serviceValue }}

+
+ `, + standalone: true, + imports: [IonContent] +}) +export class PopoverCustomInjectorPopoverComponent implements OnInit { + private testService = inject(TestService); + serviceValue = ''; + + ngOnInit() { + this.serviceValue = this.testService.getValue(); + } +} diff --git a/packages/angular/test/base/src/app/standalone/popover-custom-injector/test.service.ts b/packages/angular/test/base/src/app/standalone/popover-custom-injector/test.service.ts new file mode 100644 index 00000000000..c5fe87d1a48 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/popover-custom-injector/test.service.ts @@ -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; + } +}