Search code examples
angularangular4-router

Angular 4 - invoking child component method from app component


In a nutshell I've got an Angular 4 app, that has a header containing an android inspired floating action button, When clicked this button should execute a method on the currently active component (the page content basically). I'm using the angular 4 router if that makes any difference, It raised some concerns with the View Child approach because i won't always be sure what component is currently active and if possible i'd like to avoid adding #binding attributes to every single template.

I've attempted this by ViewChild decorators, ContentChild decorators, Events, Services, So far absolutely nothing has worked and i'm flat out stuck. At this point i'm not sure if this is genuinely complicated or it's just PEBCAK.

The button in question is contained within app.component -

app.component.ts

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

export class AppComponent {
  currentUrl: string = null;

  constructor(private router: Router, private currentRoute: ActivatedRoute) {}

  ngOnInit() {
    this.router.events.subscribe((val) => {
      this.currentUrl = this.router.url;
    });
  }
}

app.component.html (just the button for brevity)

<div class="fab-container">
  <button class="btn btn-warning btn-fab" (click)="fabAction()">
    +
  </button>

The general idea here is that when this button is clicked, It executes the fabAction() method on the child component;

@Component({
  selector: 'app-course-list',
  templateUrl: './course-list.component.html',
  styleUrls: ['./course-list.component.scss']
})
export class CourseListComponent implements OnInit {

  courses: Course[];
  showFilter: boolean = false;

  constructor(
    private courseService: CourseService
  ) { }

  ngOnInit(): void {
    this.getCourses();
  }

  getCourses(): void {
    this.courseService.getCourses().then(courses => this.courses = courses);
  }

  // Should execute this method when button is clicked on app component
  fabAction(): void {
    console.log('responding to click on parent component');
    this.showFilter = !this.showFilter;
  }
}

</div>

One thing worth nothing is that each component will have it's own action to execute when the fab button is clicked on the app component, Some will open modals, others will show/hide things, Some will redirect so it'd be really nice to be able to specify this behaviour on the component itself.

Reading the official angular docs on this has left me with more questions than answers, So I've taken it all back to a clean slate in hopes someone can suggest a more ideal way of tackling this.

Edit - Full markup for app.component.html as requested

<header *ngIf="currentUrl != '/landing'">
  <div class="brand">
    <img src="/assets/circle-logo.png" />
  </div>
  <nav>
    <div class="page-title">
      <h2>{{ page?.title }}</h2>
      <h4>{{ page?.strapline }}</h4>
    </div>
    <ul class="header-nav">
      <li [routerLink]="['/courses']">Courses</li>
      <li>Dashboard</li>
      <li>Community</li>
      <li>Blog</li>
      <li>About</li>
    </ul>
    <div class="fab-container">
      <button class="btn btn-warning btn-fab" (click)="fabAction()">
        +
      </button>
    </div>
  </nav>
</header>
  <router-outlet></router-outlet>
<app-footer></app-footer>

Here's my app-routing module where i define my routes and which component to display for which route (as done in the tour of heroes angular 4 demo)

const routes: Routes = [
    { path: '', redirectTo: 'landing', pathMatch: 'full' },
    { path: 'landing', component: LandingComponent },
    { path: 'courses', component: CourseListComponent },
    { path: 'course/:id', component: CourseComponent }
];

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

export class AppRoutingModule {}

Solution

  • Create a service that has an RxJS Subject which you call next on when the FAB is pressed. Then in any component that cares, you can subscribe to the subject.

    import { Subject } from 'rxjs/Subject'
    
    @Injectable()
    export class FabService {
      clickSubject = new Subject();
    }
    

    app.component

    @Component(...)
    export class AppComponent {
    
      constructor(private fabService: FabService) {}
    
      fabAction() {
        this.fabService.clickSubject.next();
      }
    }
    

    child.component

    @Component(...)
    export class ChildComponent implements OnInit, OnDestroy {
    
      private clickSub: Subscription;
    
      constructor(private fabService: FabService) {}
    
      ngOnInit() {
        this.clickSub = this.fabService.clickSubject.subscribe(() => {
          // FAB was clicked
        });
      }
    
      ngOnDestroy() {
        this.clickSub.unsubscribe();
      }
    }
    

    The only situation I can think of where this might not work is if you lazy-load feature modules, since those will get separate instances of the FabService (or whatever you end up calling it). I think there's a way around that, but I can't think of it off the top of my head.