1

I'm working on an Angular 2 app, where I want to store some "global variables" (state) across different components. This can be the user's choice of year and customerId etc. All components need to access these, for use in GET calls to the API/backend.

I have implemented router parameters, so it is possible to pass parameters into a component this way, but I find this easier to work with than having to maintain state by forwarding parameters through the router on every click. Hope this makes sense.

Anyway, following this question on StackOverflow, I've set up the following service:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/share';
import { Observer } from 'rxjs/Observer';

interface params {
    year: string;
    customerId: number;
};

@Injectable()
export class ParameterService {

    // initial (test) values:
    p: params = {
        year: "2017",
        customerId: 101
    };

    constructor() {}
}

This seems to work, two ways, allowing my components to get and set parameters like this:

    // get global value
    this.year = this._parameterService.p['year'];

    // set global value
    this._parameterService.p['year'] = "2018";

And data binding works, too. This is due to p being a class (object), rather than just accessing string year directly, as I understand it. If I change the year value from component1, then component2 (below) will reflect the change:

  <h1>Year = {{this._parameterService.p['year']}}</h1>

All I need to do is inject the ParameterService into any component that needs it, and I'm done. However, I can't figure out how to extend this such that a component will reload (execute another GET call) with the changed parameter.

For router parameters, a component "subscribes" to the params in its ngOnInit method, causing it to "refresh" whenever a route changes:

ngOnInit(): void {
    this.sub = this._route.params.subscribe(params => {
        this.customerId = params['customerId'];
        this.year = params['year'];

        this.resetComponentState(); // based on new parameter this time
    }); 
}

resetComponentState(): void { 
    this._otherService.getDataFromApi(this.year)
        .then(
        myData => this.myData = myData,
        error => this.errorMessage = <any>error);
}

How can I achieve the same thing based on the ParameterService? Is it possible to somehow "bind" this.year with this._parameterService.p['year'] in resetComponentState()? Btw, I have tried doing just that, but to no effect.

UPDATE here is the full component which I want to refresh on parameter change (not just router parameter change):

import { Component, Input, OnInit } from '@angular/core';
import { MyData} from './mydata';
import { ActivatedRoute } from '@angular/router';
import { OtherService } from './other.service';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { Headers, RequestOptions } from '@angular/http';
import { ParameterService } from '../parameter.service';
import { LocalStorageService } from 'angular-2-local-storage';

@Component({
    templateUrl: 'app/test/test.component.html'
})

export class TestComponent implements OnInit {

    subscription: any;
    customerId: string;

    // set getter as a property on the variable `year`:
    ////year: string;
    get year(): string {
      console.log("test.component.get()");
      const value = this._parameterService.p['year'] + "";
      // Any other code you need to execute when the data is changed here
      this.getDataFromApi();
      return value;
    }

    private sub: any;

    errorMessage: string;
    data: MyData[];
    constructor(private _otherService: OtherService,
        private _parameterService: ParameterService,
        private _route: ActivatedRoute,
        private _localStorageService: LocalStorageService) { }

    getDataFromApi() {

        // store route parameters in Local Storage
        // if none found, try reading from Local Storage
        if (this.customerId == null)
            this.customerId = this._localStorageService.get('customerId') + "";
        //if (this.year== null)
        //    this.year = this._localStorageService.get('year') + "";            

        this._localStorageService.set('customerId', this.customerId);
        //this._localStorageService.set('year', this.year);

        // try getting YEAR (not customerId, yet) from getter
        this._otherService.getDataFromApi(this.customerId, this.year)
            .then(
            data => this.data = data,
            error => this.errorMessage = <any>error);
    }

    getData(): void{
        this.data = this._otherService.getData();
    }

    resetComponentState(): void {
        this.getDataFromApi();
    }

    ngOnInit(): void {

        this.sub = this._route.params.subscribe(params => {
            this.customerId = params['customerId'];
            //this.year = params['year'];

            this.resetComponentState(); // based on new parameter this time
        });
    }
}
joakimk
  • 822
  • 9
  • 26
  • As for generic global service, I would suggest to have both get/set and observable for each variable, this makes it flexible enough. I cannot recommend to do this for route params. They are not global, they are specific to current route. You've already got `route.params` observable and `route.snapshot.params` object for ActivatedRoute, why reinvent the wheel? – Estus Flask Aug 19 '17 at 09:40
  • Yes, maybe I'm overcomplicating things? Point is, some component has a date picker, so if the user changes year I need some mechanism to update year in all other active components. This can't be done through route, as far as I know. So that calls for a global service? If I understand you, you're saying "do use a global service, but don't tie it to the route parameters. You've already got these covered by route observable." My point is that I also want to support "direct links" to components through route. But then I could use `year_param` or something in the route, not same variable `year`? – joakimk Aug 19 '17 at 09:56
  • So invoking route, `mycomponent/2015/102` could be matched by route observable, but go into variables `year_param` and `id_param`, respectively. This would then not conflict with getter on variable `year`. And, if the user does pass in year and id directly in the URL, so to speak, I could reflect this app-wide by setting (storing) values in the global service, through the setters. I'd just have to ensure that doesn't result in a duplicated refresh of the component (first from route params, and then second time from global params changing). – joakimk Aug 19 '17 at 10:06
  • *do use a global service, but don't tie it to the route parameters. You've already got these covered by route observable* - yes, I guess that's what I'm saying. From what I see, a 'service' looks and acts like a model, so I would call it a model. It clearly should be decoupled from the router. Can you provide an example of 'a duplicated refresh of the component' if such problem exists? – Estus Flask Aug 19 '17 at 10:11

1 Answers1

0

Use getters and setters. Something like this:

// get global value
get year() {
   const value = this._parameterService.p['year'];
   // Any other code you need to execute when the data is changed here
   this.getDataFromApi();
   return value;
}

set year(value) {
   this._parameterService.p['year'] = value;
}

I have a blog post about this here: https://blogs.msmvps.com/deborahk/build-a-simple-angular-service-to-share-data/

And a plunker here: https://plnkr.co/edit/KT4JLmpcwGBM2xdZQeI9?p=preview

DeborahK
  • 57,520
  • 12
  • 104
  • 129
  • Thanks for your help! I had a look, but I can't see how this helps me trigger a new (refreshed) call to the API when the parameters change. Data binding (showing the value in the template), however, works already. I've updated the question with the full component. Should I invoke the `getter` somehow in the call to `getDataFromApi()`? – joakimk Aug 18 '17 at 20:10
  • Or should the `getter` invoke `getDataFromApi` (as per your comment, "any other code you need to execute when the data is changed here")? – joakimk Aug 18 '17 at 20:21
  • Yes, the second one. When the data is checked, Angular's change detection will recognize the change and re-retrieve the value, calling the getter. You can then add any code you need there that should be executed when the data is changed. Give it a try ... – DeborahK Aug 18 '17 at 20:33
  • Thanks again, I did try it but nothing happened. Surely, there is something I'm getting wrong. First, Typescript won't let me define the getter without a `return` value: `a 'get' accessor must return a value`. So, I just `return value` (and type the getter to `get year(): string`). When I change (set) the parameter, using `this._parameterService.p['year'] = value;` from some other component (say, `menuComponent`), the value is changed in the `TestComponent` view (HTML), but the closest I can get to something working is a `RangeError` where `get` and `getDataFromApi` recurse until crash. – joakimk Aug 18 '17 at 22:10
  • Well -- something did happen, not just what I wanted ;) I've tried updating the example (TestController) with the code. – joakimk Aug 18 '17 at 22:21
  • Did you try the plunker? – DeborahK Aug 18 '17 at 22:23
  • Yes, of course :) But that demonstrates setting the value in one component, and seeing it update in the view of another component (as far as I could understand)? What I'm looking for (and forgive me if I'm just not seeing your point) is a way to set a value (in the `ParameterService`) from one controller, with the effect of "refreshing" another component. I.e. the other component immediately *re-does* its API GET call, and updates its entire HTML. Please note, **both** these components are shown/active at the same time in the application (as in, menu and report components). – joakimk Aug 18 '17 at 22:27
  • I tried to fork and modify your Plunker, placing component `a` with a `menu` selector, alongside component `b`. I couldn't save the edits, so here is my modified `a` controller: https://pastebin.com/szqugSJq (I also added `` to `app.ts`). **Since it works there, in the Plunker, but not in my app, this must mean my app contains some other code which causes the problems/conflict (read, crash).** Thanks again for your help! – joakimk Aug 18 '17 at 22:59