1

I'm quite new to Angular 2 and I would like to transfer an array made in a parent component, via @Input(), to its child.

In the parent I create the array, add data from a service, and display it in the console (Console output 1). In the child component I then use ngOnChanges to display it in the console again (Console output 2). As you can see below, the length of the array changes from 12 to 0. I suppose this is because the array changes to an object when it's passed to the child?

How would I fix this?

Parent

import { Component, OnInit } from '@angular/core';
import { Module, MapMarkerData } from './coreclasses';
import { TimelineService } from './input.service';

@Component({
  selector: 'my-app',
  templateUrl: 'app/app.component.html',
  providers: [TimelineService]
})

export class AppComponent implements OnInit {
  modules: Module[];
  mapMarkerData: any;

  constructor(private timelineService: TimelineService) {
    this.mapMarkerData = new Array<MapMarkerData>();
  }

  getModules(): void {
    this.timelineService.getModules().then(modules => {this.modules = modules; this.setMapModuleData(this.modules);});
  }

  setMapModuleData(modules: Array<any>): void {
    for (let module of modules) {
      if (module.className) {
        var id = module.id;
        var className = module.className;
        let contents: Object = {id: id, className: className};
        this.mapMarkerData.push(contents);
      }
    }
    console.log(this.mapMarkerData); // CONSOLE OUTPUT 1
    console.log(this.mapMarkerData.length);
  }
}

Child

import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { MapMarkerData } from './coreclasses';

@Component({
    selector: 'timeline-map',
    templateUrl: 'app/timeline.map.component.html'
})

export class TimelineMapComponent implements OnChanges {
    @Input()
    mapMarkerData: any;

    ngOnChanges(changes: any) {
      console.log(this.mapMarkerData);  // CONSOLE OUTPUT 2
      console.log(this.mapMarkerData.length);
    }
}

Parent Template

...
<div id="map" class="mapLarge">
  <timeline-map [mapMarkerData] = "mapMarkerData"></timeline-map>
</div>
...

Console Output 1 Array[12]: [Object, Object, ... ]

Console Output 2 Array[0]: [Object, Object, ... ]

Merlin
  • 43
  • 1
  • 5

1 Answers1

2

EDIT Important

because you're passing same reference into child component, so the ngOnChanges lifecycle only fired 1 time.

please checkout this version, open your console tabs: https://plnkr.co/edit/WUDGOx?p=preview

so, if you wanna catch every changes in ngOnChanges lifecycle, you must passing a difference array, like this: https://plnkr.co/edit/8awiqe?p=preview

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

@Component({
  selector: 'app-root',
  template: `
    <h2>App Component</h2>
    <p><strong>This app will trigger ngOnChanges with immutable array</strong></p>
    <app-content [posts]="posts">
    </app-content>
  `
})
export class AppComponent implements OnInit {
  latestPosts: any[] = [];
  posts: any[] = [];


  ngOnInit() {
    // fake api call
    setTimeout(() => {
      this.latestPosts.push.apply(this.latestPosts, [
        {name: 'Post no.1'}, 
        {name: 'Post no.2'},
        {name: 'Post no.3'}
      ]);
      this.posts = [].concat(this.latestPosts);
    }, 300);
  }

}

=== 2nd option === you could check by yourself in DoChecklifecycle: https://plnkr.co/edit/oxsISD?p=preview

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

@Component({
  selector: 'app-content',
  template: `
    Status: {{ status }}
    <div *ngFor="let post of pp">
      {{ post.name }}
    </div>
  `
})

export class ContentComponent implements DoCheck {

  @Input()
  posts: any[];
  differ: IterableDiffers;
  status: string = '';

  constructor(private differs: IterableDiffers) {
        this.differ = this.differs.find([]).create(null);
    }

  ngDoCheck() {
    var changes = this.differ.diff(this.posts);
    if (changes) {
      console.log('ngDoCheck');
      this.status = 'ngDoCheck invoked!'
    }
  }
}

Note that you must pay a cost because the above ngDoCheck method will invoke on every change detection run.

https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html

https://angular.io/docs/ts/latest/api/core/index/DoCheck-class.html

https://angular.io/docs/ts/latest/api/core/index/SimpleChange-class.html

https://angular.io/docs/ts/latest/api/core/index/IterableDiffers-class.html

END

for the initial state, it's empty, then the value will assign to this property.

js log async js log async

Tiep Phan
  • 12,386
  • 3
  • 38
  • 41
  • Thanks for responding so quickly! Console output 2 isn't empty (see [Object, Object, ...]), and contains the contents of the array it received from the parent, but its length is somehow zero. console.log(changes['mapMarkerData'].currentValue.length) also gives 0, but I can see it contains the array contents, and console.log(changes['mapMarkerData'].previousValue) is empty, as it should be. So the child received the modified array from its parent, but its displayed length has changed? This really doesn't make sense to me? – Merlin Feb 04 '17 at 12:16
  • hey man, this is js way, if you `log` an array empty at timeline 0, then timeline 1, you put value to this, it'll output length 0, but content is not empty – Tiep Phan Feb 04 '17 at 12:24
  • open console tab(f12), then try this http://jsbin.com/perujayewu/edit?js,console,output – Tiep Phan Feb 04 '17 at 12:28
  • I see! But what if I now want to use this array? I'd like to loop over its contents, and that doesn't work as it is right now. – Merlin Feb 04 '17 at 13:13
  • iust do this in `ngOnInit() life-cycle` – Tiep Phan Feb 04 '17 at 13:14
  • Is there any chance you could give an example? – Merlin Feb 04 '17 at 13:19
  • That doesn't seem to be making much of a difference. Even in ngOnInit() as in your example, I end up with an array[0]. If I then loop over this array (e.g. for (let k in this.mapMarkerData) { console.log('test') }) I get nothing (probably because the length of the array is zero). I really hate bothering you with this but I've already spent hours on it and can't seem to figure it out. – Merlin Feb 04 '17 at 13:41
  • huh, as i can see your code only have data after finished get information from your service. so, i have idea, you create an event, whenever `console.log(this.mapMarkerData); // CONSOLE OUTPUT 1` this line call, just public event to other subscriber, in child component, you subscribe to this event, then whenever even fire, you'll get the data. 2nd, check in ngOnChange or ngOnInit `if (this.mapMarkerData && this.mapMarkerData.length > 0) { // do something here }` – Tiep Phan Feb 04 '17 at 13:53
  • Ok, thank you very much! I can't put everything in ngOnInit, but I'll use your second method or try to find another way. I still don't understand why my first code didn't work. The changes in the array are reflected in the template (where I use a for-loop), but I can't use a for-loop in the code of the component itself. That just seems weird. But thanks anyway! – Merlin Feb 04 '17 at 16:43
  • I think I found the cause of my problems and I thought I'd let you know. MapMarkerData is a custom class and so is a mutable object. Therefore, the changes I apply in setMapModuleData will not trigger ngOnChanges in the child, unless I reset mapMarkerData within that method. That's what I do now, and it works perfectly! I'm looking into this a bit further, see e.g. [link](https://vsavkin.com/immutability-vs-encapsulation-90549ab74487#.cppuh61b2) – Merlin Feb 04 '17 at 17:12
  • your `mapMarkerData` working as same as `latestPosts` in my demo code, when you using same `reference`, the ngOnChange won't fire. i don't know what `I reset mapMarkerData` do, i think this will create new Object/reference. just like i create new array. btw, the `slice`, `concat` method of Array class will create new array, but the `splice, push` working on same reference. be careful when using these methods – Tiep Phan Feb 04 '17 at 17:20
  • Hey Merlin, I have similar problem, I want to use @input data as table datasource. But its length in child component is 0. can you give me sample code how you get data into it. – Ninja Turtle Aug 18 '20 at 04:07