Reputation: 33
Issue
I'm having an issue binding JSON data to an Angular Material data table.
Background
I'm developing a small application which is designed to provide users with film information. An API call is initiated following the user triggering a search. This should then populate the data table with the response data.
Whilst I can successfully make the API call, no data is passed into the data table:
Furthermore, no errors are shown in the console and I can populate the table with test data.
Here is the code:
api-calls.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
@Injectable()
export class ApiService {
constructor(private http:HttpClient){}
public getFilms(searchTerm): Observable<any> {
const apiUrl = 'http://www.omdbapi.com/?apikey=b1464edd&s=';
const fullLink = apiUrl + searchTerm
return this.http.get(fullLink)
}}
app.component.ts
import { Component, OnInit } from '@angular/core';
import { Films } from './models/films.model';
import { ApiService } from './services/api-calls.service';
import { MatTableDataSource } from '@angular/material';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{
constructor(private apiService:ApiService) {}
displayedColumns: string[] = ['title', 'year', 'imdbID', 'poster', 'type']
dataSource: MatTableDataSource<any[]>;
searchTerm = '';
handleSearch(event) {
if(event.action === 'SEARCH') {
this.searchTerm = event.query
this.apiService.getFilms(this.searchTerm).subscribe(
data => this.dataSource = new MatTableDataSource<Films[]>(data)
)}
}
ngOnInit() {
}
}
app.component.html (the search function outlined in the container class is handled in another component)
<div class = "container">
<mat-card>
<h2>Lightweight IMDb Search Engine</h2>
<app-search (searchEvent)="handleSearch($event)"></app-search>
</mat-card>
<div>
<mat-table [dataSource] = "dataSource">
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef> Title </mat-header-cell>
<mat-cell *matCellDef="let films"> {{ films.title }}</mat-cell>
</ng-container>
<ng-container matColumnDef="year">
<mat-header-cell *matHeaderCellDef> Year </mat-header-cell>
<mat-cell *matCellDef="let films"> {{ films.year }}</mat-cell>
</ng-container>
<ng-container matColumnDef="imdbID">
<mat-header-cell *matHeaderCellDef> imdbID </mat-header-cell>
<mat-cell *matCellDef="let films"> {{ films.imdbID }}</mat-cell>
</ng-container>
<ng-container matColumnDef="poster">
<mat-header-cell *matHeaderCellDef> Poster </mat-header-cell>
<mat-cell *matCellDef="let films"> {{ films.poster }}</mat-cell>
</ng-container>
<ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef> Type </mat-header-cell>
<mat-cell *matCellDef="let films"> {{ films.type }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
</div>
</div>
film.model.ts
export interface Films {
title: string;
year: string;
imdbID: string;
poster: string;
type: string;
}
Upvotes: 3
Views: 1123
Reputation: 31803
The JSON returned by the API has the following shape
{
"Search": [
{"Title":"Hello, My Name Is Doris","Year":"2015","imdbID":"tt3766394","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTg0NTM3MTI1MF5BMl5BanBnXkFtZTgwMTAzNTAzNzE@._V1_SX300.jpg"},
// etc.
]
}
Therefore you need to make some adjustments.
Firstly, you need to project the response, extracting the Search
property which contains the array of films. This should be done in your service (note the improved use of types)
api-calls.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import Film from './models/film.model';
@Injectable()
export class ApiService {
constructor(private http:HttpClient){}
getFilms(searchTerm): Observable<Film[]> {
const apiUrl = 'http://www.omdbapi.com/?apikey=b1464edd&s=';
const fullLink = apiUrl + searchTerm;
type Response = { Search: Film[] };
return this.http.get<Response> (fullLink)
.pipe(map(response => response.Search));
}
}
Then we need to declare the property names in the model interface to correctly describe the shape of the films in the response
film.model.ts
export default interface Film {
Title: string;
Year: string;
imdbID: string;
Poster: string;
Type: string;
}
Now let's adjust the component itself to improve the types a bit
app.component.ts
import { Component } from '@angular/core';
import { ApiService } from './services/api-calls.service';
import { MatTableDataSource } from '@angular/material';
import Film from './models/film.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(private apiService:ApiService) {}
displayedColumns: (keyof Film)[] = ['Title', 'Year', 'imdbID', 'Poster', 'Type'];
dataSource?: MatTableDataSource<Film[]>;
searchTerm = '';
handleSearch({ action, query }) {
if (action === 'SEARCH' && query) {
this.searchTerm = query;
this.apiService.getFilms(this.searchTerm)
.subscribe(films => this.dataSource = new MatTableDataSource(films));
}
}
}
Note how the use of types has been improved to constrain the names of the columns, that they stay in sync between the component and the service. Also note that redundant type information has been improved to take advantage of the type inference flowing from the improved type signature of the service method.
Upvotes: 1