128

I have a component which receives an array of image objects as Input data.

export class ImageGalleryComponent {
  @Input() images: Image[];
  selectedImage: Image;
}

I would like when the component loads the selectedImage value be set to the first object of the images array. I have tried to do this in the OnInit lifecycle hook like this:

export class ImageGalleryComponent implements OnInit {
  @Input() images: Image[];
  selectedImage: Image;
  ngOnInit() {
    this.selectedImage = this.images[0];
  }
}

this gives me an error Cannot read property '0' of undefined which means the images value isn't set on this stage. I have also tried the OnChanges hook but I'm stuck because i can't get information on how to observe changes of an array. How can I achieve the expected result?

The parent component looks like this:

@Component({
  selector: 'profile-detail',
  templateUrl: '...',
  styleUrls: [...],
  directives: [ImageGalleryComponent]
})

export class ProfileDetailComponent implements OnInit {
  profile: Profile;
  errorMessage: string;
  images: Image[];
  constructor(private profileService: ProfileService, private   routeParams: RouteParams){}

  ngOnInit() {
    this.getProfile();
  }

  getProfile() {
    let profileId = this.routeParams.get('id');
    this.profileService.getProfile(profileId).subscribe(
    profile => {
     this.profile = profile;
     this.images = profile.images;
     for (var album of profile.albums) {
       this.images = this.images.concat(album.images);
     }
    }, error => this.errorMessage = <any>error
   );
 }
}

The parent component's template has this

...
<image-gallery [images]="images"></image-gallery>
...
zgue
  • 3,793
  • 9
  • 34
  • 39
Optimus Pette
  • 3,250
  • 3
  • 29
  • 50
  • 1
    How is the `images` data being populated in the parent component? I.e., is it via an http request(s)? If so, you might be better off having the ImageGalleryComponent subscribe() to the http observable. – Mark Rajcok Jun 24 '16 at 19:45
  • @MarkRajcok `images` are just a part of data that is used by the parent like this `{profile: {firstName: "abc", lastName: "xyz", images: [ ... ]}}` which means if I subscribe in the child, I'll still have to subscribe for the parent and i would like to avoid the repetition – Optimus Pette Jun 24 '16 at 19:49
  • If the images array in the parent component is populated when the child component is created, then the images input property should be populated before ngOnInit() is called. You would need to provide more information about how the images array is populated in the parent component for anyone to help you further (or create a minimal Plunker showing the problem). – Mark Rajcok Jun 24 '16 at 19:54
  • @MarkRajcok I have added the parent component and how images are populated in it. – Optimus Pette Jun 24 '16 at 20:10
  • 2
    So yes, it looks like your parent component is using http (since it is using a service) to populate its images property. Since this is an asynchronous operation, the child component's input property will not be populated by the time its ngOnInit() method is called. Move your code from ngOnInit() to ngOnChanges() and it should work. – Mark Rajcok Jun 24 '16 at 20:36
  • Instead of using `image[]`, you can simply use images: `BehaviorSubject` and then access it like `this.images.value` – Imran Faruqi Aug 16 '22 at 13:11

4 Answers4

144

Input properties are populated before ngOnInit() is called. However, this assumes the parent property that feeds the input property is already populated when the child component is created.

In your scenario, this is not the case – the images data is being populated asynchronously from a service (hence an http request). Therefore, the input property will not be populated when ngOnInit() is called.

To solve your problem, when the data is returned from the server, assign a new array to the parent property. Implement ngOnChanges() in the child. ngOnChanges() will be called when Angular change detection propagates the new array value down to the child.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 1
    I agree with you because I myself faced this same problem. Even after I implemented `ngOnchanges()` in the child, the error `Cannot read property '0' of undefined` comes. But, the app can carry on without problem. This error comes because initially, the input property has not been populated. It is populated by the second time call of `ngOnChanges()` when the data from async call is received. For my case, additional to this solution, I added the **guard against null** operator in the html. It clearly resolved the error. – Thilina Samiddhi Jun 15 '17 at 10:03
  • 1
    @Mark ..Nicely explained and solved my problem. – Radiant May 29 '21 at 15:38
13

You can also add a setter for your images which will be called whenever the value changes and you can set your default selected image in the setter itself:

export class ImageGalleryComponent {
  private _images: Image[];

  @Input()
  set images(value: Image[]) {
      if (value) { //null check
          this._images = value;
          this.selectedImage = value[0]; //setting default selected image
      }
  }
  get images(): Image[] {
      return this._images;
  }

  selectedImage: Image;
}
Sachin Parashar
  • 1,067
  • 2
  • 18
  • 28
3

You can resolve it by simply changing few things.

  export class ImageGalleryComponent implements OnInit, OnChanges {

  @Input() images: Image[];
  selectedImage: Image;

  ngOnChanges() {
      if(this.images) {
        this.selectedImage = this.images[0];
      }
  }

 }
Community
  • 1
  • 1
Raj Patel
  • 49
  • 4
  • 2
    Caution, you need to add `implements OnChanges` in order to be able to use `ngOnChanges()` method. – Sixteen Jun 23 '21 at 09:20
2

And as another one solution, you can simply *ngIf all template content until you get what you need from network:

...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
...

And switch flag value in your fetching method:

  getProfile() {
    let profileId = this.routeParams.get('id');
    this.profileService.getProfile(profileId).subscribe(
    profile => {
     this.profile = profile;
     this.images = profile.images;
     for (var album of profile.albums) {
       this.images = this.images.concat(album.images);
     }
     this.imagesLoaded = true; /* <--- HERE*/
    }, error => this.errorMessage = <any>error
   );
 }

In this way you will renderout child component only when parent will have all what child needs in static content. It's even more useful when you have some loaders/spinners that represent data fetching state:

...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
<loader-spinner-whatever *ngIf="!imagesLoaded" [images]="images"></loader-spinner-whatever>
...

But short answer to your questions:

  • When inputs are available?
    • In OnInit hook
  • Why are not available to your child component?
    • They are, but at this particular point in time they were not loaded
  • What can I do with this?
    • Patiently wait to render child component utul you get data in asynchronous manner OR learn child component to deal with undefined input state
Tomas
  • 3,269
  • 3
  • 29
  • 48