简体   繁体   中英

Angular 8: How to provide different service instances to multiple instances of the same component

I am trying to create reusable d3-based dashboard components in Angular 8. I want to create components (like a barchart) that can be packaged in a module and reused without any modification of the component code (not even the constructor parameters). I also want to allow x number of sibling instances that display different data.

I have a working component and I have factored its api into input parameters for simple display config and a service interface that all components would need for the data/interactions. I implemented that service interface as an abstract base class and the component takes that base class as a constructor parameter (user/developer cannot modify the constructor params or any component code in my scenario).

This leaves me with the problem - how to provide different implementation of the service base class to different component instances without modifying the component constructor params.

I have also already tried creating an abstract base class for the bar chart and then creating derived bar chart instances that only differ by taking derived barchartservice instances, but the problem there is that the template and styles are not inherited from the component base class.

export class BarChartComponent implements AfterViewInit
{
    ...

    public constructor(public service: BarChartService) {}

    public ngAfterViewInit() {
        ...

        this.service.dataInit$.subscribe(() => {
            let data = this.service.barChartData;
            this.drawChart(data);
        });

        this.service.dataRefresh$.subscribe(() => {
            let data = this.service.barChartData;
            this.drawChart(data);
        });
    }

    private drawChart(data): void {
        ...
    }
}

@Injectable()
export abstract class BarChartService {

    abstract barChartData: any;

    abstract dataInit$: Observable<boolean>;
    abstract dataRefresh$: Observable<boolean>;

    abstract barChartSelect(string): void;
    abstract barChartXValue: (d: any) => any;
    abstract barChartYValue: (d: any) => any;
}

Ultimately I just want reusable multi-instance components that can display different data. Any insight would be greatly appreciated.

For clarity, this is what a barChartService derived class is doing (Approval service parameter hold a backend access logic and crossfilter that is shared across the dashboard):

@Injectable()
export class ApprovalsBarChartService implements BarChartService {

    private init = new Subject<boolean>();
    dataInit$ = this.init.asObservable();

    private refresh = new Subject<boolean>();
    dataRefresh$ = this.refresh.asObservable();

    public get barChartData(): any {
        return this.service.approvalsGroupedByApprover.all();
    }

    public barChartXValue = function (d) { return d.key; };
    public barChartYValue = function (d) { return d.value; };

    public constructor(public service: ApprovalService) {   

        this.service.init$.subscribe(() => {
            this.init.next(true);
        });

        this.service.refresh$.subscribe(() => {
          this.refresh.next(true);
        });
    } 

    public barChartSelect(processor: string): void {
        this.service.approvalsIndexedByApprover.filter(processor);
        this.service.selectedProcessor = processor;
        this.service.selectItems = +this.service.xfilter.groupAll().reduceCount().value();
        this.service.refreshCharts();
    }
}

Does BarChartService really need to be a service? It seems to me that if you don't need to reuse the same instance of the service between different components, then you don't need a service.

So you can instantiate the "service" inside of the constructor of your component, instead of injecting it. So instead of:

public constructor(public service: BarChartService) {}

You can:

public service: BarChartService;
public constructor() {
    this.service = new BarChartService();
}

Note that because that class is abstract you'd need to instantiate the concrete subclasses.

Now, if you really want to inject different instances from your service into different components (because some of these services will be shared between some components for example), then you can provide a different injection token for each instance, and use the injection tokens in the constructors of your component to select which instance you want:

Declare two injection tokens:

export const MyToken1 = new InjectionToken<BarChartService>('MyToken1');
export const MyToken2 = new InjectionToken<BarChartService>('MyToken2');

Assign a different service instance to each token:

@NgModule(...) 
export class ... {
    ...
    providers: [
        {provide: MyToken1, useValue: new MyServiceInstance(1, 2, 3)},
        {provide: MyToken2, useValue: new MyServiceInstance(4, 5, 6)},
    ]
}

And then in the components, select which service you want injected:

public constructor(@Inject(MyToken1) public service: BarChartService) {}

What I was originally trying to do here is not possible.

Thankfully however, it is also not necessary:

Solution: The service (BarChartService) is not needed. All of the things defined in the BarChartService can and should be passed in as @Input parameters with the exception of BarChartSelect which should be an @Output event that is caught in the parent and invokes a method on the ApprovalService which is essentially a data retrieval service that holds the crossfilter for the entire dashboard (not component specific). I was unaware that we can pass functions into the component as input parameters to customize the behavior.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM