19

I have a settings page where users can save some config variables, such as a Username. The user can also change the username on that page. But when I go to another Component (page) and back, the username isn't saved.

I also want to show the username in the other components. How do I do that? With a global variable?

Structure: - App - app.ts (main) - setting.ts - someOtherComponent.ts

Michael Oryl
  • 20,856
  • 14
  • 77
  • 117
  • You basically want to use - what was known as a service in angular 1 - that holds your variable. That service can be injected into every other component you need it in. – Rico Herwig Jan 03 '16 at 00:08
  • 2
    It is still called a [service](https://angular.io/docs/ts/latest/tutorial/toh-pt4.html) in Angular 2. – Mark Rajcok Jan 03 '16 at 00:09
  • ngOnChanges() is calling before the service got initialized(to get user) in my case – Sukumar MS Jun 05 '17 at 10:54

5 Answers5

38

What you need is a service to maintain the state of your variables. You would then be able to inject that service into any component to set or get that variable.

Here's an example (/services/my.service.ts):

import {Injectable} from "angular2/core";

@Injectable()
export class MyService {
    private myValue;

    constructor() {}

    setValue(val) {
        this.myValue = val;
    }

    getValue() {
        return this.myValue ;
    }
}

You would probably want to put that service in the providers array of your app's bootstrap function (which might be in your main app component file, or in a separate file depending on how you like to do things).

In your main app file (/app.ts):

import {MyService} from './services/my.service';
bootstrap(App, [MyService, COMMON_DIRECTIVES, ROUTER_DIRECTIVES, ROUTER_PROVIDERS, HTTP_PROVIDERS]); // directives added here are available to all children

You needn't have COMMON_DIRECTIVES and the other all caps items in the array, but those are commonly included just to make it so that you don't have to configure them in each component you write.

Then you would access the service from within a component like this (/components/some-component.ts):

import {MyService} from '../services/my.service';

@Component({
    selector: 'some-component',
    template: `
<div>MyValue: {{val}}</div> 
`
})
export class SomeComponent {
    constructor(private myService:MyService) {
    }

    get val() {
        return this.myService.getValue();
    }
}

You might also want to add to the service so that it saved the value to somewhere (semi) permanent so that it could be accessed the next time the user entered the application.

rudrasiva86
  • 573
  • 7
  • 15
Michael Oryl
  • 20,856
  • 14
  • 77
  • 117
  • this.MyService should be this.myService, note the capitalization! Also, a good practice is to add a underscore in front of the names for private variables. – causztic Mar 15 '16 at 01:30
  • 2
    it is not a real global variable, because it will create a new instance of "MyService". – Sulot May 22 '16 at 11:21
  • 1
    I am having the same issue and this is not working for me as myService is a new instance like @Sulot said – tony Jun 16 '16 at 03:29
  • @tony What I have done is to import your service class in the app.ts, and put it as a provider. Then follow what it is said above. Don't put it as a provider when you want to access it. – Sulot Jul 24 '16 at 11:53
  • I am getting "Error: No Directive annotation found on MyService" any idea why ? – debD Feb 01 '17 at 06:47
  • Simply you can add provider of this service to app module – Liu Zhang Jun 08 '17 at 10:45
  • 2
    @causztic It is against the [Angular Official Style Guide](https://angular.io/guide/styleguide#style-03-04) to name your private variables with underscore. – Hristo Enev Jan 15 '18 at 13:53
  • Once we have set the variable, and the page gets refreshed (new tab/CTRL+f5). The variable does not keep the value, its gets reinitialized. What's the solution? – Aneeq Azam Khan Nov 15 '18 at 08:03
12

This might be an overkill in your current situation, but if you are going to grow your application into a larger one, this might save you a lot of troubles down the line. That said, I believe Angular with Redux-like store is a really powerful combination. My suggestion is to use ngrx/store module.

RxJS powered state management for Angular applications, inspired by Redux

Source: ngrx/store GitHub repo

The code below should outline all steps you need to follow to integrate the ngrx/store into your application. For the sake of brevity, I have omitted any existing code you might have and all imports which you will need to add.

Install the Module

npm install --save ngrx/core ngrx/store

Create a Reducer

export const SETTINGS_UPDATE = 'SETTINGS_UPDATE';

const initialState = {
    username: ''
};

export function settingsReducer(state: Object = initialState, action: Action) {
    switch (action.type) {
        case SETTINGS_UPDATE:
            return Object.assign({}, state, action.payload);
        default:
            return state;
    }
};

Import the StoreModule

@NgModule({
    imports: [
        // your other imports
        StoreModule.provideStore({
            settings: settingsReducer
        })
    ]
})
export class AppModule {
    //
}

Create Interface for Settings Reducer

export interface SettingsStore {
    username: string;
}

Create a Store Interface

export interface AppStore {
    settings: SettingsStore;
}

Create Settings Service

@Injectable()
export class SettingsService {

    settings$: Observable<SettingsStore>;

    constructor(private store: Store<AppStore>) {
        this.settings$ = this.store.select('settings');
    }

    public update(payload) {
        this.store.dispatch({ type: SETTINGS_UPDATE, payload });
    }

}

Settings Component Controller

@Component({
    ...
})
export class SettingsComponent implements OnInit {

    settings$: Observable<SettingsStore>;

    constructor(private settingsService: SettingsService) {
        //
    }

    ngOnInit() {
        this.settings$ = this.settingsService.settings$;
    }

    public updateUsername() {
        this.settingsService.update({ username: 'newUsername' });
    }

}

Settings Component Template

<p *ngIf="(settings$ | async)?.username">
    <strong>Username:</strong>
    <span>{{ (settings$ | async)?.username }}</span>
</p>

<button (click)="updateUsername()">Update Username</button>

With the setup outlined above you can then inject the SettingsService into any component and display the value in its template.

The same way you can also update the value of the store from any component by using this.settingsService.update({ ... }); and the change will be reflected on all places which are using that value - be it a component via an async pipe or another service via .subscribe().

Vladimir Zdenek
  • 2,270
  • 1
  • 18
  • 23
4

You would need a service this so you can inject in wherever you need it.:

@Injectable()
export class GlobalService{
  private value;

  constructor(){

  }

  public setValue(value){
    this.value = value;
  }

  public getValue(){
    return this.value;
  }
}

You could provide this service in all your modules but there is a chance you will get multiple instances. Therefore we will make the service a singleton.

@NgModule({
  providers: [ /* DONT ADD THE SERVICE HERE */ ]
})
class GlobalModule {
  static forRoot() {
    return {
      ngModule: GlobalModule,
      providers: [ GlobalService ]
    }
  }
}

You should call the forRoot method only in your AppModule:

@NgModule({
  imports: [GlobalModule.forRoot()]
})
class AppModule {}
Robin Dijkhof
  • 18,665
  • 11
  • 65
  • 116
  • I am already using this approach but seems like @Vladimir solution above sounds more performance-oriented, but this seems easy to implement, I would continue with the same in new Angular 8 upgrade unless something breaks :) – Naga May 03 '20 at 11:28
2

Vladimir correctly mentioned about ngrx.

I would use a service with Subject/BehaviourSubject and Observable to set and get the state (values) that I need.

We discussed here Communication between multiple components with a service by using Observable and Subject in Angular 2 how you can share values in multiple components that do not have parent-child or child-parent relationship and without importing external library as ngrx store.

DrNio
  • 1,936
  • 1
  • 19
  • 25
  • I really love this approach and I am using this in my current application. But on page refresh, How to manage the state of this application. I have asked the question here. https://stackoverflow.com/questions/49171680/angular-5-app-with-behavior-subject-observable-is-loosing-the-state-on-page-re – Talk is Cheap Show me Code Mar 08 '18 at 11:10
  • as it was answered already in your question, you will need to save it locally in the browser and OnInit try to get it from localstorage/sessionstorage or whatever or send it to backend and OnInit use a GET to populate the initial state – DrNio Mar 08 '18 at 14:44
2

I have the same problem, most of the component needs loggedInUser information. For eg : username, preferred language, preferred timezone of the user etc.

So for this, once user logs in, we can do the following :

If all the information is in JWT Token or /me RESTful Api, then you can decode the token in one of the service.

eg: user.service.ts

@Injectable()
export class UserService {

  public username = new BehaviorSubject<string>('');
  public preferredLanguage = new BehaviorSubject<string>('');
  public preferredTimezone = new BehaviorSubject<string>('');

  constructor(
    private router: Router,
    private jwtTokenService: JwtTokenService
  ) {
    let token: string = localStorage.getItem('token'); // handled for page hard refresh event
    if (token != null) {
      this.decode(token);
    }
  }

  private decode(token: string) {
    let jwt: any = this.jwtTokenService.decodeToken(token);
    this.username.next(jwt['sub']);
    this.preferredLanguage.next(jwt['preferredLanguage']);
    this.preferredTimezone.next(jwt['preferredTimezone']);
  }

  public setToken(token: any) {
    localStorage.setItem('auth_token', token);
    this.decode(token);
  }

}

which holds three public Behaviour subject, whoever is listening to this variable, will get the value if it is change. Please check the rxjs programming style.

and now wherever this variable are required, just subscribe from that component.

For eg: NavComponent will definitely needs username. hence the code will like this below:

export class NavComponent implements OnInit {

    public username: string;

    constructor(
        private userService: UserService,
        private router: Router) {
    }        

    ngOnInit() {
        this.userService.username.subscribe(data => {
          this.username = data;
        });        
    }
}

Hence, reactive programming gives better solution to handle the dependency variable.

The above also solves the problem if the page is hard refresh, as I am storing the value in localstorage and fetching it in constructor.

Please note : It is assumed that logged in user data is coming from JWT token, we can also the same service(UserHttpService) if the data is coming from Restful Api. Eg: api/me

virsha
  • 1,140
  • 4
  • 19
  • 40
  • I agree using a service like you proposed is the proper approach. If you're using router wouldn't it be possible and better to resolve `this.username` before performing the route? – Raven Jun 12 '17 at 10:23