0

We are currently developing an Angular 13 application that is split into several independent modules that are maintained by different teams and each module represents the UI for a different application that is backed by its own microservice. These modules are allowed to depend on our "SharedModule", but not the other way around.

Now, we want to implement a "Help Center" which consists of a question mark button that floats in the lower right corner and if clicked provides help for the currently loaded page. The thing is, that we want to keep the content of each modules Help Center pages inside that module, furthermore, some modules do not need Help Center pages.

Our current solution adds a component to our main layout:

<router-outlet name="navbar"></router-outlet>

<router-outlet></router-outlet>

<app-help></app-help>

<router-outlet name="footer"></router-outlet>

The app-help component has said floating button,

<button mat-fab (click)="openHelpCenter()">
  <mat-icon>help</mat-icon>
</button>

that opens a Material dialog with a named router-outlet inside:

@Component({
  selector: 'app-help',
  templateUrl: './help.component.html',
  styleUrls: ['./help.component.scss']
})
export class HelpComponent {


  constructor(
    private dialog: MatDialog,
  ) {
  }

  openHelpCenter() {
    this.dialog.open(HelpDialogComponent, {
      width: '300px'
    })
  }
}

@Component({
  selector: 'app-help-dialog',
  template: '<router-outlet name="help"></router-outlet>'
})
export class HelpDialogComponent {

  constructor() {
  }
}

Inside each module the teams may now implement routes that use the named router-outlet:

...
{
        path: '',
        component: HelpComponent,
        outlet: 'help'
}
...

which "injects" the modules HelpComponent inside the Dialog according to the currently loaded url.

This works fairly well, however, we have an open problem:

How to we hide the floating button if the router-outlet is empty, i.e. if you are on a page that does not provide help?

I believe the easiest solution (i.e. the solution that manages stuff with minimal amount of code inside the modules) would be to listen to Router events inside HelpComponent, e.g. NavigationEnd, and then find out whether there is an "active" routing for the "help"-router-outlet and set visibility accordingly. (similar to the RouterLinkActive-Directive)

However, I am not able to find out how to retrieve this information from the Router-Framework.

Of course, I would also appreciate ideas on how to tackle the problem differently.

Simon
  • 148
  • 1
  • 14

3 Answers3

1

You can have a service hold a boolean, and have modules manually flip the help on or off on init to fit their needs. We do something like this in our app, but also have something subscribed to navigation events that flips based on certain routes.

You could also add in a query param into the routes that is something like showhelp=true or false, and react to that in the main component.

You could also have a whitelist in the main component where you hold the help button and listen to routing events here, and just use the mapping to control the ngif on it.

Lots of options, not sure what is the best one, I'm curious what route you end up taking though

iamaword
  • 1,327
  • 8
  • 17
  • Thanks for you advice, I think your suggestions are reasonable and would certainly work. The thing is, that they all require the modules to do additional work which I really try to keep at a minimum : The service solution and the query params would require all module components to flip the switch on and off, which would put complexity in the module. The whitelist option would imho violate the allowed direction of dependencies. – Simon Feb 02 '22 at 17:56
  • yeah, ideally we would be able to just check for a route's existence but idk if you can. I wonder if you could use a viewchild within the helpDialogComponent that grabs whatever the router has, and if it doesnt't see anything don't show anything. I've never done anything like that but it sounds cool, if you try it let me know what you find out – iamaword Feb 02 '22 at 18:20
  • I think I found a solution. Please check my answer – Simon Feb 02 '22 at 20:47
1

After reading through the Angular Router source, I stumbled upon ChildrenOutletContexts which seems to be a map of the currently "active" outlets.

I adjusted my Component to get it injected and check it every time a navigation completes:

import {
  ChildrenOutletContexts,
  NavigationEnd,
  Router
} from "@angular/router";
import {filter} from "rxjs";
import {map} from "rxjs/operators";

@Component({
  selector: 'app-help',
  templateUrl: './help.component.html',
  styleUrls: ['./help.component.scss']
})
export class HelpComponent {
  helpOutletActive$ = this.router.events.pipe(
    filter(event => event instanceof NavigationEnd),
    map(() => this.contexts.getContext('help') !== null)
  )

  constructor(
    private dialog: MatDialog,
    private router: Router,
    private contexts: ChildrenOutletContexts
  ) {
  }

  openHelpCenter() {
    this.dialog.open(HelpDialogComponent, {
      width: '300px'
    })
  }
}

According to my tests this seems to produce the desired result

Simon
  • 148
  • 1
  • 14
0

There is another solution by using RouterLinkActive directive

<div routerLink="/specific/route" routerLinkActive="d-none">
   ... show/hide content on a specific route ... 
</div>

style.scss

.d-none {
  display: none
}

Another way is to use template variable

<div routerLink="/specific/route" routerLinkActive #rla="routerLinkActive">
  <div *ngIf="!rla.isActive">
    ... show/hide content on a specific route ... 
  </div>
</div>
urDMG
  • 428
  • 1
  • 6
  • 14