Passing two different instances of a service down to two different instances of a component, one through a router-outlet and one through a MatDialog

Viewed 254

Fully rephrased question:

I tried to simplify the problem, cause I understand the way I initially wrote it down is pretty convoluted; what follows is a simpler representation of the problem, albeit a slightly different representation of the problem.

I need a way to create two instances of a service in a top level component (namely the “app” component), and to pass each instance down to two different instances of another component (namely the "Client" component):

  • The first instance of the "Client" component will be rendered within a <router-outlet>;
  • The second instance of the "Client" component will be rendered within a MatDialog;

The service itself will just hold two methods and two observables in order to achieve parent-child communication, and implementation shouldn’t really matter in this case, but it would go something like this:

interface ChildInput {
    someData: number;
}

interface ParentInput {
    someData: string;
}

@Injectable
export class ComService {

    pushToChild(data: ParentInput) {
        // code to push data to the "child" observable
    }

    pushToParent(data: ChildInput) {
        // code to push data to the "parent" observable
    }

    // code to instantiate two observables, one for the parent to subscribe to and one for the child to subscribe to

}

What should be noted though, is that I need to track down which “child” component is sending which data; hence a shared instance of the service won’t work in this case.

Here's how the top-level component's template is structured:

<a [routerLink]="['/new-client']">
    New client
</a> <!-- this will render the "Client" component within router-outlet -->

<router-outlet></router-outlet>

<button (click)="openClientPopup()">
    Add new client
</button> <!-- this will render the "Client" component within a MatDialog instance created by the openClientPopup() method -->

As I said creating a singleton instance at the top level component and injecting it into the "child" module the classic way won't work, because each instance of the "child" module will exchange data with the parent that should be bound, in the parent, to that specific instance of the component itself, and tracking down the sender by using some sort of id or something like that feels unnecessarily convoluted (It might not be though if that's the only way to go about doing it; I really don't know, and that's why I'm asking into first place);

On the other hand, while I could easily pass a service instance down to a component rendered within a MatDialog (there are actually multiple ways to do it), I'm not aware of a way to pass an instance of a service down to a component rendered within a <router-outlet>.

Any suggestion?

2 Answers

Provide the service directly to the parent using the providers parameter. Then Angular will inject that same instance into its children via their constructor.

Parent

@Component({
  providers: [MyService]
})
class MyParentComponent {
    constructor(private myService: MyService)
}

Child

@Component({})
class MyChildComponent {
    constructor(private myService: MyService)
}

Edit:

So you are saying you want one parent, two children, two services; parent and respective children can share their service? If so, you can use a factory function.

Parent Component

@Component({
    providers: [{
        provide: MyService,
        useFactory: () => (id: string) => new MyService(id)
    }]
})
export class ParentComponent {

    public service_a: MyService;
    public service_b: MyService;

    constructor(@Inject(MyService) myService: any) {
        this.service_a = myService('A');
        this.service_b = myService('B');
    }
}

Child Component

export class ChildComponent implements OnInit {

    @Input() service: MyService

    constructor() { }

    ngOnInit(): void {
        this.service.log();
    }
}

Service

@Injectable()
export class MyService {

    constructor(private id: string) { }

    log() {
        console.log(this.id);
    }

}

I know the perceived solution is to create separate instances of the service class, but I don't think this is a good idea as it adds complications by changing the intended use case of service singletons.

However, if you require to have one service and one component, the easiest solution may be to add an additional parameter to the service methods so it knows the context as to which component instance is invoking it.

The flag for your component can come from the data object that can be passed inside the MatDialog. If the component is loaded through the router, the MatDialog data object will be undefined.

Component file that loads MatDialog

public openClientPopup(){
  const dialogRef = this.dialog.open(ClientComponent, {
    //...dialog options,
    data: {
      isDialog: true
    }
  });

client.component.ts

private source: 'dialog' | 'router';
constructor(
  @Inject(MAT_DIALOG_DATA) private data: { isDialog: boolean }
){
  // Assign the source based on whether the MatDialog data object exists
  this.source = !!this.data?.isDialog ? 'dialog' : 'router';
}

pushToChildService(data){
  this.comService.pushToChild(data, this.source);
}

pushToParentService(data){
  this.comService.pushToParent(data, this.source);
}

Then finally in your singleton service file.

@Injectable
export class ComService {

    pushToChild(data: ParentInput, source:'dialog'|'router') {
        // You now know the source of the data.
    }

    pushToParent(data: ChildInput, source:'dialog'|'router') {
        // You now know the source of the data.
    }

    // code to instantiate two observables, one for the parent to subscribe to and one for the child to subscribe to

}

If this makes your two service methods too complex, you can define four service methods (one for each case). Then in the component, use the MatDialog data flag to invoke the necessary method.

Related