1

I have a side navigation component to be used on many pages. This is not a single-page web app. It is dynamically loaded on a parent component that toggles the menu. ContentChild is used to get a handle on the child component so that it can be toggled by setting @input overlayHidden. The problem is that ContentChild is null.

Run this plunker with the debug console open. Click "Dashboard" from the first page, and then "Toggle Menu" from the second page to see the null pointer.

What am I doing wrong?

Here is the code.

import { Component, Input} from 'angular2/core';
import { Router } from 'angular2/router';

@Component({
    selector: 'side-navigation',
    template: `
    <div id="overlay" [hidden]="overlayHidden">
    <div id="wrapper">
        <nav id="menu" class="white-bg active" role="navigation">
            <ul>
                <li><a href="#">Dashboard</a></li>
                <li><a href="#">Help & FAQs</a></li>
                <li><a href="#">Contact Us</a></li>
                <li><a href="#">Log Out</a></li>
            </ul>
        </nav>
    </div>
</div>
`
})
export class SideNavigationComponent {
    @Input() overlayHidden: boolean;
}

import { Component, 
    DynamicComponentLoader, 
    Injector, 
    ContentChild,   AfterContentInit 
} from 'angular2/core';

import { Router, RouterLink } from 'angular2/router';
import { SideNavigationComponent } from './side-navigation';

@Component({
    selector: 'dashboard',
    template: `
    <side-navigation id="side-navigation"></side-navigation>...
`,
directives: [ 
    RouterLink, 
    SideNavigationComponent ]
})
export class DashboardComponent {
    title = 'Dashboard';
    @ContentChild(SideNavigationComponent)
        sideNavigationComponent: SideNavigationComponent;

    constructor(private _router: Router, 
        private _dcl: DynamicComponentLoader, 
        private _injector: Injector) {
        this._router = _router;
        this._dcl = _dcl;
        this._injector = _injector;
        this._dcl.loadAsRoot(
            SideNavigationComponent, 
            '#side-navigation', this._injector);
    }

    toggleOverlay() {
        this.overlayHidden = !this.overlayHidden;
        this.sideNavigationComponent.overlayHidden = this.overlayHidden;
    }
}

I built a publish/subscribe solution that works (except for an issue with the 'hidden' attribute of the overlay div), but I thought this would be a simpler solution if I could get it to work.

  • You should use either `loadNextToLocation` or `loadIntoLocation` https://github.com/angular/angular/issues/6223#issuecomment-193908465 – Eric Martinez Mar 13 '16 at 16:39

2 Answers2

1

As Günter has suggested in his answer about @ContentChild and in addition of that you may think about this,
As discussed with Günter - see plunker
Sorry I didn't go into deep with your plunker and don't what you want to achieve further. But if you're going to implement ViewChild api think about to implement AfterViewInit hook as well. And it also works with DynamicComponentLoader.

micronyks
  • 54,797
  • 15
  • 112
  • 146
0

There are several issues

  • LoadAsRoot doesn't invoke change detection

Currently loadAsRoot() is only used to bootstrap the root component (AppComponent) and inputs aren't supported on the root component.

A workaround is explained in this comment https://github.com/angular/angular/issues/6370#issuecomment-193896657

when using loadAsRoot you need to trigger change detection and manually wire up Inputs, Outputs, Injector, and the component dispose function

function onYourComponentDispose() {
}
let el = this.elementRef
let reuseInjectorOrCreateNewOne = this.injector;
this.componentLoader.loadAsRoot(YourComponent, this.elementRef, reuseInjectorOrCreateNewOne, onYourComponentDispose)
.then((compRef: ComponentRef) => {
  // manually include Inputs
  compRef.instance['myInputValue'] = {res: 'randomDataFromParent'};
  // manually include Outputs
  compRef.instance['myOutputValue'].subscribe(this.parentObserver)
  // trigger change detection
  cmpRef.location.internalElement.parentView.changeDetector.ref.detectChanges()
  // always return in a promise
  return compRef
});

This comment https://github.com/angular/angular/issues/7453#issuecomment-193138577 also has a link to a Plunker example demonstrating the workaround

  • DynamicComponentLoader can't be used in constructor(). This code should should be moved to ngAfterViewInit():

    this._dcl.loadAsRoot( SideNavigationComponent, '#side-navigation', this._injector);

  • as @Miconyks pointed out @ViewChild() should be used instead of @ContentChild(). @ContentChild() is for content passed to <ng-content>

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567