ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
51.02k stars 13.51k forks source link

bug: Angular NullInjector when referencing Injection Tokens in modal components #28817

Closed dexster closed 9 months ago

dexster commented 9 months ago

Prerequisites

Ionic Framework Version

v7.x

Current Behavior

When an InjectionToken is provided in a context that is not the root context, it is not available in a component created using the ModalController. A NullInjector error is thrown.

Expected Behavior

The token should be available in components created via ModalController.

An injection parameter could be added to the modal options. Modifying the create method like this fixed the issue for me.

create(opts) {
       return super.create({
           ...opts,
           // added the opts.injector. 
           delegate: this.angularDelegate.create(this.environmentInjector, opts.injector || this.injector, 'modal'),
       });
}

// In the component
private environmentInjector = inject(EnvironmentInjector);

constructor(private modalCtrl: ModalController) { }

async openModal() {
    const modal = await this.modalCtrl.create({
      component: ModalExampleComponent,
      injector: this.environmentInjector
});

Steps to Reproduce

  1. Create an InjectionToken and add it to a route provider
    
    export const TEST_DIALOG = new InjectionToken('TEST_DIALOG');

export const remoteRoutes: Route[] = [ { path: '', providers: [{provide: TEST_DIALOG, useValue: 'test'}], loadComponent: () => import('./dialog.component').then(m => m.DialogComponent) } ];


2. Open a modal that uses the token

```ts
@Component({
  selector: 'my-lib',
  standalone: true,
  imports: [CommonModule],
  template: `
    <button (click)="openModal()">Open</button>`,
})
export class DialogComponent {

  private environmentInjector = inject(EnvironmentInjector);

  constructor(private modalCtrl: ModalController) {}

 async openModal() {
    const modal = await this.modalCtrl.create({
      component: ModalExampleComponent,
    });
    modal.present();
  }
}

@Component({
  selector: 'my-lib',
  standalone: true,
  imports: [CommonModule],
  template: `
    <button (click)="openModal()">Open</button>`,
})
export class DialogComponent {

  constructor(private modalCtrl: ModalController) {
  }

  async openModal() {
    const modal = await this.modalCtrl.create({
      component: ModalExampleComponent
    });
    modal.present();
  }
}

@Component({
  selector: 'my-modal',
  template: `<h1>dialog</h1>`,
  standalone: true
})
export class ModalExampleComponent {

  // nullinjector error thrown here 
  testDialog = inject(TEST_DIALOG);

  constructor() {
    console.log('TEST DIALOG: ', this.testDialog);
  }
}

Code Reproduction URL

No response

Ionic Info

Ionic:

Ionic CLI : 7.2.0

Utility:

cordova-res : not installed globally native-run : not installed globally

System:

NodeJS : v20.9.0 npm : 10.1.0 OS : macOS Unknown

Additional Information

No response

ionitron-bot[bot] commented 9 months ago

Thanks for the issue! This issue has been labeled as needs reproduction. This label is added to issues that need a code reproduction.

Please reproduce this issue in an Ionic starter application and provide a way for us to access it (GitHub repo, StackBlitz, etc). Without a reliable code reproduction, it is unlikely we will be able to resolve the issue, leading to it being closed.

If you have already provided a code snippet and are seeing this message, it is likely that the code snippet was not enough for our team to reproduce the issue.

For a guide on how to create a good reproduction, see our Contributing Guide.

dexster commented 9 months ago

Repro here.

Open the console to view the error. Follow the comments in the main.ts and app.routes.ts

https://stackblitz.com/edit/angular-lfuuzo?file=src%2Fmain.ts,src%2Fapp%2Fapp.routes.ts,src%2Fapp%2Fdialog.component.ts

liamdebeasi commented 9 months ago

Thanks for the repo. I can reproduce the reported issue, but this is not a bug in Ionic Framework. Adding the provider on the route level breaks inside of the modal because modals are intentionally created outside of any routing. These components are temporary views and are dependent on the presenting context. Adding the provider in main.ts works because the injection token is in the same context that the modal is in.

In terms of avoiding this error, I recommend providing the token in main.ts. Alternatively, you can use the options parameter to define a factory for the token: https://angular.io/api/core/InjectionToken#constructor

dexster commented 9 months ago

@liamdebeasi I understand this is not an Ionic bug but I do believe this functionality is critical going forward. With the prevalence of micro frontends and standalone apps in Angular this could be a blocker for many apps.

The alternatives suggested are not possible as my app is a remote app inside a micro frontend shell app. Adding providers in the entry route is how the app bootstraps when using a standalone app (previously a module would be passed to the shell app). Asking the owners of the shell app to add my providers is not feasible

The injection token I used above was just for repro purposes, the actual one in my app sits in a 3rd party lib so I cannot modify it.

I believe the solution I provided in my original comment, where we can pass the injection context, could work. I modified the Ionic code and tested it and it solved my issue. I could attempt a proper fix with a PR?

Angular Material provides this option with the injector property in their dialog. https://material.angular.io/components/dialog/api#MatDialogConfig

liamdebeasi commented 8 months ago

Are you able to use an inline modal instead of a controller modal?

dexster commented 7 months ago

Not in this case. We create reusable modals using the controller

kamilchlebek commented 7 months ago

@liamdebeasi we're experiencing the similar issue. Do you plan to add injector option to ModalController ?

Update. I just found workaround that works for us.

In our case it started to occur after migrating to ionic standalone components. Previously importing IonicModule in lazily loaded module was good enough. As we moved to standalone components adding ModalController to list of providers in lazily loaded module seems to fix the issue.

If you plan to investigate it deeper here's an example from ng-bootstrap library that seems to handle injectors better: https://github.com/ng-bootstrap/ng-bootstrap/blob/b5d5febde5691286211dd2401c2cb5334db0851f/src/modal/modal-stack.ts#L86-L88

ionitron-bot[bot] commented 6 months ago

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.