0

I develop project of music site using Angular. Back-end is written in Spring. I download Album array using get request from back-end, then I use ngFor in html to print all albums data. I try to call album.getArtists() function in html to get artists (bands and musicians) in string form. Unfortunately, I get following error:

AlbumsViewComponent.html:5 ERROR TypeError: _v.context.$implicit.getArtists is not a function
    at Object.eval [as updateRenderer] (AlbumsViewComponent.html:7)
    at Object.debugUpdateRenderer [as updateRenderer] (core.js:30068)
    at checkAndUpdateView (core.js:29443)
    at callViewAction (core.js:29679)
    at execEmbeddedViewsAction (core.js:29642)
    at checkAndUpdateView (core.js:29439)
    at callViewAction (core.js:29679)
    at execComponentViewsAction (core.js:29621)
    at checkAndUpdateView (core.js:29444)
    at callViewAction (core.js:29679)

Here is html file:

<h1 class="main-header">Albums</h1>
<ul class="albums-view">
  <li *ngFor="let album of albums">
    <img class="album-cover" src={{album.coverPath}}>
    <span routerLink="../album-view/{{album.id}}" class="albums-view-element-text" id="album-name">{{album.title}} </span>
    <br>
    <span class="albums-view-element-text" id="album-artist"> by {{album.getArtists()}} </span>
    <br>
    <span class="albums-view-element-text" id="album-release-date"> Release Date: {{album.getReleaseDate()}} </span>
    <br>
    <span class="albums-view-element-text" id="album-length album-length-hours-minutes-seconds" style="visibility: hidden;"> Length: {{album.duration.hours}}:{{album.duration.minutes}}:{{album.duration.seconds}} </span>
    <br>
    <span class="albums-view-element-text" id="album-length album-length-minutes-seconds" style="visibility: hidden;"> Length: {{album.duration.minutes}}:{{album.duration.seconds}} </span>
  </li>
</ul>

Here is AlbumsViewComponent class:

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

import { Album } from '../album';
import { ALBUMS } from '../mock-albums';
import { AlbumsViewService } from './albums-view.service';

import { ActivatedRoute } from '@angular/router';
import { routes } from '../app-routing.module';

@Component({
  selector: 'app-albums-view',
  templateUrl: './albums-view.component.html',
  styleUrls: ['./albums-view.component.css']
})

export class AlbumsViewComponent implements OnInit {
  public albums :Album[] = [];

  constructor(private albumsViewService: AlbumsViewService) { }

  ngOnInit() {
    this.albumsViewService.getAlbums().subscribe(albums => {
      this.albums = albums;
    });
  }
}

Here is AlbumsViewService class:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

import { Album } from '../album';

@Injectable()
export class AlbumsViewService {
  constructor(private http: HttpClient) { }

  getAlbums(): Observable<Album[]> {
    return this.http.get<Album[]>('http://localhost:8090/album/');
  }
}

Here is Album class:

import { Duration } from './duration';
import { Band } from './band';
import { Musician } from './musician';

export class Album {
    id: number;
    title: string;
    duration: Duration;
    releaseDate: Date;
    coverPath: string;
    bands: Band[];
    musicians: Musician[];

    constructor(id: number, title: string, duration: Duration, releaseDate: Date, coverPath: string, bands: Band[], musicians: Musician[]) {
        this.duration = duration;
        this.id = id;
        this.title = title;
        this.duration = duration;
        this.releaseDate = releaseDate;
        this.coverPath = coverPath;
        this.bands = bands;
        this.musicians = musicians;
    }

    public getArtists(): string {
        let artists: string[];
        let concatenatedArtists = "";
        artists = this.getBands().concat(this.getMusicians());

        for (let artist of artists) {
          concatenatedArtists += artist + ", ";
        }
        concatenatedArtists = concatenatedArtists.slice(0, -2);

        return concatenatedArtists;
  }

  public getReleaseDate(): string {
    let rd:any = this.releaseDate;
    let releaseDate = rd.split("/");
    var releaseDateString: string;
    releaseDateString = this.getMonth(+releaseDate[1]) + " " + +releaseDate[0] + ", " + +releaseDate[2];
    return releaseDateString;

  }

  public getMonth(releaseDate): string {
    switch(releaseDate) {
      case 1:
      return "Jan";
      case 2:
      return "Feb";
      case 3:
      return "Mar";
      case 4:
      return "Apr";
      case 5:
      return "May";
      case 6:
      return "Jun";
      case 7:
      return "Jul";
      case 8:
      return "Aug";
      case 9:
      return "Sep";
      case 10:
      return "Oct";
      case 11:
      return "Nov";
      case 12:
      return "Dec";
      default:
      throw new Error("wrong month number");
    }
  }

  public getBands(): string[] {
    var bands: string[] = [];
    if (this.bands == null) {
      return bands;
    }

    for (let band of this.bands) {
      bands.push(band.name);
    }

    return bands;
  }

  public getMusicians(): string[] {
    var musicians: string[] = [];
    if (this.musicians == null) {
      return musicians;
    }

    for (let musician of this.musicians) {
      musicians.push(musician.name + " " + musician.surname);
    }

    return musicians;
  }
}
BlackHawk
  • 1
  • 3

1 Answers1

1

The issue is the way you get data from backend. The exact issue being service:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

import { Album } from '../album';

@Injectable()
export class AlbumsViewService {
  constructor(private http: HttpClient) { }

  getAlbums(): Observable<Album[]> {
    return this.http.get<Album[]>('http://localhost:8090/album/');
  }
}
return this.http.get<Album[]>('http://localhost:8090/album/');

This line won't magically transform data coming from the service to proper instances with methods, it will assign a type definition only, so you really have only a simple JS object. Your actual class doesn't have a date,but a date string, band array is also an array of plain objects, same as musicians.

So you could a static method to construct a proper Artist instance from any (which is what backend brings back).

import { Duration } from './duration';
import { Band } from './band';
import { Musician } from './musician';

export class Album {
    constructor(
        public id: number,
        public title: string,
        public duration: Duration,
        public releaseDate: Date,
        public coverPath: string,
        public bands: Band[],
        public musicians: Musician[]
    ) {}

    public static fromAny(a: any): Album {
        //    Obviously other types have to be properly constructed as well
        //  - Duration
        //  - Band[]
        //  - Musician[]
        return new Album(a.id, a.title, a.duration, new Date(a.releaseDate), a.coverPath, a.bands, a.musicians);
    }

    public getArtists(): string {
        let artists: string[];
        let concatenatedArtists = '';
        artists = this.getBands().concat(this.getMusicians());

        for (let artist of artists) {
            concatenatedArtists += artist + ', ';
        }
        concatenatedArtists = concatenatedArtists.slice(0, -2);

        return concatenatedArtists;
    }

    // This method will be also more elegant because
    // you don't have to operate on string, but on the actual Date instance
    // You could use a build in method to display date
    // https://stackoverflow.com/questions/3552461/how-to-format-a-javascript-date 
    public getReleaseDate(): string {
        let rd: any = this.releaseDate;
        let releaseDate = rd.split('/');
        var releaseDateString: string;
        releaseDateString = this.getMonth(+releaseDate[1]) + ' ' + +releaseDate[0] + ', ' + +releaseDate[2];
        return releaseDateString;
    }

    public getMonth(releaseDate): string {
        switch (releaseDate) {
            case 1:
                return 'Jan';
            case 2:
                return 'Feb';
            case 3:
                return 'Mar';
            case 4:
                return 'Apr';
            case 5:
                return 'May';
            case 6:
                return 'Jun';
            case 7:
                return 'Jul';
            case 8:
                return 'Aug';
            case 9:
                return 'Sep';
            case 10:
                return 'Oct';
            case 11:
                return 'Nov';
            case 12:
                return 'Dec';
            default:
                throw new Error('wrong month number');
        }
    }

    public getBands(): string[] {
        return (this.bands || []).map(band => band.name);
    }

    public getMusicians(): string[] {
        return (this.musicians || []).map(({ name, surname }) => `${name} ${surname}`);
    }
}

And then change service so that calls that method:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import { Album } from '../album';

@Injectable()
export class AlbumsViewService {
    constructor(private http: HttpClient) {}

    getAlbums(): Observable<Album[]> {
        return this.http.get<any[]>('http://localhost:8090/album/').pipe(map(albums => albums.map(Album.fromAny)));
    }
}

I also would encourage to write a test to make sure that your service properly transforms plain JS objects to actual class instances.

Evaldas Buinauskas
  • 13,739
  • 11
  • 55
  • 107