Solucionado (ver solução)
Solucionado
(ver solução)
12
respostas

ngFor em array dentro de outro array com links da API(Angular)

Eu estou usando a API publica swapi para listar filmes do star wars em uma lista e dentro de cada filme quero listar os personagens, a resposta em json da api é essa:

{
"title": "A New Hope",
"episode_id": 4,
"opening_crawl": "It is a period of civil war.\r\nRebel spaceships, striking\r\nfrom a hidden base, have won\r\ntheir first victory against\r\nthe evil Galactic Empire.\r\n\r\nDuring the battle, Rebel\r\nspies managed to steal secret\r\nplans to the Empire's\r\nultimate weapon, the DEATH\r\nSTAR, an armored space\r\nstation with enough power\r\nto destroy an entire planet.\r\n\r\nPursued by the Empire's\r\nsinister agents, Princess\r\nLeia races home aboard her\r\nstarship, custodian of the\r\nstolen plans that can save her\r\npeople and restore\r\nfreedom to the galaxy....",
"director": "George Lucas",
"producer": "Gary Kurtz, Rick McCallum",
"release_date": "1977-05-25",
"characters": [
    "https://swapi.co/api/people/1/",
    "https://swapi.co/api/people/2/",
    "https://swapi.co/api/people/3/",
    "https://swapi.co/api/people/4/",
    "https://swapi.co/api/people/5/",
    "https://swapi.co/api/people/6/",
    "https://swapi.co/api/people/7/",
    "https://swapi.co/api/people/8/",
    "https://swapi.co/api/people/9/",
    "https://swapi.co/api/people/10/",
    "https://swapi.co/api/people/12/",
    "https://swapi.co/api/people/13/",
    "https://swapi.co/api/people/14/",
    "https://swapi.co/api/people/15/",
    "https://swapi.co/api/people/16/",
    "https://swapi.co/api/people/18/",
    "https://swapi.co/api/people/19/",
    "https://swapi.co/api/people/81/"
],
"planets": [
    "https://swapi.co/api/planets/2/",
    "https://swapi.co/api/planets/3/",
    "https://swapi.co/api/planets/1/"
],
"starships": [
    "https://swapi.co/api/starships/2/",
    "https://swapi.co/api/starships/3/",
    "https://swapi.co/api/starships/5/",
    "https://swapi.co/api/starships/9/",
    "https://swapi.co/api/starships/10/",
    "https://swapi.co/api/starships/11/",
    "https://swapi.co/api/starships/12/",
    "https://swapi.co/api/starships/13/"
],
"vehicles": [
    "https://swapi.co/api/vehicles/4/",
    "https://swapi.co/api/vehicles/6/",
    "https://swapi.co/api/vehicles/7/",
    "https://swapi.co/api/vehicles/8/"
],
"species": [
    "https://swapi.co/api/species/5/",
    "https://swapi.co/api/species/3/",
    "https://swapi.co/api/species/2/",
    "https://swapi.co/api/species/1/",
    "https://swapi.co/api/species/4/"
],
"created": "2014-12-10T14:23:31.880000Z",
"edited": "2015-04-11T09:46:52.774897Z",
"url": "https://swapi.co/api/films/1/"
}

O array com os personagens da links para /people que é onde ficam os personagens na api, o ngFor que eu fiz é esse:

  <li *ngFor="let filme of filmes$">
      <p>{{ filme.title }} , {{filme.episode_id}}</p>
      <p>{{ filme.opening_crawl }}</p>
      <p *ngFor= "let character of filme.characters">{{character}}</p>
  </li>

mas ele lista os links, queria saber como é possível eu listar os personagens nesse caso, o código para buscar os filmes é:

   getFilmes(): Observable<Filme[]> {
      return this.http.get<Filme[]>(`${this.url}/films`)
        .pipe(
          map((resposta: any) => resposta.json())
        )
  }
12 respostas

Fala aí Vinicius, tudo bem? Bom, sua lista de personagens é um link para uma chamada API, com isso, você pode resolver esse problema de algumas maneiras, a que eu tentaria seria:

Assim que você buscar todos os filmes, você já faz um map no seu array de personagens e busca as informações de cada um deles.

<p *ngFor= "let character of filme.characters">{{character.name}}</p>

Ou, você pode fazer essa busca na hora de executar seu for:

<p *ngFor= "let character of buscarPersonagens(filme.characters)">{{character.name}}</p>

Espero ter ajudado.

Fala Matheus, tudo bem e você? Eu não entendi muito bem, eu teria que realizar um map com um outro GET? seria isso?

Opa, Vinicius! Na realidade seriam outras requisições GET! Afinal de contas temos vários links para personagens diferentes!

Então, veja, além da demora na resposta da requisição inicial ( listagem de filmes ) o usuário teria que esperar por N requisições dos personagens de cada um dos filmes! Tudo isso pq o usuário só queria ver a lista de filmes! Imagina um usuário mobile com uma rede lenta! Ele certamente não ficaria nada feliz! Um outro problema importante nessa situação é que vc passaria a lidar com diversos observables ao mesmo tempo! E isso iria aumentar a complexidade do seu código!

Portanto, essa abordagem não seria a ideal!

O que eu faria seria o seguinte:

No momento em que o usuário clicar num filme para obter o detalhamento dele, então aí sim faríamos a requisição para os personagens! Pq nesse caso estaríamos buscando apenas os N personagens do filme selecionado! O que diminuirá o tempo esperado pelo usuário!

Daí, pra trabalhar com os vários observables resultantes de cada uma das requisições que vc fizer, vc pode tentar utilizar o switchMap() que o mestre Flávio apresenta durante o curso!

O que vc acha da ideia? Faz sentido pra vc?

Qualquer coisa é só falar!

Grande abraço e bons estudos, meu aluno!

Olá Gabriel, na verdade eu tinha acabado de pensar nisso e coloquei a listagem de personagens no componente de detalhes do filme, mas só que não consegui trabalhar com o switchMap() nesse caso

<div class="jumbotron">
    <div *ngIf="filme">

        <h1 class="display-4">{{filme.title}}</h1>
        <p class="lead">{{filme.opening_crawl}}</p>
        <hr class="my-4">
        <p><b>Director: </b>{{filme.director}}</p>
        <p><b>Producers: </b>{{filme.producer}}</p>
        <p><b>Release Date: </b>{{filme.release_date}}</p>

        <div class="row">

            <div class="col-sm-3">
                <h6>Characters: </h6>
                <div *ngFor= "let character of filme.characters">
                        <p id="char">{{character.name}} </p>
                </div>
            </div>

            <div class="col-sm-3">
                <h6>Starships: </h6>
                <div *ngFor= "let starships of filme.starships">
                        <p id="char">{{starships}} </p>
                </div>
            </div>

            <div class="col-sm-3">
                <h6>Planets: </h6>
                <div *ngFor= "let planets of filme.planets">
                        <p id="char">{{planets}} </p>
                </div>
            </div>            
        </div>
     </div>
</div>

no caso o código está assim agora e a requisição está assim:

   getFilmesById(): void {
      const id = +this.route.snapshot.paramMap.get('id');

      this.filmesService.getFilmesById(id)
        .subscribe(filme => this.filme = filme)
    }

Mas ainda estou com dificuldades de encontrar um jeito de entrar no array de personagens

O componente:

export class FilmeDetailComponent implements OnInit {

  @Input() filme: Filme;
  @Input() character: Character;
  @Input() characters: Character[];

  constructor(
    private route: ActivatedRoute,
    private filmesService: FilmesService,
    ) { }

    ngOnInit(): void {

      this.getFilmesById();
      this.getCharactersById();
    }

    getFilmesById(): void {
      const id = +this.route.snapshot.paramMap.get('id');

      this.filmesService.getFilmesById(id)
        .subscribe(filme => this.filme = filme)
    }

    getCharactersById(): void {
      const id = +this.route.snapshot.paramMap.get('id');
      this.filmesService.getCharactersById(id)
        .subscribe(character => this.character = character);
    }
}

Fala aí Vinicius, tudo bem? Consegue postar o código do service?

Fico no aguardo.

E ai Matheus, esse é o código do service:

import { Character } from './shared/character.model';
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Filme } from './shared/filme.model';
import { Observable, forkJoin, of } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

@Injectable({
  providedIn: 'root'
})
export class FilmesService {

  constructor(private http: HttpClient) {}

  url = 'https://swapi.co/api'

  //////// Get methods //////////

  getFilmes(): Observable<Filme[]> {
      return this.http.get<Filme[]>(`${this.url}/films`)
  }

  getCharacters(): Observable<Character[]> {
      return this.http.get<Character[]>(`${this.url}/people`)
  }

  getCharactersById(id: number): Observable<Character> {
      return this.http.get<Character>(`${this.url}/people/${id}`)
  }

  getFilmesById(id: number): Observable<Filme> {
    return this.http.get<Filme>(`${this.url}/films/${id}`)
  }  

  getCharactersByUrl(charUrl: any): Observable<Character[]> {
    return this.http.get<Character[]>(charUrl)
  }

  searchFilme(term: string): Observable<Filme[]> {
    return this.http.get<Filme[]>(`${this.url}/films/?title_like=${term}`)
  }


   //////// Save methods //////////

  postFilme (filme: Filme): Observable<Filme> {
    return this.http.post<Filme>(this.url, filme, httpOptions)
  }

  updateFilme (filme: Filme): Observable<any> {
    return this.http.put(this.url, filme, httpOptions)
  }

  deleteFilme (filme: Filme | number): Observable<Filme> {
    const id = typeof filme === 'number' ? filme : filme.id;
    const url = `${this.url}/films/ ${id}`;

    return this.http.delete<Filme>(url, httpOptions)
  }

}
solução!

Vinicius, fiz algumas alterações no código para que atendesse sua necessidade:

getFilmesById(id: number): Observable<any> {
    return this.http.get<any>(`${this.url}/films/${id}`)
      .pipe(switchMap(movie => new Observable(observer => {
        forkJoin(movie.characters.map((character: string) => this.http.get(character)))
          .subscribe((characters: any) => {
            movie.characters = characters;
            observer.next(movie);
            observer.complete();
          })
      })));
}

Dessa maneira, ao buscar um filme, eu fiz uma interação em todos os characters e busquei os dados deles.

Espero ter ajudado.

Fala Matheus, deu certo, está funcionando certinho agora, mas eu não entendi muito bem o código, poderia me explicar?

Fala aí Vinicius, fico feliz que tenha dado certo, bom, vamos lá, vou tentar explicar o código:

getFilmesById(id: number): Observable<any> {
    return this.http.get<any>(`${this.url}/films/${id}`)
      .pipe(switchMap(movie => new Observable(observer => {
        forkJoin(movie.characters.map((character: string) => this.http.get(character)))
          .subscribe((characters: any) => {
            movie.characters = characters;
            observer.next(movie);
            observer.complete();
          })
      })));
}

Irei tentar explicar por pedaços os códigos:

this.http.get<any>(`${this.url}/films/${id}`)

Aqui nada de mais, simplesmente estou fazendo uma requisição do verbo (tipo) GET para a URL: ${this.url}/films/${id} que poderá retornar qualquer coisa any.

this.http.get<any>(`${this.url}/films/${id}`)
    .pipe()

O pipe é uma função dos Observable's para realizar composições de operadores da RxJS.

this.http.get<any>(`${this.url}/films/${id}`)
    .pipe(switchMap(movie => ...)

Aqui estou usando o switchMap que é um operador da RxJS onde recebe um o valor do retorno de um Observable como parâmetro e devolve um novo (juntando todas as entradas em um único Observable de saída).

getFilmesById(id: number): Observable<any> {
    return this.http.get<any>(`${this.url}/films/${id}`)
      .pipe(switchMap(movie => new Observable(observer => {})));
}

Aqui estou criando um novo Observable que somente será finalizado, ou seja, somente será possível acessar o .subscribe dele quando realizar o observer.next e observer.done (isso foi necessário pois precisamos fazer várias requisições assíncronas para buscar as informações dos personagens).

E ele só pode ser resolvido quando todas forem finalizadas.

getFilmesById(id: number): Observable<any> {
    return this.http.get<any>(`${this.url}/films/${id}`)
      .pipe(switchMap(movie => new Observable(observer => {
        forkJoin()
      })));
}

O forkJoin vai resolver todos os Observables que forem passados para ele, similar ao Promise.all.

getFilmesById(id: number): Observable<any> {
    return this.http.get<any>(`${this.url}/films/${id}`)
      .pipe(switchMap(movie => new Observable(observer => {
        forkJoin(movie.characters.map((character: string) => this.http.get(character)))
          .subscribe((characters: any) => {})
      })));
}

Veja que dentro do forkJoin estou fazendo uma interação em todos os characters utilizando o map e para cada personagem faço uma nova requisição GET buscando as informações dele.

O problema é que isso irá retornar um array de Observable, então, precisamos resolver todos eles, ou seja, precisamos transformar esse array de Observable para um array de personagem, por isso utilizamos o forkJoin.

Após o forkJoin resolver todos eles, chamamos o subscribe dele e pegamos esse array de personagens.

getFilmesById(id: number): Observable<any> {
    return this.http.get<any>(`${this.url}/films/${id}`)
      .pipe(switchMap(movie => new Observable(observer => {
        forkJoin(movie.characters.map((character: string) => this.http.get(character)))
          .subscribe((characters: any) => {
            movie.characters = characters;
            observer.next(movie);
            observer.complete();
          })
      })));
}

Por fim, pegamos esse array de personagens e atualizamos o array retornado pela API de filmes, antes era um array de String, mas, agora é um array de personagens (exatamente como você queria).

Por fim, realizamos o observer.next e done para que tudo isso chegue no .subscribe de quem chamar a função getFIlmesById.

Repare que estamos passando o movie para o next, esse movie é nosso filme com o array de characters atualizado.

Espero ter ajudado.

Matheus, muito obrigado pela explicação! Eu entendi e vou tentar replicar o conceito pra buscar outras informações de cada filme pra tentar fixar bem como fazer isso, porque não sabia que seria tão complexo, mas enfim muito obrigado pela ajuda =D

Magina, sempre que precisar não deixe de criar suas dúvida.

Abraços e bons estudos.