I was recently working on an Angular project with ng material. During one portion of the project, we were creating a modal with mat-dialog, and as often happens with Angular components, we were injecting services into that modal.
We had an issue where the service would not inject. These services were not global services, which was the cause of the issue. This is how we solved it.
The Setup
To demonstrate this, create an Angular project with the Angular CLI, install ng material. Then create two components, a child component and a popup component. Create two services, a global service and a local service. this should be pretty easy with the Angular CLI. Just run these commands:
2ng add @angular/material
3ng generate service services/global
4ng generate service services/local
5ng generate component child
6ng generate component popup
Import the matDialogModule into the app.module.ts file
2 BrowserModule,
3 NoopAnimationsModule,
4 MatDialogModule
5 ],
You'll also need to import it:
Now, open up the local service:
2
3@Injectable({
4 providedIn: 'root'
5})
6export class LocalService {
7
8 constructor() { }
9}
The providedIn metadata will tell Angular to make this component a global provider. We don't want that, so remove that. Also add in a single variable, for our purposes a number count:
2
3@Injectable()
4export class LocalService {
5 localCount - 0;
6 constructor() { }
7}
It is now up to us to list the LocalService in a provider array when we want to use it. Pop open the global service and add a similar count variable:
2@Injectable({
3 providedIn: 'root'
4})
5export class GlobalService {
6 globalCount = 0;
7 constructor() { }
8}
This global service keeps the providedIn: 'root' config object so that Angular automatically defines it as a provider at the app module level.
Open up the app.component.html file. Replace all the contents with this:
Then your setup should be all good. You can try to ng serve the app to make sure it works.
Set up the Popup
Let's open up the popup.component.ts. Inject the two services into the constructor:
Don't forget the file imports:
2import {LocalService} from '../services/local.service';
And let's display the count in the popup.component.html file. Replace the contents with this:
2Local Count: {{localService.localCount}}!
That's all we need.
Open the Popup
Now, open the child component. Set up the localService as a provider on this component:
2 selector: 'app-child',
3 templateUrl: './child.component.html',
4 styleUrls: ['./child.component.css'],
5 providers: [LocalService]
6})
This defines LocalService as a provider that can be injected into the child component and all of that child's children components. Components elsewhere in your app, in different component hierarchies will not be able to access this service.
2 public globalService: GlobalService,
3 public localService: LocalService
4 ) { }
This injects the local service, the global service into this component along with ng material's MatDialog service. We'll use the MatDialog service to open up our popup component as a modal.
Let's write a function to open the popup:
2 this.globalService.globalCount++;
3 this.localService.localCount++;
4 this.dialog.open(PopupComponent, {
5 width: '250px',
6 height: '250px'
7 });
8
9 }
First thing it does is increment the count on both the global and local service. Then it opens the popup with the matDialog service. Add a button in the child.component.html to open this:
Run the app and click that button:
This means that the service is missing. How come?
The Really Bad Solution
We can fix this console error by going back to the local service and making it global again. Add back in the Injectable:
This doesn't fix our problem, though. Run the app and click the button:
Then close the dialog and click it again:
Even though we are incrementing the counter on the local service every time the modal opens, it is not reflected inside the popup, still showing the default count.
This can be confusing.
What is going on?
The problem is that when we make a modal with mat dialog, we are giving it a parent at that top level component. So, it has access to services at that level, but not those defined by children.
I drew up a diagram to represent:
This can get especially confusing when we are setting up multiple services using the same object. I try to avoid this as much as possible.
Set the Popups Parent
We do have a solution to this, though. We can set the parent's popup. Go to the child.component.ts and inject the ViewContainerRef:
2 public globalService: GlobalService,
3 public localService: LocalService,
4 public viewContainerRef: ViewContainerRef
5 ) { }
Be sure to import that:
The view container ref is a special angular reference to this component.
Now, when we open the dialog, we pass in the View ContainerRef:
2 width: '250px',
3 height: '250px',
4 viewContainerRef: this.viewContainerRef
5});
This tells the popup to use the current component as a parent instead of the main app component.
Rerun the app and try again:
We see that the local count variable is 1 instead of 0, so that is a positive sign. Go ahead and close the popup and open it again:
The local count is no increasing correctly, so that we see that the child component and the popup component are using the same instance of the local service.
I put together another diagram for this scenario: