angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.39k stars 6.76k forks source link

bug(mat-dialog): MatDialog doesn't have access to providers defined in a route when given a ViewContainerRef #28217

Open WoMayr opened 1 year ago

WoMayr commented 1 year ago

Is this a regression?

The previous version in which this bug was not present was

No response

Description

When providing services via a route, MatDialog.open is not able to create a component instance that injects this service

MatDialog.open is called like this

this.matDialog.open(MyServiceConsumerComponent, {
  viewContainerRef: this.viewContainerRef,
});

Structure of the routes and components looks like this: image

I did try to debug into MatDialog.open but the created Injectors were all able to get the service. So I don't really know what prevents the dialog component to be created.

It did correctly work, when declaring the Service in the providers-part of the component itself (see comment in my stack-blitz in my-feature.component.ts)

Also I'm not sure if this is an issue of Angular Material, CDK or Angular itself.

Reproduction

StackBlitz link: https://stackblitz.com/edit/angular-standalone-ts-strict-ijt5jr Steps to reproduce:

  1. Use standalone components
  2. Create a service (MyFeatureService)
  3. Create a route with a component (MyFeatureComponent) and define the service in the providers
  4. Create a component (MyServiceConsumerComponent) that consumes the service
  5. Open a dialog in MyFeatureComponent using matDialog.open(MyServiceConsumerComponent, { viewContainerRef: this.viewContainerRef })

Expected Behavior

The Dialog is able to resolve the requested service

Actual Behavior

The Dialog failes to resolve the service:

ERROR NullInjectorError: R3InjectorError(Environment Injector)[MyFeatureService -> MyFeatureService]: 
  NullInjectorError: No provider for MyFeatureService!
    at NullInjector.get (injector_token.ts:27:5)
    at R3Injector.get (r3_injector.ts:294:90)
    at R3Injector.get (r3_injector.ts:294:90)
    at ChainedInjector.get (component_ref.ts:238:23)
    at lookupTokenUsingModuleInjector (di.ts:372:8)
    at getOrCreateInjectable (di.ts:424:2)
    at Object.ɵɵdirectiveInject (shared.ts:86:5)
    at NodeInjectorFactory.MyServiceConsumerComponent_Factory [as factory] (my-service-consumer.component.ts:8:40)
    at getNodeInjectable (di.ts:661:20)
    at createRootComponent (inherit_definition_feature.ts:43:27)

Environment

kalitine commented 6 months ago

This is quite a cumbersome issue. I want to have a Dialog that relies on information from a feature-store service. Said store service is provided under a route that is dedicated to this feature.

That dialog plain does not function. Because the store performs side effects when it is being initialized, providing it on the root would trigger an initialization that would occur too early and in the wrong environment.

Additionally the dialog does not have access to the store and therefore no access to updated information - making it impossible for the dialog to know whether it should display various UI states.

The only workaround for this so far is to not update the dialogs UI and just continuously open new dialogs with new information and I refuse to make the end user sit through that hell.

Another workaround I used is to create a Service/Store (provided by the component where the dialog open function is called with viewContainerRef passed as parameter) as a relay for the route store. Thus you can inject this service into dialog and access the store through it.

DmitryEfimenko commented 6 months ago

@kalitine An example would be helpful.

Meanwhile, I'm passing an instance of the needed service to the Dialog component using the data property:

In the component opening the dialog:

interface MyDialogComponentData {
  myService: MyService;
}

class SomeComponent {
  private myService = inject(MyService);

  openDialog() {
    const data: MyDialogComponentData = { myService: this.myService };
    this.dialog.open(MyDialogComponent, { data });
  }
}

MyDialogComponent:

class MyDialogComponent {
  private data = inject<MyDialogComponentData>(MAT_DIALOG_DATA);

  someMethod() {
    // use data.myService
  }
}
DmitryEfimenko commented 6 months ago

possibly related: #25262

msibhuiyan commented 3 months ago

We faced the same issue, However importing MatDialogModule in MyFeatureComponent solves the issue.

@Component({
  standalone: true,
  templateUrl: './my-feature.component.html',
  //Import MatDialigModule here
  imports: [MatDialogModule],
})
jeandat commented 1 month ago

I confirm that material v17 has this problem. The injector created for the dialog by material is not a child of the injector provided to matDialog.open() config (either directly via the injector property or indirectly via the viewContainerRef property).