Home > Mobile >  What's the most 'Angular' way to make a top level nav bar aware of component data?
What's the most 'Angular' way to make a top level nav bar aware of component data?

Time:03-27

I have a top level navigation bar declared like so:

<app-loading *ngIf="isLoading"></app-loading>

<div *ngIf="!isLoading">
    <app-navigation></app-navigation>
    <router-outlet></router-outlet>
</div>

In my application, I have the concept of workspaces, workspaces have names of type string (e.g. 'Company X', 'Company Y').

My top level navigation links within app-navigation (the nav bar) are Workspaces, Teams, Activity. Each item takes you through to a component of the same name.

Clicking Workspaces loads the Workspaces component, which renders a collection of workspaces. Clicking one of the workspace items loads a child component that renders the data relevant to the workspace, including its name, and from there the user can load further components that are children of that workspace component. I want to be able to show the name of the currently active workspace in the nav bar.

In other words, I want the nav bar to display a link called Workspaces when above a certain level in the component hierarchy, but if the user navigates down the component tree into a workspace, for all components below that workspace in the hierarchy, I want the nav bar to display the name of the currently active workspace. Here's a quick diagram to illustrate:Component Hierarchy

What is the most 'Angular' way of achieving this? I can think of a few options:

  1. Use event emitters at each level of the component hierarchy and emit and catch events up through the tree and into the nav bar. This seems like a bad option. Lots of extra work, lots of coupling between components that don't really need coupling. Brittle component structure and probably much more flakey code.

  2. I could try and create an interface with a field for the active workspace, and make each relevant component implement that interface. Then I might be able to use the ActivatedRoute snapshot to load the current component and access the field - I'm not really sure if this is possible, or if it's what ActivatedRoute is meant for.

  3. I could use the router, and listen for changes in the URL - when the URL detects a child e.g. /workspaces/${WorkspaceId}/** I could pull the workspaceId out of the route and make a get request to the API - this would make the service very chatty and seems wasteful as I'm asking for data I already have

  4. I have a workspace service that handles API requests, I could update the GET request for a workspace and ensure that whenever I make a GET request I save a copy of the workspace data into the service - and call it something like lastRequestedWorkspace. Then I could inject that service into the nav bar and read the data back out. This seems like the best approach but there would probably be edge cases to handle where I want to load workspace data in a situation where that workspace is not the currently active one (for example rendering a list of workspaces with relationships to the current one). Another potential issue here is deep linking directly to content. If a user were to click through the application, when the workspace is requested, the name would be rendered in the nav bar as expected, but in the case where a user navigates directly to a child, I would have no way of making that connection. I.e. A direct link to /workspaces/${WorkspaceId}/sub-page would load that sub component as expected, but since the parent component was never loaded, the workspace was never set and so I would not be able to show the name. The user would have to navigate back up to the list of workspaces and navigate back down in before it would be loaded. Additionally, the nav bar is the first component that loads due to being a child of app-component and a sibling to the top level router-outlet - meaning that I couldn't even read which component is eventually loaded - the nav bar would be loaded and rendered by that point. How would I get the URL out of the activated route (the nav bar wouldn't have access to the parameter map) or force a re-render of the nav bar onces the sub-component is loaded??

Is there an option from the above that stands out to you as the best? Why? Are there simple options that Ive missed here? Keen to hear about it.

CodePudding user response:

Two options I can think of.

#1 - You can pass any data you want to a route.

{ path: 'static', component: StaticComponent, data :{ id:'1', name:"Angular"}},

So if your active component always correlates to a route, you can pass some kind of enum value or something along. Any component should be able to subscribe to the route data and act on it.

constructor(private activatedroute:ActivatedRoute) {}

ngOnInit() {
    this.activatedroute.data.subscribe(data => {
        this.product=data;
    })
}

Credit: https://www.tektutorialshub.com/angular/angular-pass-data-to-route/.

#2 - You can use an Angular service with an observable. This is similar to your first option, but you eliminate the required parent/child relationship and it technically uses observables instead of events. But any component can raise an "event" (modify the subject) and any other components can subscribe to changes and get updated in real time.

shared.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable()
export class SharedService {

  private message = new BehaviorSubject('First Message');
  sharedMessage = this.message.asObservable();

  constructor() { }

  nextMessage(message: string) {
    this.message.next(message)
  }
  
}

parent.component.ts

import { Component, OnInit } from '@angular/core';
import { SharedService } from "../shared.service";

@Component({
  selector: 'app-parent',
  template: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit {

  message:string;

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
    this.sharedService.sharedMessage.subscribe(message => this.message = message)
  }

}

sibling.component.ts

import { Component, OnInit } from '@angular/core';
import { SharedService } from "../shared.service";

@Component({
  selector: 'app-sibling',
  template: './sibling.component.ts',
  styleUrls: ['./sibling.component.css']
})
export class SiblingComponent implements OnInit {

  message:string;

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
    this.sharedService.sharedMessage.subscribe(message => this.message = message)
  }

  newMessage() {
    this.sharedService.nextMessage("Second Message")
  }

}

Note they don't have to be parent/sibling like the filenames in this example imply.

Credit: https://enlear.academy/sharing-data-between-angular-components-f76fa680bf76

  • Related