Seperate instances of an angular service per (set of) component(s)
Asked Answered
C

3

21

TLDR:

How do I use one instance of an Angular (6) service for one set of (instances of) components (and directives) and another one for another set of the same components.

Or with more information:

I am currently working on adding a sorting feature to tables in my Angular 6 based application. Because I am using custom styles (materialize based) most libraries aren't working for me. I found this great example though which is totally independent of the styles used.

It creates a SortableColumnComponent that gets added to each <th> header and a SortableTableDirective that gets added to the <table> element. They communicate via a SortService that basically just provides a RxJS subject to notify other columns when the sorting direction/attribute changes.

The problem here is that this works great as long as only one sortable table is shown at a time. Once you add more though, sorting can only be applied to one at a time (as they all share the same service).

According to the angular docs a service becomes a singleton automatically when you inject it only into the application root (which I did). So instead I tried to inject it only into the sortableColumn:

@Component ({
    selector: '[sortable-column]',
    templateUrl: './sortable-column.component.html'
    providers: [SortService]
})

but then each column seems to get its own version of the service and can be sorted at the same time (which obviously does not work as intended).

So the overall question is: how can I assign one instance of an angular service to one (instance of a) component (the SortableTableDirective) and the matching SortableColumnComponent components and another instance of that same service to other tables.

To maybe make this clearer, this is what I am trying to achieve:

-------------------------------------------------------------------------
| page                                                                  |
|                                                                       |
| table 1 - SortableTableDirective - using instance 1 of sortingservice |
|   th 1 - sortablecolumncomponent - using instance 1 of sortingservice |
|   th 2 - sortablecolumncomponent - using instance 1 of sortingservice |
| ....                                                                  |
| table 2 - SortableTableDirective - using instance 2 of sortingservice |
|   th 1 - sortablecolumncomponent - using instance 2 of sortingservice |
|   th 2 - sortablecolumncomponent - using instance 2 of sortingservice |
-------------------------------------------------------------------------

Or is there a way to bind certain column components to the table directive directly and just remove the service? There seems to be this one concept of "linking" that I am missing here.

Concordance answered 9/8, 2018 at 20:6 Comment(0)
C
22

After reading some more I found another part of the angular docs (last paragraph) that finally got me on the right track: you can limit the provider scope by loading it in a component. Then its available only to the component and its children (!). Exactly what I need in this case.

When I first tried it I made the mistake of loading the service in the column and thereby providing a different instance to each one. Loading it in the table directive works great as this provides one instance to the table and all child elements (e.g. the columns). Another instance of the table directive then loads another instance of the service which allows me to sort all tables independently from each other.

Code:

@Directive({
  selector: '[sortable-table]',
  providers: [SortService]
})
Concordance answered 10/8, 2018 at 17:18 Comment(4)
So essentially by providing the service in the actual class annotation then a new instance of that service is provided for each new instance of that class.Externalize
But how does Angular know that SortableColumnComponent is a child/nested component for SortableTableDirective?Priggish
@AlexeyGrinko Angular creates a tree of injectors. So the nested component injector is searched for a provider, and if that fails, it's parent's injector is searched etc. until a provider is found.Contuse
Also if you wanna use one instance of service for few components make sure you don't use Subject() to transfer data. Instead use ReplaySubject(1) to avoid data lossHine
Z
2

To help someone in the future, now we can use Hierarchical Dependency Injection

In Table component, put your service in [providers] and mark the dependence injection with @Host decorator. In the children, mark with @SkipSelf.

Now, the children will ignore their "self" instances and starts search for a service in the parent marked with @Host.

See te sample:

Parent

@Component({
    selector: 'table-component',
    providers: [TableService],
    templateUrl: './table.component.html',
    styleUrls: ['./table.component.scss'],
})
export class TableComponent {
    constructor(@Host() private tableService: TableService)
}

Child

@Component({
    selector: 'child-component',
    templateUrl: './child.component.html',
    styleUrls: ['./child.component.scss'],
})
export class ChildComponent{
    constructor(@SkipSelf() private tableService: TableService)
}
Zackzackariah answered 31/5, 2022 at 21:11 Comment(0)
M
-4

You should not provide the service in the component but in the AppModule and 'inject' your service by creating an attribute in the constructor.

service.ts

@Injectable({
  providedIn: 'root'
})

export  class MyService {
}

EDIT:

component.ts

@Component ({
    selector: '[sortable-column]',
    templateUrl: './sortable-column.component.html'
})

constructor(private myService: MyService) {}

appModule.ts

@NgModule({
declarations:[component], 
imports: [YourModules], 
providers: [MyService],
bootstrap: [component]
})
export class AppModule {}
Maquis answered 9/8, 2018 at 20:18 Comment(6)
Is that not exactly what I first did and what the article I linked to describes? In that case the service becomes a singleton and only one table is sortable at a time. That is why I tried to directly inject it. Or am I missing something else here?Concordance
This is the correct way to inject services, if your app still don't have the desired behavior, the issue may be in another part of the code.Maquis
So there is no way to use multiple instances of a service in the same part of the application?Concordance
oh I think I misunderstood your question at the beginning, so you want a new instance for each instance of the component? I have edited my answer you can check if this works.Maquis
Ah ok, so you simply create a new instance in that case. That instance can't be shared with other components at all then, right? I added a new "picture" to my initial question that might clarify it. Perhaps you can take a look at that.Concordance
Is that providers unnecessary in your edit version. If you create instance with new you are not using DI. In this scenario there is no point that it is even Service except if you are injecting it somewhere else as normal Service.Vaquero

© 2022 - 2024 — McMap. All rights reserved.