1

My Application has the following app-routing.module.ts:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './core/page-not-found/page-not-found.component';

const routes: Routes = [
  { path:  '', redirectTo: 'forms', pathMatch: 'full'  },
  { path:  'forms', loadChildren : () => import('./pages/forms/forms.module').then(m => m.FormsModule)   },
  { path:  'admin', loadChildren : () => import('./pages/admin/admin.module').then(m => m.AdminModule)   },
  { path: '**', component: PageNotFoundComponent }  

@NgModule({
   imports: [RouterModule.forRoot(routes)],
   exports: [RouterModule]
}) 
export class AppRoutingModule { }

As you can see, I have two of what I call "products", they are "forms" and "admin", and they are acessed by the url /forms and /admin respectively.

The app.component.html is as follows:

<navbar></navbar>

<div class="container">
  <ngx-spinner
    bdColor = "rgba(51, 51, 51, 0.6)"
    size = "large"
    color = "white"
    type = "ball-running-dots">

  <p style="font-size: 20px; color: white">Loading...</p>

  </ngx-spinner>

  <router-outlet></router-outlet>
</div>

<app-footer></app-footer>

As you can see, I have the component "navbar", tagged as <navbar></navbar>. The thing is that, the content of this navbar will vary based on the product that the user are in. To make things more clear, let's take a look in the navbar.component.html:

<default-navbar *ngIf="currentProduct == 'forms'"></default-navbar>
<div *ngIf="currentProduct != 'forms'">
    lalalallala
</div>

And also in the navbar.component.ts:

import { OnInit, OnDestroy, Component } from '@angular/core';
import { Subscription } from '../../../../../node_modules/rxjs';
import { ProductEnum } from '../../../shared/models/enums/product-enum';
import { ProductContainer } from '../../../shared/containers/product-container';

@Component({
    selector: 'navbar',
    templateUrl: './navbar.component.html',
    styleUrls: ['./navbar.component.scss']
  })
export class NavbarComponent implements OnInit, OnDestroy 
{
    public currentProduct:ProductEnum;

    private subscription = Subscription.EMPTY;

    constructor(private _productContainer:ProductContainer) 
    {
        
    }

    ngOnInit(): void 
    {
        this.currentProduct = this._productContainer.getCurrentProduct();
        this.initializeSubscribers();
    }

    ngOnDestroy(): void 
    {
        this.subscription.unsubscribe();
    }

    private initializeSubscribers():void
    {
        this.subscription.add(this._productContainer.getChangeCurrentProductSubject().subscribe(_newCurrentProduct => {
            this.currentProduct = _newCurrentProduct;
        }));
    }
}

So the navbar content will vary depending on the currentProduct property. And this variable will be updated by the subject property that exists on a injectable service that I called ProductContainer (horrible name by the way, I know).

Here is the ProductContainer(product-container.ts) code:

import { Injectable } from '@angular/core';
import { Subject } from '../../../../node_modules/rxjs';
import { ProductEnum } from '../models/enums/product-enum';

@Injectable({
    providedIn: 'root',
  })
export class ProductContainer
{
    private changeCurrentProductSubject: Subject<ProductEnum> = new Subject<ProductEnum>();

    private currentProduct: ProductEnum = ProductEnum.Forms;
    
    public setCurrentProduct(_newCurrentProduct:ProductEnum):void
    {
        this.currentProduct = _newCurrentProduct;
        this.changeCurrentProductSubject.next(this.currentProduct);
    }

    public getCurrentProduct():ProductEnum
    {
        return this.currentProduct;
    }

    public getChangeCurrentProductSubject():Subject<ProductEnum>
    {
        return this.changeCurrentProductSubject;
    }
}

So if we go back to the app-routing.module.ts, we can see the when the \admin url is acessed, the AdminModule is loaded. Here is the AdminModule code:

import { CommonModule } from '@angular/common';
import { SharedModule } from '../../shared/shared.module';
import { NgModule } from '@angular/core';
import { AdminRoutingModule } from './admin-routing.module';
import { AdminHomeModule } from './admin-home/admin-home.module';
import { CoreModule } from '../../core/core.module';
import { ProductContainer } from '../../shared/containers/product-container';
import { ProductEnum } from '../../shared/models/enums/product-enum';
import { Router, RouterModule } from '@angular/router';

@NgModule({
    declarations: [],
    imports: [
      CommonModule,
      SharedModule,
      AdminRoutingModule,
      AdminHomeModule,
      CoreModule,
      RouterModule
    ]
})
export class AdminModule
{
  constructor(private _productContainer:ProductContainer, private router: Router)
  {  
    this._productContainer.setCurrentProduct(ProductEnum.Admin);
  }
}

So, when the \admin url acessed, it will set the current product to Admin, so the navbar will have knowledge that the product has been updated.

But the issue is very simple, the subscribe inside the navbar is not being triggered.

(edit: Part of the bug was also caused by the use of private subscription = Subscription.EMPTY;, it stated working when I replaced with private subscription = new Subscription();)

Gui Oliveira
  • 155
  • 2
  • 9
  • It might be because the RxJS `Subject` emits only if it receives the value *after* the subscription. There was a similar question few minutes back. Try it's solution, you might have the same issue: https://stackoverflow.com/a/62543513/6513921 – ruth Jun 23 '20 at 22:30
  • @MichaelD didn't work as well. I don't think it has to do with execution order, cause even if the ```NavbarComponent``` didn't manage to subscribe in time, it will get the new value in the ```getCurrentProduct()``` method on the ngOnInit. – Gui Oliveira Jun 23 '20 at 22:50

2 Answers2

1

First of all I would like to advise moving all the logic from NgModule, this is bad practice, the module does not exist for this.

I cant understand how you want to change currentProduct, in construcor from AdminModule? This this will not work, after the first download the module will no longer load, so the constructor will not be called.

You can subscribe to url change event. In navbar component:

      if (this.router.url.startsWith(`/forms`)) {
               this.currentProduct = ProductEnum.Forms;
      } else if (this.router.url.startsWith(`/admin`)) {
               this.currentProduct ProductEnum.Admin;
      }

      this.router.events.subscribe(event => {
        if (event instanceof NavigationEnd) {
            if (event.url.startsWith(`/forms`)) {
               this.currentProduct = ProductEnum.Forms;
            } else if (event.url.startsWith(`/admin`)) {
               this.currentProduct = ProductEnum.Admin;
            }
        }
      })

But if I were you, I would define the "page type" (admin or forms or something else) parameter in navbar and transfer this navbar to the root pages of the modules and also make an outlet for child pages (so as not to add navbar to each page)

Just in case use BehaviorSubject instead Subject

bitvalser
  • 465
  • 2
  • 5
  • I tried to add in the initializeSubscribers method inside the navbar: ```this.subscription.add(this._router.events.subscribe(event => { debugger; if (event instanceof NavigationEnd) { if (event.url.startsWith(`/forms`)) { this._productContainer.setCurrentProduct(ProductEnum.Forms); } else if (event.url.startsWith(`/admin`)) { this._productContainer.setCurrentProduct(ProductEnum.Admin); } } }))```. And it didn't fall into the debugger. – Gui Oliveira Jun 23 '20 at 23:19
  • I think in the component we no longer make sense to access the service and you can just do this: `this.currentProduct = ProductEnum.Admin` or ProductEnum.Forms. If this didnt work plese show me log of event inside if block – bitvalser Jun 23 '20 at 23:26
  • I put the debugger inside the ```router.events```, I dont think it will make much difference, cause the event is not even being triggered. I think this happens because the whole application is being reloaded when I update the URL. And this includes the navbar component, so the component will be rebuild as well. – Gui Oliveira Jun 23 '20 at 23:54
  • Okay, I forgot to add that for the first time it shouldn’t work (only for changes), please add this before subscribing to url (update in post) – bitvalser Jun 23 '20 at 23:57
  • The issue now is that ```this._router.url``` content is just "/" instead of "/forms" or "/admin", even though the url is /forms or /admin. Do you have any idea why this happens? – Gui Oliveira Jun 24 '20 at 00:05
  • It gets harder to research, could you fill in your example on stackblitz – bitvalser Jun 24 '20 at 00:34
  • Sure, I will do it in a sec – Gui Oliveira Jun 24 '20 at 01:13
  • Hey sry for the late response, but now it's working, the issue with the ```this._router.url``` was solved in https://stackoverflow.com/questions/45320416/angular-router-url-returns-slash. So I just implemented the method like this: ```if (this._location.path().startsWith(`/forms`)) { this.currentProduct = ProductEnum.Forms; } else if (this._location.path().startsWith(`/admin`)) { this.currentProduct = ProductEnum.Admin; }``` – Gui Oliveira Jun 25 '20 at 08:26
  • Thank very much, it was afterall a conceptual bug. – Gui Oliveira Jun 25 '20 at 08:27
0

Try using this function in your product.container.ts file,

public getChangeCurrentProductSubject(): Observable<ProductEnum>
{
    return this.changeCurrentProductSubject.asObservable();
}

and this in your navbar.component.ts file

ngOnInit(): void 
{
    this.initializeSubscribers();
    this.currentProduct = this._productContainer.getCurrentProduct();
}

also, try modifying the Subscription in navbar.component.ts file like this

import { Subscription } from "rxjs";
private subscription = Subscription;

this.subscription = this._productContainer.getChangeCurrentProductSubject().subscribe(_newCurrentProduct => {
         this.currentProduct = _newCurrentProduct;
        });

I hope this works

  • @GuiOliveira did you try using the ngOnInit() function as above? – Aayush Goyal Jun 23 '20 at 22:43
  • The issue is now on this line ```private subscription = Subscription;```. If I do this, I cannot do this line: ```this.subscription = this._productContainer.getChangeCurrentProductSubject().subscribe(_newCurrentProduct => { this.currentProduct = _newCurrentProduct; });```. Because is of different type – Gui Oliveira Jun 23 '20 at 23:00
  • I think _newCurrentProduct should be in parenthesis, `this.subscription = this._productContainer.getChangeCurrentProductSubject().subscribe((_newCurrentProduct) => { this.currentProduct = _newCurrentProduct; });` – Aayush Goyal Jun 23 '20 at 23:04