Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access ViewChild reference defined on parent component - Angular 2

I have a spinner/loader element defined in app template (root component's template) like

<!--I have it here so that I don't have to paste it in all my templates-->
<div #spinner></div>

In my child components, I am trying to access it using @ViewChild but that seems to always return undefined. My code for accessing this in child component is

@ViewChild('spinner', { read: ViewContainerRef }) container: ViewContainerRef;  //this is always undefined

However when I place my #spinner in my child component's HTML, it gets picked up correctly.

Is there a way to get the element defined in parent component in your child component as a ContainerRef?

I need the view reference to create the component on it dynamically using ComponentFactoryResolver.

It seems a similar issue but can't find a way to overcome.

EDIT: I am now using a shared service with the observable, but still it doesn't raise an event on .next.

Here is my code in SpinnerComponent

@Component({
    selector: 'spinner',
    styleUrls: ['app/styles/spinner.component.css'],
    template:
    `<div [hidden]="state.visible" class="in modal-backdrop spinner-overlay"></div>
     <div class="spinner-message-container" aria-live="assertive" aria-atomic="true">
        <div class="spinner-message" [ngClass]="spinnerMessageClass">{{ state.message }}</div>
    </div>`
})
export class SpinnerComponent {

    constructor(spinnerService: SpinnerService) {
        spinnerService.spinnerStatus.subscribe(event => {
            console.log('Event: ' + event); <= not getting called
            this.state.visible = event;
        });
    }

    public state = {
        message: 'Please wait...',
        visible: false
    };
}

In SpinnerService, I have

@Injectable()
export class SpinnerService {
    public events: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

    public get spinnerStatus(): Observable<boolean> {
        return this.events.asObservable();
    }

    public showSpinner() {
        this.events.next(true);
    }

    public hideSpinner() {
        this.events.next(false);
    }
}

And in the calling component, I have

@Component({
    selector: 'edit-auction',
    templateUrl: '/auctions/edit.html'
})
export class EditAuctionComponent {

    constructor(public spinnerService: SpinnerService) {  }

    ngAfterViewInit() {
        //start the spinner
        this.spinnerService.showSpinner();
    }
}

In app.module.ts (root module)

@NgModule({
    imports: [BrowserModule, FormsModule, HttpModule, routes],
    declarations: [..],
    providers: [NotificationsService, SpinnerService],
    bootstrap: [AppComponent]
})
export class AppModule { }
like image 653
Ali Baig Avatar asked Feb 05 '17 08:02

Ali Baig


2 Answers

Accessing data from other components does not sounds good to me.

For what you are trying to do probably best would be to define service which will share observable:

@Injectable()
export class EventService {
    public selectedCategoryName: string = '';
    private events = new BehaviorSubject<Boolean>(false);
    constructor() {

    }

    public showSpinner(){
        this.events.next(true)
    }

    public hideSpinner(){
        this.events.next(false);
    }

    public get spinnerStatus() : Observable<boolean> {
        return this.events.asObservable();
    }
}

Then in your root component you will subscribe to

eventServiceInstance.spinnerStatus.subscribe(state=>{
            //thisSpinner.visible = state
        })

And now in all other places you will call

eventServiceInstance.showSpinner()
eventServiceInstance.hideSpinner()

PS. To make it works EventService provider should be added in NgModule and not inside of components

like image 80
Vova Bilyachat Avatar answered Nov 01 '22 23:11

Vova Bilyachat


Although it's better to use either Output parameters or a common service for this purpose its possible to inject a component from a child component by:

  1. Inject the app component to the child component
  2. In the app component add a ViewChild to the wrapper element and make it accessible
  3. From the child create the new component with ComponentFactoryResolver by calling createComponent on the wrapper element ViewContainerRef
  4. add the loaded component to entryComponents of the module

code is available in plunker

app.ts:

@Component({
selector: 'my-app',
template: `
    <div>
    <h2>App/h2>
    <my-child></my-child>
    <div #spinner></div>
    </div>
`,
})
export class App {
@ViewChild('spinner', { read: ViewContainerRef }) private spinner: any;

public getSpinnerRef() {
    return this.spinner;
}

}

Child Component:

@Component({
selector: 'my-child',
template: `
    <div>
    <h3>Child</h3>
    </div>
`,
})
export class ChildCmp implements OnInit {

constructor(private app: App, private componentFactoryResolver: ComponentFactoryResolver) {

}

public ngOnInit() {
    const spinnerCmp = this.componentFactoryResolver.resolveComponentFactory(SpinnerCmp);
    this.app.getSpinnerRef().createComponent(spinnerCmp);
}


}
like image 26
Ofer Herman Avatar answered Nov 02 '22 00:11

Ofer Herman