6

Am new to Angular2 but I have already made 30+ component and services and used Input, Output, ChangeDetectorRef, Pipes. So pretty much know about Angular2 by now. My app.component.ts looks like this

import { Component } from '@angular/core';
@Component({
    selector: 'soc',
    template: `
        <header-block></header-block>
        <router-outlet></router-outlet>
    `,
})
export class AppComponent { }

As I need my header-block in every page so I added it outside router-block so I don't have to add it everywhere. Now the problem I am having is changing the value of variable in the header-block after doing an action in view inside router-outlet.

For example I have menu which will be visible to the user only after they login.

{{ user_is_logged }}
<ul class="nav navbar-nav navbar-right" *ngIf="user_is_logged == '1'">
    <li><a href="#" (click)="logout($event)">Logout</a></li>
</ul>

user_is_logged is a variable whose value am getting from localStorage in header.block.component.ts

user_is_logged:string = localStorage.getItem("logged");

When the user is not logged in i.e. user_is_logged is undefined for home.component.ts ( where login logic is written ), I ask the user to login and after successful login am updating the value of user_is_logged variable and also updating the localStorage value of the variable and then triggering detect changes by using the ChangeDetectionRef(cdr) and then routing to user profile

this.user_is_logged = "1";
localStorage.setItem('logged', '1');
this.cdr.detectChanges();
this.router.navigate(['u/'+user_id']); //user_id is from login response from server

The problem is when I reach the user profile after successful login the value of {{ user_is_logged }} never updates even when am using the variable itself or using the value from localStorage or even calling for detection.

Is it possible to do it this way or do I have to add header-block to every page separately? Please ask for more information if I missed something.


EDIT ============>

So after going through Subject, BehaviourSubject, AsyncSubject, ReplaySubject, Observables and what not I just couldn't get this to work. This is the last code I have written :

home.page.component.ts (where login happens and has to emit the event)

import { HomeService } from '../services/home.service';
// No observable or subject imported here.

login() {
    // Sending status 1 to HomeService
    this.HomeService.changeStatus("1");
}

home.service.ts (which is imported by both component)

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
// I imported Subject, BehaviourSubject, Observables but cant get it to work

export class HomeService {

    // This looked more reliable than Subject so used this; init with value "0"
    public subjectStream = new BehaviorSubject<string>('0');


    constructor(
    ){
        this.subjectStream.subscribe(value => {
            // Successfully logs value "1" sent by changeStatus below
            console.log(value);
        });
    }


    changeStatus(value: any){

        // Gives value "1" when called by home.page.component.ts
        console.log(value);

        // Looks correct because it successfully sent value to construct
        this.subjectStream.next(value);

    }

}

header.block.component.ts

import { HomeService } from '../services/home.service';
// No observable or subject imported here also.


constructor(
){
    this.HomeService.subjectStream.subscribe(value => {
        // Logs value "0" at beginning but never logs value "1"
        console.log(value);
    });
}

My Log

// logged by constructor when imported by header.block.component
home.service.ts:31                  0

// logged by constructor of header.block.component itself
header.block.component.ts:25        0

// logged by constructor when imported by home.page.component (I think)
home.service.ts:31                  0

// ===================
// Login called here
// ===================

// logged by changeStatus after login after called by home.component
home.service.ts:36                  1 

// logged by constructor after getting event from changeStatus
home.service.ts:31                  1

What am I missing in header.block.component.ts? As the value is successfully updated within the home.service.ts but never goes to header.

Akash Joshi
  • 598
  • 1
  • 5
  • 15
  • Sounds like http://stackoverflow.com/questions/35397198/how-can-i-watch-for-changes-to-localstorage-in-angular2/35397253#35397253 – Günter Zöchbauer Jan 28 '17 at 14:03
  • Just create a service. Inject it into both components, have them both subscribe to an emitter (EventEmitter or Subject, or even just a simple little Observer pattern) held by the service. When one needs to inform the other, one just tells the service, "emit an event". Everything subscribed to it will hear it. Use constants to be able to filter the events by name, or use a little factory to emit typed events, however you want to go about it. There are numerous examples all over the web, mostly under the title, "sending data between components with no parent/child relationship". – Tim Consolazio Jan 28 '17 at 14:05
  • FYI I do the same exact thing you do; I have a header, router-outlet, and footer in most of my apps. The router-outlet changes, the header and footer stay the same, they communicate entirely through that emitter service and it makes the whole thing so easy. – Tim Consolazio Jan 28 '17 at 14:09
  • @TimConsolazio Are you using Observables like Günter suggested in his comment or are you using EventEmitter? – Akash Joshi Jan 28 '17 at 18:05
  • I currently use EventEmitter (and I asked around a bit, it's not unusual), since it extends Subject (so it is observable). However I understand it is a possibility that this will change and that EventEmitter may no longer be structured that way. I guess you can worry about that if and when it happens, but for forward moving code, it might be better to get closer to RxJS and further from away from Angular event mechanisms, so going with Subject is reasonable. – Tim Consolazio Jan 28 '17 at 20:35
  • This comment is more or less the salient point: "because even though it extends Subject, if you look at the docs they only put 3 methods in there, and even goes as far as recommending emit() instead of Subject's next(), meaning we cannot count that it will continue to extend Subject in future iteration of angular." Me, I'm not a fan of the whole "output" thing for events. I use inputs for what used to be params that got fed to scope (or bindToController), but I don't use outputs for events at all anymore. I just subscribe/emit from a service and don't worry about parent/child etc. – Tim Consolazio Jan 28 '17 at 20:45
  • @GünterZöchbauer: I tried using everything I could find but couldnt get it to work. Some examples have "async pipe", some have old rx method. Please see my edit to see how far I reached. – Akash Joshi Jan 30 '17 at 12:38
  • @TimConsolazio : I use event emitter with event attached to child component selector like , but its not working with in app.component.ts; is there any other way of using Event Emitter. Please guide me to some tutorial or example code. – Akash Joshi Jan 30 '17 at 12:42
  • I will post a simple emitter service as an answer, since it's too long for a comment. – Tim Consolazio Jan 30 '17 at 14:10
  • You can edit your question and add the info there. When you post an answer nobody will look at your question because it looks like it's already answered. I don't have time just now for a closer look. Perhaps tomorrow. – Günter Zöchbauer Jan 30 '17 at 17:27
  • @GünterZöchbauer Yes I edited the question only. Please have a look at your earliest, am flipping tables here (╯°□°)╯︵ ┻━┻ – Akash Joshi Jan 30 '17 at 18:37
  • Where are you providing `HomeService`? `@Component(...)`, @NgModule(...)`, ...? – Günter Zöchbauer Jan 30 '17 at 18:44
  • `@Component` in both homepage and header. I used `@NgModule` only once in app.module.ts where am importing all my 50 components and declaring them in @NgModule. Rest I havnt used `@NgModule` anywhere . – Akash Joshi Jan 30 '17 at 18:48
  • Nothing was working in `ngOnInit` so I just used a hack. I just updated the variable in `ngAfterContentChecked() { this.user_is_logged = localStorage.getItem("logged"); }` – Akash Joshi Jan 30 '17 at 20:23

2 Answers2

1

Was asked to post this, it is a bare-bones event/subject service to get you started. I use "notifier/notes/note" just so the nomenclature doesn't get confused with "events" and other framework-api mechanisms.

// Constant of named "notes" analogous to named events. 
// I always found it easier to have one event type with a "name",
// rather than creating a specific typed event for every action. 
const Notes = {
    HEADER_BUTTON_CLICKED : 'header_button_clicked'
};

// Any component that wants to send data creates a new "Note" to send.
// Just makes sure you know what all callbacks are getting.
class Note {
    constructor ( name, data ) {
        this.name = name; // Should be from your Notes constant.
        this.data = data; // Can be anything at all.
    }
}

// You inject this service into any component that needs it. 
class NotifierService {

    constructor() {

        let Rx = require ( 'rx' );
        // We'll use Subject here. 
        this.subject = new Rx.Subject ( );
    }

    // You only subscribe if you want to hear notes. 
    subscribe ( callback ) {
        // Subscribing component feeds in a callback.
        // The return of this can be used to unsubscribe as well.
        return this.subject.subscribe ( b => callback ( b ) );
    }

    // Any component with access to this service can just create a note
    // and send it to all subscribers to this service. 
    sendNote ( note ) {
       // Component uses the service to emit a "note" with a 
       // named event and a payload. All subscribers hear it in the
       // callbacks they subscribed with, and can filter on the name
       // in the "note" to make sure it's the event they care about. 
       // This is pretty naive but should give the idea.
       this.subject.onNext ( note );
    }
}
export { NotifierService } // Service to inject
export { Note } // A useful type to send out to subscribers 
export { Notes } // Constant of event/note names.

Typical usage:

import { NotifierService, Notes, Note } from './notifier.service';
... // inject in your constructor either TS or ES6 style...

// I want to hear notes
this.noteSvc.subscribe ( b => this.myCallback ( b ) );

// I want to send a note
this.noteSvc.sendNote ( new Note ( Notes.HEADER_BUTTON_CLICKED, { btnName : 'Home' } );

   // In the callback 
   myCallback ( note ) {
     if ( note.name === Notes.HEADER_BUTTON_CLICKED ) {
        console.log ( note.data ); // whatever the payload is
     }
   }
Tim Consolazio
  • 4,802
  • 2
  • 19
  • 28
1

If you provide HomeService in homepage and header, you get 2 instances of HomeService. If you want to share a service, then provide it only once on the parent component. If you want to share a single instance with your whole application, provide it only at @NgModule() of AppModule

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • If I remove HomeService from providers of @Component of either of the two component then am getting this error `No provider for HomeService!` Actually the issue is that Homepage and header are not parent child. Like you can see in question in my app.component.ts I have two selected for header and my homepage is in so they are definitely not parent child but more like siblings or uncle-nephew, since homepage is child of – Akash Joshi Jan 31 '17 at 11:28
  • You need to provide it somewhere. You should only provide it once. You should provide it somewhere where both get get it injected from. This means if one component is the parent of the other, provide it at the parent. If they are siblings, provide it on a common parent. Providing it **only** in the `AppModule` always makes it an application-wide singleton. – Günter Zöchbauer Jan 31 '17 at 11:29
  • The only parent they both have is the app.component.ts itself. Will try there and update you. – Akash Joshi Jan 31 '17 at 11:32
  • AppComponent is pretty much the same as AppModule (except for other providers added to AppModule that might inject the service) – Günter Zöchbauer Jan 31 '17 at 11:36
  • Holy-co-moly! That actually worked! Imported HomeService in AppModule, Header and Home but provided only in AppModule and now its working. Thanks a lot sir. – Akash Joshi Jan 31 '17 at 11:43
  • You're welcome. Glad to hear you could make it work :) – Günter Zöchbauer Jan 31 '17 at 11:44
  • Will it be a good practice to remove Providers from all of the components and to be provided once in AppModule? Currently I have all my declarations In AppModule but all providers are with separate component file. – Akash Joshi Jan 31 '17 at 11:47
  • That always depends on what you try to accomplish. Providers are looked up from where they need to be injected, then parent components up to AppComponent, and if still not found found in the root scope (providers added to `AppModule` or added to modules imported to `AppModule`). If you want to share a service only with a component instance and its children, then the component is a better place. For global singletons, the `AppModule` is the right place. – Günter Zöchbauer Jan 31 '17 at 11:50