0

I've been trying for days to figure out 'the Angular paradigm', and I'm still failing to comprehend something about services that are not singletons. I cannot seem to provide a runtime-determined value to a service constructor (rather only hard-coded values).

Say I want to create a service to provide a persistent connection to some remote API for each of several objects -- on/off switches, for example. How can I cause the component to provide the unique connection URL to the service at runtime, without knowing it at compile-time? That URL is provided to the component when the component is instantiated, but I don't see how to pass it on.

//app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
  <app-toggler-control *ngFor="let control of controls" 
    name='{{ control.name }}' 
    baseUrl='{{ control.baseUrl }}'
    icon='{{ control.icon }}'
    username = '{{ control.username }}'
    password = '{{ control.password }}'
  ></app-toggler-control>
  `
})
export class AppComponent {
  title = 'testapp1';
  controls:any[] = [
    {
      'name': 'Fan1',
      'baseUrl': 'baseUrl1',
      'icon': '../assets/images/Fan.png',
      'username': 'authuser1',
      'password': 'P@$$w0rd!'
    },
    {
      'name': 'Lamp1',
      'baseUrl': 'baseUrl2',
      'icon': '../assets/images/Lamp.png',
      'username': 'authuser1',
      'password': 'P@$$w0rd!'
    },
    {
      'name': 'Valve1',
      'baseUrl': 'baseUrl3',
      'icon': '../assets/images/Valve.png',
      'username': 'authuser1',
      'password': 'P@$$w0rd!'
    },
  ]
}
//toggler-control.component.ts
import { Component, Input, OnInit } from '@angular/core';
import { TogglerCommsService } from '../toggler-comms.service'

@Component({
  selector: 'app-toggler-control',
  template: `
  <button style="background-color:{{this.currentState==true?'green':'red'}};">
    <img (click)="this.toggleState()" src="{{ this.icon }}" width="50px">{{ this.name }}
  </button>
  `,
  providers: [
    TogglerCommsService,
    {provide: 'url', useValue: 'wishes it was the baseUrl[1,2,or 3]'},  // <== obviously not right
    {provide: 'name', useValue: 'wishes it was Fan1, Lamp1 or Valve1'}
  ]
})
export class TogglerControlComponent implements OnInit {
  @Input() name:string = '';
  @Input() baseUrl:string = '';
  @Input() icon:string = '';
  @Input() username:string = '';
  @Input() password:string = '';
  currentState!:boolean;
 

  constructor(private togglerComms:TogglerCommsService) { }

  ngOnInit(): void {
    console.log('init for: ', this.name);
    this.togglerComms.getState().subscribe((val)=>{this.currentState=val;});
  }

  toggleState(): void {
    this.currentState = !this.currentState;
    this.togglerComms.setState(this.currentState);
  }

}
//toggler-comms.service.ts
import { Inject, Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';

@Injectable({
  providedIn: 'any'
})
export class TogglerCommsService {
  controlName:string = '';
  remoteEndpoint:string = '';
  mockState:boolean = Math.random() < 0.5;  //random boolean

  //I want to provide the URL upon construction/instantiation
  constructor(@Inject('url') url:string, @Inject('name') name:string) { 
    console.log("Connecting to ",url);
    this.remoteEndpoint = url;
    this.controlName = name;
  }

  getState():Observable<boolean> {
    console.log('Querying ' + this.remoteEndpoint + ' for state of ' + this.controlName + ' control.');
    return of(this.mockState).pipe(delay(1000));
  }

  setState(newState:boolean) {
    console.log('Updating ' + this.remoteEndpoint + ' with desired state of ' + this.controlName + ' control (' + (newState === true ? 'on':'off') + ').')
    this.mockState = newState;
  }
}

I'm falling into each the chicken and the egg needing the other before they can exist. The way Angular says the component is dependent on the service when I need the service to get instantiated/constructed with values provided by the component instance. How do I replace the following part of toggler-control.component.ts with something that uses variables instead?

providers: [
    TogglerCommsService,
    {provide: 'url', useValue: 'wishes it was the baseUrl[1,2,or 3]'},  // <== obviously not right
    {provide: 'name', useValue: 'wishes it was Fan1, Lamp1 or Valve1'}
  ]

There is something obvious and fundamental that I'm just not seeing.

Jimbo1987
  • 111
  • 1
  • 16
  • Usually services provide data and/or functionallity to components, as components come and go (routing etc.). Do you know these dynamic urls at compile time or just parts of them? – robert Apr 07 '21 at 17:26
  • Technically I know what they will be at compile time, but they are subject to change, and should be kept in a config file or entered via the app by the user. In any case, they certainly shouldn't be hard-coded in the app. The thing is, I'm trying to have a service per component, which seems like it is not idiomatic. So what is the preferred way to do it? Assume I need a persistent connection (technically several persistent connections -- one per component). – Jimbo1987 Apr 07 '21 at 21:19
  • Not pretty sure understand you, but if you want change the `base href` of the index you need serve a index.html with the base href changed (if you has a server in php or in .NET) or try to do it using javascript (using write and load a json. To load json file, check this:https://stackoverflow.com/questions/7346563/loading-local-json-file. Else, I imagine that you always can have a file-config.json and read it using httpClient inside Angular -possible using APP_INITIZALIZE- – Eliseo Apr 07 '21 at 21:23

2 Answers2

0

You can set whatever using normal methods -- it might be not fancy or smth, but works:

  ngOnInit(): void {
    this.togglerComms.init(...whatever...);
  }

When you write providers: [ TogglerCommsService ] for each such component new instance of service is created, so each component has its own service and can do whatever with it.

If you have several components at the same time, several services are created. https://stackblitz.com/edit/angular-6e6ry6?file=src%2Fapp%2Fapp.component.ts

This is like having field in component, but Angular injects dependecies for you, runs OnInit, OnDestroy for you and this service is accessible from child components.

Petr Averyanov
  • 9,327
  • 3
  • 20
  • 38
  • Are you saying I should call a function in the service to set the service's attributes in/from the ngOnInit() of the component? And if so, how do I ensure that one service is create per component, instead of them all sharing one and overwriting those attributes? Notice I'm not understanding something fundamental about how the service gets instantiated and 'supplied' to the component. – Jimbo1987 Apr 07 '21 at 21:26
  • Thanks Petr, but still your example service is not instantiated with anything provided by the component, which is the thing I can't get past. To put it another way: The only variable involved in your service instantiation stems from the hard-coded value in the service itself, rather than from a value provided at runtime. It seems that the Angular paradigm requires that the service exist first, independent of the control, but I'm trying to make it be the other way around because I need to supply a variable (the URL to which the service should connect, in this case) to the service at runtime. – Jimbo1987 Apr 08 '21 at 14:55
0

As I understand it now, while you can use another service to provide values to the constructor of a service, a service necessarily gets constructed before the component that uses it, and said service's token is injected into the construction of the component. Thus, the component cannot supply construction arguments to the service. Instead, it seems the way to do it is, as Petr alluded to, just set the values in the service instance in the OnInit() function of the consuming component.

ngOnInit(): void {
    console.log('init for: ', this.name);
    this.togglerComms.controlName = this.name;
    this.togglerComms.remoteEndpoint = this.baseUrl;
    this.togglerComms.getState().subscribe((val)=>{this.currentState=val;});
  }

Remove all of the injection stuff in the constructor of the service, leaving the TogglerCommsService constructor empty.

  //I want to provide the URL upon construction/instantiation <== too bad!
  constructor() { //@Inject('url') url:string, @Inject('name') name:string) { 
    //console.log("Connecting to ",url);
    //this.remoteEndpoint = url;
    //this.controlName = name;
  }

And remove those extra providers from the TogglerControlComponent specification.

providers: [
    TogglerCommsService,
    //{provide: 'url', useValue: 'wishes it was the baseUrl[1,2,or 3]'}, 
    //{provide: 'name', useValue: 'wishes it was Fan1, Lamp1 or Valve1'}
  ]

Lastly, we ensure that the components each get their own instance of the service (instead of all sharing one) by listing the service in the providers array in the metadata of the component, as we see above was already correct. That is called "sandboxing" in this discussion: Angular DI Guide

Jimbo1987
  • 111
  • 1
  • 16