Search code examples
arraysangularngonchanges

Does array change to an object after @Input() into child?


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, ... ]


Solution

  • 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