Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular4: passing breadcrumb view components through the router's `data` property

In my Angular (v4) app, I'm trying to create a breadcrumb module. I found this S.O. question helpful, suggesting I store breadcrumb information in the router's data property like so

{ path: '', component: HomeComponent, data: {breadcrumb: 'home'}}

But what I'd like to do is store a component in the data property, and then have the breadcrumb module extract and render it. Allowing me to interact with the breadcrumb module like this

{ path: '', component: HomeComponent, data: {breadcrumb: CustomViewComponent}}

Following the angular Dynamic Component Loader guide and a helpful blog post, I've attempted to make my breadcrumb rendering component do this:

import { Component, Input, OnInit, AfterViewInit,
  ViewChild, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import 'rxjs/add/operator/filter';

import { MainLogoComponent } from '../my-material/main-logo/main-logo.component';

@Component({
  selector: 'app-breadcrumbs',
  template: `<div #parent></div>`,
  styleUrls: ['./breadcrumbs.component.scss']
})    
export class BreadcrumbsComponent implements OnInit, AfterViewInit {
  breadcrumbs: Array<any>;
  @ViewChild('parent', {read: ViewContainerRef}) parent: ViewContainerRef;

  constructor(private componentFactoryResolver: ComponentFactoryResolver,
    private router:Router, private route:ActivatedRoute) { }

  ngOnInit() {
    this.router.events
      .filter(event => event instanceof NavigationEnd)
      .subscribe(event => {  // note, we don't use event
        this.breadcrumbs = [];
        let currentRoute = this.route.root,
            url = '';
        do {
          let childrenRoutes = currentRoute.children;
          currentRoute = null;
          childrenRoutes.forEach(route => {
            if(route.outlet === 'primary') {
              let routeSnapshot = route.snapshot;
              url += '/' + routeSnapshot.url.map(segment => segment.path).join('/');
              this.breadcrumbs.push({ 
                label: route.snapshot.data.breadcrumb,
                url:   url });
              currentRoute = route;
            }
          })
        } while(currentRoute);
      })
  }

  ngAfterViewInit() {
    const logoComponent = this.componentFactoryResolver
      .resolveComponentFactory(this.breadcrumbs[0]); // at the moment just using breadcrumbs[0] for simplicity
    this.parent.createComponent(logoComponent);
  }
}

Unfortunately, it appears that the route data property cannot / will not store a component. Delving into the debug console, the data property gets saved as {data: {breadcrumb: }}

If anyone knows of a way to make this code work, or has a suggestion for a better way to achieve my goal of building a breadcrumbs component that consumes other view components, I would greatly appreciate it!!

PS: If I skip the data property, and just manually ask the breadcrumb component to add a MainLogoComponent to the page like so:

ngAfterViewInit() {
    const logoComponent = this.componentFactoryResolver.resolveComponentFactory(MainLogoComponent);
    this.parent.createComponent(logoComponent);
  }

It works

And, for completeness, the code for the associated route.

const routes: Routes = [
  {
    path: 'calendar',
    data: {
      breadcrumb: MainLogoComponent
    },
    canActivate: [ AuthGuard ],
    children: [
      {
        path: '',
        component: CalendarComponent
      }
    ]
  }
];

UPDATE

Here is the relevant NgModule:

@NgModule({
  imports: [
    CommonModule,
    MyMaterialModule
  ],
  exports: [
    BreadcrumbsComponent
  ],
  entryComponents: [BreadcrumbsComponent, MainLogoComponent],
  declarations: [BreadcrumbsComponent] // MainLogoComponent is declared in MyMaterialModule
})
export class BreadcrumbsModule { }
like image 821
John Avatar asked Oct 18 '22 13:10

John


1 Answers

So I got it working as I wanted it to work (a BIG thank you to @yurzui for taking the time to create a plnkr showing it should work, in concept!!!!! That REALLY REALLY helped and I'm incredibly greatful!!). I'm still not exactly sure where I was doing wrong, but I created a new app to make the breadcrumbs module, got it working, and then ported it to my real app. One probable issue, I've discovered, is that ng serve seems to occationally miss recompiling files I've changed (or perhaps my browser is caching errors). Several times now, the compiler has spit out errors concerning variables I've deleted and when I restart ng serve (without changing anything) the errors go away. A warning for other people troubleshooting problems (though maybe it's isolated to my dev setup).

Here is the final, working breadcrumbs code (note, if you're trying to replicate my method also read through the info / links in the original question).

import { Component, AfterViewInit, ViewChild, ComponentFactoryResolver, ViewContainerRef } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import 'rxjs/add/operator/filter';

import { BreadDividerComponent } from './bread-divider/bread-divider.component';

@Component({
  selector: 'app-breadcrumbs',
  template: `
    <div #breadContainer></div>
    <div #iconContainer></div>
    <div #lastBreadContainer></div>`
})
export class BreadcrumbsComponent implements AfterViewInit {
  breadcrumbs: Array<any>;
  storedBreadcrumbs: Array<Component>;
  @ViewChild('breadContainer', {read: ViewContainerRef}) breadContainer: ViewContainerRef;
  @ViewChild('iconContainer', {read: ViewContainerRef}) iconContainer: ViewContainerRef;
  @ViewChild('lastBreadContainer', {read: ViewContainerRef}) lastBreadContainer: ViewContainerRef;

  constructor(private componentFactoryResolver: ComponentFactoryResolver,
    private router: Router) { }
  
  ngAfterViewInit() {
    this.router.events
      .filter(event => event instanceof NavigationEnd)
      .subscribe(event => {
        // Reset variables and clear breadcrumb component on view change
        this.breadcrumbs = [];
        this.storedBreadcrumbs = [];
        this.breadContainer.clear();
        this.iconContainer.clear();
        this.lastBreadContainer.clear();

        // Drill down through child routes
        let currentRoute = this.router.routerState.snapshot.root.firstChild;
        if (currentRoute) {
          do {
            this.breadcrumbs.push(currentRoute.data.breadcrumb);
            currentRoute = currentRoute.firstChild;
          } while (currentRoute)
        }

        // Because each route's home path is actually itself a child route
        // e.g. { path: 'two', children: [ { path: '', component: ... } ] }
        if (this.breadcrumbs.length === 2 
          && this.breadcrumbs[0] === this.breadcrumbs[1]) {
            this.breadcrumbs = [this.breadcrumbs[0]];
        }

        this.breadcrumbs.forEach((breadcrumb, index, array) => {
          if (array.length > 1) {
            if (index < array.length - 1) {
              const breadFactory = this.componentFactoryResolver
                .resolveComponentFactory(breadcrumb);
              this.storedBreadcrumbs.push(this.breadContainer.createComponent(breadFactory));

              const iconFactory = this.componentFactoryResolver
                .resolveComponentFactory(BreadDividerComponent);
              this.storedBreadcrumbs.push(this.iconContainer.createComponent(iconFactory));
            } else {
              const breadFactory = this.componentFactoryResolver
                .resolveComponentFactory(breadcrumb);
              this.storedBreadcrumbs.push(this.lastBreadContainer.createComponent(breadFactory));
            }
          } else {
            const breadFactory = this.componentFactoryResolver
              .resolveComponentFactory(breadcrumb);
            this.storedBreadcrumbs.push(this.lastBreadContainer.createComponent(breadFactory));
          }
        });
      });
  }
}

Breadcrumb view components are passed through the router's data property, like so:

const routes: Routes = [
  {
    path: 'calendar',
    data: {
      breadcrumb: CalendarBreadcrumbComponent,
      menu: CalendarMenuComponent
    },
    canActivate: [ AuthGuard ],
    children: [
      {
        path: 'events',
        data: {
          breadcrumb: EventsBreadcrumbComponent
        },
        component: EventsComponent
      },
      {
        path: '',
        pathMatch: 'full',
        redirectTo: 'events'
      }
    ]
  }
];

I also use this strategy to build my app's menu. Here's an example of a breadcrumb view component

import { Component, HostBinding } from '@angular/core';
import { DomSanitizer  } from '@angular/platform-browser';

@Component({
  selector: 'app-calendar-breadcrumb',
  templateUrl: './calendar-breadcrumb.component.html',
  styleUrls: ['./calendar-breadcrumb.component.scss']
})
export class CalendarBreadcrumbComponent {
  // Modify this components parent element so that it is displayed with flexbox
  @HostBinding('attr.style') style = this.sanitizer
    .bypassSecurityTrustStyle('display: flex; flex-direction: row;');

  constructor(private sanitizer: DomSanitizer) {}
}

And the associated view file (which, I'm realizing now, really doesn't need its own file...)

<md-icon>event</md-icon><h3>Calendar</h3>
like image 94
John Avatar answered Oct 20 '22 23:10

John