45

I have a parent component which is getting data from a server through a service. I need some of this data to be passed to the Child component.

I've been trying to pass this data with the usual @Input() however as I expected the data I'm getting in the Child component is undefined.

Example

Parent Component

@Component({
    selector: 'test-parent',
    template: `
        <test-child [childData]="data"></test-child>
    `
})

export class ParentComponent implements OnInit {
    data: any;

    ngOnInit() {
        this.getData();
    }

    getData() {
        // calling service and getting data
        this.testService.getData()
            .subscribe(res => {
                this.data = res;
            });
    }
}

Child Component

@Component({
    selector: 'test-child',
    template: `
        <!-- Content for child.... -->
    `
})

export class ChildComponent implements OnInit {
    @Input() childData: any;

    ngOnInit() {
        console.log(childData); // logs undefined
    }
}

I've kept the example simple so as to show what I'm trying to do. My question is whether it's possible to pass data to the child after the initialisation. I'm not sure but would two-way data binding help in this case?

More Investigation

Following the posted answers I've tried using the ngOnChanges lifecycle hook. The issue I'm having is that the ngOnChanges function is not being fired when the data is updated. The data is an object which is why I think the changes are not being detected.

Updated code in the Child Component

@Component({
    selector: 'test-child',
    template: `
        <!-- Content for child.... -->
    `
})

export class ChildComponent implements OnChanges {
    @Input() childData: any;

    ngOnChanges() {
        console.log(childData); // logs undefined
    }
}

I've tried a test with the Live Examples on Plunker from the Angular team showcasing the Lifecycle Hooks. In the test I've updated the OnChangesComponent to pass an object which as can be seen in the example when updating an object the ngOnChanges is not detecting the change. (This is also shown in this question)

https://plnkr.co/edit/KkqwpsYKXmRAx5DK4cnV

Seems that the ngDoCheck could help solve this issue, but it seems to me that it would not help performance in the long run as every change is detected by this Lifecycle Hook.

Also I know that I could probably use the @ViewChild() to access the Child Component and set the variables that I need, but I don't think that for this use-case it makes much sense.

Posting more code to help explain better

Parent Component

@Component({
    selector: 'test-parent',
    template: `
        <test-child [childType]="numbers" [childData]="data" (pickItem)="pickNumber($event)">
            <template let-number>
                <span class="number" [class.is-picked]="number.isPicked">
                    {{ number.number }}
                </span>
            </template>
        </test-child>
        <test-child [childType]="letters" [childData]="data" (pickItem)="pickLetter($event)">
            <template let-letter>
                <span class="letter" [class.is-picked]="letter.isPicked">
                    {{ letter.displayName }}
                </span>
            </template>
        </test-child>
        <button (click)="submitSelections()">Submit Selection</button>
    `
})

export class ParentComponent implements OnInit {
    data: any;
    numbersData: any;
    lettersData: any;

    ngOnInit() {
        this.getData();
    }

    getData() {
        // calling service and getting data for the game selections
        this.testService.getData()
            .subscribe(res => {
                this.data = res;
                setChildData(this.data);
            });
    }

    setChildData(data: any) {
        for(let i = 0; i < data.sections.length; i++) {
            if(data.sections[i].type === 'numbers') {
                this.numbersData = data.sections[i];
            }

            if(data.sections[i].type === 'letters') {
                this.lettersData = data.sections[i];
            }
        }
    }

    pickNumber(pickedNumbers: any[]) {
        this.pickedNumbers = pickedNumbers;
    }

    pickLetter(pickedLetters: any[]) {
        this.pickedLetters = pickedLetters;
    }

    submitSelections() {
        // call service to submit selections
    }
}

Child Component

@Component({
    selector: 'test-child',
    template: `
        <!--
            Content for child...
            Showing list of items for selection, on click item is selected
        -->
    `
})

export class ChildComponent implements OnInit {
    @Input() childData: any;
    @Output() onSelection = new EventEmitter<Selection>;

    pickedItems: any[];

    // used for click event
    pickItem(item: any) {
        this.pickedItems.push(item.number);

        this.onSelection.emit(this.pickedItems);
    }
}

That's more or less the code that I have. Basically I have a parent that handles selections from child components and then I submit these selections to the service. I need to pass certain data to the child because I'm trying to have the object that the child returns in the format that the service is expecting. This would help me not to create the whole object expected by the service in the parent component. Also it would make the child reuseable in the sense that it sends back what the service expects.

Update

I appreciate that users are still posting answers on this question. Note that the code that has been posted above worked for me. The issue I had was that in a particular template I had a typo which caused for the data to be undefined.

Hope that it's proving to be helpful :)

Community
  • 1
  • 1
Daniel Grima
  • 2,765
  • 7
  • 34
  • 58

5 Answers5

38

Since data is undefined at start, you can postpone it with *ngIf='data'

<div *ngIf='data'>
   <test-child [childData]="data"></test-child>
</div>

Or you can implement ControlValueAccessor on your component and pass it by ngModel with ngModelChange

<test-child [ngModel]="data?" (ngModelChange)="data? ? data= $event : null"></test-child>
Mopa
  • 521
  • 1
  • 6
  • 13
  • 1
    *ngIf='data' is not the best solution in my opinion. For me it didn't work. ngOnChanges, instead, is working fine – Tuvia Khusid Jul 25 '19 at 00:07
19

you can use ngOnChanges to get data like below,

 ngOnChanges() {
   if(!!childData){         
        console.log(childData);         
   }
 }

OnChanges

Angular calls its ngOnChanges method whenever it detects changes to input properties of the component (or directive).

Read more about it here.

Update

ngOnChanges will only fire if you change entire object, if you want to just update property of the bound object not entire object you have two options,

  1. either bind the property directly in the template, make sure to use ? like {{childData?.propertyname}}

  2. or you can create a get property depending upon childdata object,

    get prop2(){
      return this.childData.prop2;
    }
    

Copmlete Example,

import { Component, Input } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `<h1>Hello {{name}}</h1>
  <child-component [childData]='childData' ></child-component>
  `
})
export class AppComponent { 
  name = 'Angular'; 
  childData = {};
  constructor(){
    // set childdata after 2 second
    setTimeout(() => {
      this.childData = {
        prop1 : 'value of prop1',
        prop2 : 'value of prop2'
      }
    }, 2000)
  }
}

@Component({
  selector: 'child-component',
  template: `
    prop1 : {{childData?.prop1}}
    <br />
    prop2 : {{prop2}}
  `
})
export class ChildComponent { 
  @Input() childData: {prop1: string, prop2: string};

  get prop2(){
    return this.childData.prop2;
  }
}

Here is the Plunker!

Hope this helps!!

Madhu Ranjan
  • 17,334
  • 7
  • 60
  • 69
  • Thanks for your answer, it seems to have pointed me in the right direction. The only problem I'm having now is that if the data was a "simple" string the `ngOnChanges` is detecting changes, however if it's an object for some reason it's staying `undefined`. – Daniel Grima Dec 16 '16 at 09:25
  • @DanielGrima: Updated the answer. – Madhu Ranjan Dec 16 '16 at 20:55
  • Thanks again for your help... I just found out what the issue was. Had a typo in my template variable and so it was always undefined of course. Didn't realise before! At least it seems like it's working and apparently I don't need any `ngOnChanges` or getters for the object.. seems okay now. – Daniel Grima Dec 17 '16 at 08:58
8
@Component({
    selector: 'test-child',
    template: `
        <!-- Content for child.... -->
    `
})

export class ChildComponent implements OnChanges {
    @Input() childData: any;

    ngOnChanges() {
        console.log(childData); // logs undefined
    }
}

This is supposed to be like this, no? I think missing "this" keyword is the problem.

    ngOnChanges() {
        console.log(this.childData); // reference with this
    }
Nambi N Rajan
  • 491
  • 5
  • 15
5

At the time your ChildComponent (you called it also ParentComponent, but i guess it's a typo) gets initialized and executes its ngOnInit, there is no data in your parent avaible since your service will still be out getting your data. Once your data is there it will be pushed to the child.

What you can do depends on your usecase..

If you just want to display the data in your ChildComponent template, you could use the elvis operator (?). Something like: {{ childData?}} or {{ childData?.myKeyName }}...

If you want to do some other stuff, you may use a setter in your ChildComponent. It will intercept the input change and give you the opportunity to alter/test/"do what ever you want" with it before you do other things with it in your ChildComponent.. eg.:

@Component({
    selector: 'test-child',
    template: `
        <!-- Content for child.... -->
    `
})

export class ChildComponent implements OnInit {
    private _childData: any;
    @Input()
    set childData(parentData: any) {
        // every time the data from the parent changes this will run
        console.log(parnetData);
        this._childData = parentData; // ...or do some other crazy stuff
    }
    get childData(): any { return this._childData; }

    ngOnInit() {
      // We don't need it for that...
    }
}

or use ngOnChanges as Madhu mentioned.

You also might want to take a look at the cookbook entry for Component Communication in the official docs.

benny_boe
  • 881
  • 8
  • 9
  • Yeah it was a typo as you mentioned - I've updated the question with a few more points. I've tried your suggestion as well, however I'm still getting the same result :/ – Daniel Grima Dec 16 '16 at 10:47
  • would you mind posting your original code? I'm not 100% clear on why it doesn't work for you... One other thing you might want to try is - given you don't do anything other in your ParentComponent with your data from the service - put your `testService.getData()` directly into your child input and let angular handle the subscribtion with the `async` pipe.. like so: `` – benny_boe Dec 16 '16 at 12:22
  • I've updated the question with more code.. hope that it makes it more clear... thanks for your help! – Daniel Grima Dec 16 '16 at 12:50
  • Thanks for your help, as I commented above my problem was a template variable typo! So frustrating that I didn't notice it before... seems to be working fine now.. thanks again :) – Daniel Grima Dec 17 '16 at 08:58
  • Glad you've got it running now. :) Maybe you would want to try out Visual Studio Code (or the typescript plugin for your editor of choise) for the typechecking in the IDE oder run codelyzer from time to time to lint your code and prevent such typos :) – benny_boe Dec 17 '16 at 11:42
  • Yeah I do have all that set up :) the typo was in the template which unfortunately is not picked up as an error. – Daniel Grima Dec 17 '16 at 14:33
  • 1
    Fyi: linting for that will come to vs code :) when u are using the angular-cli in the newest version "ng lint" will give you the errors for templates also ;) – benny_boe Dec 17 '16 at 19:17
0

I'm assuming that the child is getting a subset of the parents data, potentially from a user action (like loading a form when a user clicks on a grid row). In this case I would use an event.

Have the parent fire an event, maybe dataSelected, and the contents of the event would be the data that was selected.

Then have the child listen for those events and when fired load the data from the event into the model.

This will help decouple your components, which always a good idea.

R. McIntosh
  • 166
  • 4