0

I have a component that I'm using to display some common output to my application. This component is injected into other services so that those services can trigger the behavior. After a lot of troubleshooting I think I've tracked the issue to Angular's DI creating multiple instances of the component. I've created a bare-bones version that illustrates the issue. This is with Angular 2.4.x

AppModule:

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {AppComponent} from './app.component';
import {TestComponent} from "./test.component";

@NgModule({
    declarations: [
        AppComponent,
        TestComponent
    ],
    imports: [BrowserModule, FormsModule, HttpModule],
    providers: [TestComponent],
    bootstrap: [AppComponent]
})
export class AppModule {
}

Test Component (this is a simplified version of a component that I'm trying to use to display info and use it as a service):

import {Component, OnInit} from '@angular/core';
@Component({
    selector: 'app-test',
    template: `<p>Message:</p>
    <p *ngIf="show">hi</p>`
})
export class TestComponent {
    private show: boolean = false;
    doTest() {
        setTimeout(() => {
            console.log('timeout callback');
            this.show = true;
        }, 5000);
    }
}

App Component (component using my Test Component):

import {Component, OnInit} from '@angular/core';
import {TestComponent} from "./test.component";

@Component({
    selector: 'app-root',
    template:`
<h1>{{title}}</h1>
<app-test></app-test>
`
})
export class AppComponent implements OnInit{
    title = 'app works!';
    constructor(private test: TestComponent) { }
    ngOnInit() {
        this.test.doTest();
    }
}

The behavior I was hoping for is that AppComponent would call TestComponent's doTest function and the TestComponent would show the 'hi' message after 5 seconds.

The callback happens and I see the console message, but 'hi' isn't displayed. I think that's do to dependency injection providing separate instances so the instance that's injected App's constructor is different from the instance in App's template.

If my understanding is correct, how do I make it so that it's the same instance in both cases? And am I missing a better way to go about achieving this behavior?

josh_in_dc
  • 119
  • 1
  • 2
  • 11
  • To get a reference to the test component you can use `` in your `AppComponent`'s template or use the `ViewChild` decorator. Injecting it into the constructor is not correct but it is a reasonable thing to think of doing. – Aluan Haddad Jul 19 '17 at 19:35
  • 1
    `TestComponent` or any other components should NOT be declared as providers in the `provider` property. Generally, you'd only put classes decorated with @Injectable(), usually services. – Alexander Staroselsky Jul 19 '17 at 19:45
  • I was originally going to have a separate service but I read somewhere (another stack overflow post I think) that Component extends Injectable, so anything marked as a component could be treated as an injectable (whether or not that's a wise thing to do...) – josh_in_dc Jul 19 '17 at 19:47
  • @AlexanderStaroselsky you are absolutely correct that components should not be added to the `providers` array, however it has nothing to do with the `Injectable` decorator factory. – Aluan Haddad Jul 19 '17 at 19:52
  • 1
    @AluanHaddad you're absolutely right. I agree with your approach to utilize `ViewChild` to solve this issue. My goal with my comment was to promote better practices regarding `providers`. Thanks! – Alexander Staroselsky Jul 19 '17 at 19:58
  • @AlexanderStaroselsky which is quite a good thing to do. I'm glad you mentioned it. I missed it until I read your comment. – Aluan Haddad Jul 19 '17 at 19:58

1 Answers1

1

You need to use the ViewChild decorator to access a child component instead of injecting it into your constructor. Intuitively, what you are trying to do makes sense but it will not work.

Please note that as, Alexander Staroselsky points out in his comment, you should not list any components in your providers array!

Here is what you need to write

import { Component, NgModule, ViewChild } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

@Component({
    selector: 'app-test',
    template: `
        <p>Message:</p>
        <p *ngIf="show">hi</p>
    `
})
export class AppTestComponent {
    doTest() {
        setTimeout(() => {
            console.log('timeout callback');
            this.show = true;
        }, 3000);
    }
    show = false;
}

@Component({
    selector: 'my-app',
    template: '<app-test></app-test>',
})
export class App {
    // this queries your view for elements of the type passed to the
    // ViewChild decorator factory.
    @ViewChild(AppTestComponent) test: AppTestComponent;

    ngAfterContentInit() {
        this.test.doTest();
    }
}

@NgModule({
    imports: [BrowserModule],
    declarations: [App, AppTestComponent],
    bootstrap: [App]
})
export class AppModule { }

Here is a working example

https://plnkr.co/edit/DDx4INrgixsnJKiU1E0h?p=preview

If you were working solely with the child component from your view, you could leave out all of the additional decorators and lifecycle hooks.

For example:

@Component({
    selector: 'my-app',
    template: `
        <!-- give the child component a name for use in the template -->
        <app-test #test>
        </app-test>
        <button (click)="test.doTest()">Do Test</button>
    `,
})
export class App {}

Here is a working example

https://plnkr.co/edit/nnXW2z7pbgdTk5Qzl1Sw?p=preview

Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
  • 1
    Thanks. I think this solves my problem as I illustrated above, but I think in trying to create a simple version of my problem (as opposed to posting actual code snippets) I slightly misrepresented my problem. Component A has my TestComponent, but it's a 3rd service (call it Service B) that's actually invoking TestComponent's doTest method. I think I should probably do something like this: https://stackoverflow.com/a/36404625/3621712 Create a service and have my Test Component listen to events from the service. Other services could then call the method that will trigger a new event. – josh_in_dc Jul 19 '17 at 20:02
  • Perhaps elaborate the question or ask a new one. – Aluan Haddad Jul 19 '17 at 20:04
  • @josh_in_dc based on your updated comment, I agree with the approach suggested in the linked answer. There are various ways to achieve this but you definitely need an intermediary of some kind. A service is a logical choice but there are other ways as well. – Aluan Haddad Jul 19 '17 at 20:11
  • 1
    Just out of curiosity, what would some other options for the intermediary be? – josh_in_dc Jul 20 '17 at 11:47
  • 1
    You could use plain old shared object or function exported by some ECMAScript module in your app (OK, but not ideal). You could use a global variable (terrible), you could use `localStorage` and its native events (interesting if you wanted to cache). That said, a service is your best bet here but I wanted to point out that the "Angular way" is not the only way. – Aluan Haddad Jul 20 '17 at 11:58
  • 1
    Thank you. Coming from a Java (and all the assorted options under that umbrella) background doing Angular recently has been an interesting experience. Always appreciate learning about all the options that are available. – josh_in_dc Jul 20 '17 at 12:26
  • JavaScript is about 10x as flexible as Java, I barely scratched the surface of the various ways you can accomplish this. Any kind of pattern can be implemented in JavaScript and with TypeScript (or perhaps Flow), it can be formally specified as well. That said, the best things in JS are the simple things, functions, arrays, and object literals will get you incredibly far, so keep it simple where you can. For example, I've never once needed a class outside of when a framework required one, and I use Dependency Injection all over the place. – Aluan Haddad Jul 20 '17 at 12:29