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

Método do template sendo chamado antes de dados serem carregados

Olá, estou com uma dúvida a respeito da plataforma. Estou a desenvolver um sistema usando o que foi aprendido nos cursos de Ionic e encontrei um problema.

Estou tentando usar de dados externos (API), tanto para conseguir os produtos que quero mostrar, quanto as categorias que são mostradas em um Segment.

Os produtos são mostrados tranquilamente, e as categorias também, mas o Segment da página não funciona, deixando a navegação inacessível. Quando insiro manualmente os dados no array de categorias tudo funciona normalmente, mas quando puxo da API a navegação é quebrada.

Página de produtos:

import {Component} from '@angular/core';
import {IonicPage, LoadingController, Modal, ModalController} from 'ionic-angular';
import {Product} from "../../models/product";
import {Category} from "../../models/category";
import {ProductDetailsPage} from "../product-details/product-details";
import {ProductsServiceProvider} from "../../providers/products-service/products-service";
import {CategoriesServiceProvider} from "../../providers/categories-service/categories-service";

@IonicPage()
@Component({
  selector: 'page-products',
  templateUrl: 'products.html',
})
export class ProductsPage {
  category: string = 'Vips';
  productList: Array<Product> = [];
  categoryList: Array<Category> = [];

  constructor(private _modal: ModalController, private _loadingCtrl: LoadingController,
              private _productsService: ProductsServiceProvider, private _categoryService: CategoriesServiceProvider) {
  }


  ionViewDidLoad() {
    this._categoryService.list().subscribe((categories) => {
      console.log('categorias carregadas!');
      this.categoryList = categories;
    }, error => console.log(error));

    let loading = this._loadingCtrl.create({content: 'Carregando produtos...'});
    loading.present();
    this._productsService.list().subscribe((products) => {
      products.map(product => {
        product.category = this.categoryList.find(category =>
          category.id === product.category_id);
        product.features = product.features.split(',')
      });
      this.productList = products;
      console.log(this.productList);
      loading.dismiss();
    }, error => console.log(error));
  }

  showDetails(product: Product) {
    let myModal: Modal = this._modal.create(ProductDetailsPage.name, product);
    myModal.present();
  }

  getProducts(category: Category) {
    return this.productList.filter(product => product.category === category);
  }

}

Provider das categorias:

import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Category} from "../../models/category";

@Injectable()
export class CategoriesServiceProvider {

  constructor(private _http: HttpClient) {
  }

  list() {
    return this._http.get<Category[]>('http://localhost:3000/categories');
  }

}

Interface da Categoria:

export interface Category {
  id : number;
  name: string;
  description: string;
}

Template HTML:

https://gist.github.com/IanPedroV/8bbb0e271044573d33ad03b0976d8ddf

Algo estranho que notei, é que a página, é chamada diversas vezes antes mesmo do subscribe das categorias ser chamado, ou seja, a categoryList é chamada os ngFors do html várias vezes, e antes, da lista de categorias ser preenchida. Comportamento descrito: https://imgur.com/a/UUy92Pi

Caso o código do da Página de produtos seja este abaixo, tudo funciona como esperado:

https://gist.github.com/IanPedroV/9eaf8751be88ef9a5b4a5119db390d9a

Desculpem pelo post gigante, só quis dar o máximo de informações possíveis, já tentei de tudo para resolver e nada. Detalhe: o JSON retornado pela API é idêntico ao que insiro manualmente. Talvez seja algum comportamento estranho do Segment mesmo... O carregamento dessa página é feito por LazyLoading e ela é mostrada dentro de uma TabsPage.

11 respostas

Boa tarde, Ian! Como vai?

Esse problema está acontecendo pq o JavaScript é assíncrono! Como vc está fazendo duas requisições ao mesmo tempo quando a requisição dos produtos termina não há garantia que a requisição das categorias já tenha finalizado! E veja que mesmo sem essa garantia vc utiliza a lista de categorias dentro do subscribe() da requisição de produtos!

Para vc resolver esse problema, vc precisa compor os Observables, afinal de contas, vc só pode fazer a requisição de produtos depois que a requisição de categorias já tiver sido finalizada! Para isso, utilize o mergeMap() como mostrado durante o curso!

Inclusive, eu falei sobre isso nesse outro tópico. Dá uma olhada lá e veja se te ajuda!

Grande abraço e bons estudos, meu aluno!

Irei verificar neste momento, já agradeço de antemão pela ajuda.

Se o mergeMap irá retornar apenas um array de Observables flatten, como posso extrair os valores para o meu array de produtos e para o array de categorias?

No código abaixo, anyValue retorna um array com os 7 produtos, mas onde foram parar as categorias?

    this._categoryService.list().mergeMap(() =>
      this._productsService.list()
    ).subscribe(anyValue => console.log(anyValue));
  }

Outra, com o código abaixo consigo atribuir ao array de produtos seus valores, mas como faço pra recuperar os valores para o array de categorias?

    this._categoryService.list().mergeMap(() =>
      this._productsService.list()
    ).subscribe(anyValue => productList = anyValue);
  }

Também tentei desta forma:

  ionViewDidLoad() {
    Observable.forkJoin([this._categoryService.list(), this._productsService.list()]).subscribe(results => {
      this.categoryList = results[0];
      this.productList = results[1];
      console.log(this.categoryList);
      console.log(this.productList)
    });
  }

Tudo no console está correto, mas nada é mostrado no app. http://prntscr.com/jotop6

Outro detalhe: mesmo que eu carregue apenas as categorias através do service, o componente do segment continua bugado, sem poder realizar os clicks.

Boa noite, Ian! Como vai?

Logo na aula seguinte que eu linkei no outro tópico eu mostro que o mergeMap() recebe como parâmetro os dados do Observable de entrada. Então, no seu caso ficaria algo assim:

this._categoryService.list()
     .mergeMap((categories) => {
          // fazer qualquer coisa com a lista de categorias.

          return this._productsService.list();
     })
     .subscribe(products => {
          // fazer qualquer coisa com a lista de produtos.
     });

Pegou a ideia? Qualquer coisa é só falar!

Grande abraço e bons estudos!

  ionViewDidLoad() {
    let loading = this._loadingCtrl.create({content: 'Carregando produtos...'});
    loading.present();
    this._categoryService.list()
      .mergeMap((categories) => {
        this.categoryList = categories;
        return this._productsService.list();
      }).subscribe(products => {
      loading.dismiss();
      this.productList = products;
      this.assignCatetories(this.productList);
      console.log(this.productList);
      console.log(this.categoryList);
    });
  }

Tentei desta forma, mesmo comportamento ao usar o forkJoin, tudo correto no console, mas os segments ficam não navegáveis...

Ian, cole aqui o template onde vc cria os segments e os dados de cada um deles pra eu poder dar uma olhada.

<ion-header>
  <ion-navbar color="warning">
    <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <div class="titleicon">
      <img src="https://gallery.mailchimp.com/cc1505233db775a46edcf7259/images/a0fd24ee-e4ff-4916-92f2-f675891f21f3.png"
           width="100px"/>
    </div>
  </ion-navbar>

  <ion-toolbar>
    <ion-segment *ngIf="categoryList" [(ngModel)]="category" color="warning">
      <ion-segment-button *ngFor="let category of categoryList" value="{{category.name}}">
        {{category.name}}
      </ion-segment-button>
    </ion-segment>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div [ngSwitch]="category">
    <ng-container *ngFor="let category of categoryList">
      <ion-list *ngSwitchCase="category.name">
        <ion-item *ngFor="let product of getProducts(category)" (click)="showDetails(product)">
          <ion-thumbnail item-start><img src={{product.image}}></ion-thumbnail>
          <h2>{{product.name}}</h2>
          <p>{{product.description}}</p>
          <button ion-button clear item-end color="warning">Ver</button>
        </ion-item>
      </ion-list>
    </ng-container>
  </div>
</ion-content>
solução!

Boa tarde, Ian! Me perdoe pela demora na resposta! Eu resolvi o problema dessa forma:

@IonicPage()
@Component({
  selector: 'page-exemplo-segment',
  templateUrl: 'exemplo-segment.html',
})
export class ExemploSegmentPage {

  pet;
  categorias;
  animais;

  constructor(public navCtrl: NavController, public navParams: NavParams) {
  }

  ionViewDidLoad() {
    console.log('ionViewDidLoad ExemploSegmentPage');
    this.getCategorias()
        .mergeMap(categorias => {
          this.categorias = categorias;
          console.log(this.categorias);
          return this.getAnimais();
        })
        .subscribe(animais => {
          this.animais = animais;
          this.pet = 'puppies';
          console.log(this.animais);
        });
  }

  listaAnimaisPorCategoria(categoria) {
    let retorno = this.animais.filter(animal => animal.categoria === categoria);
    return retorno;
  }

  // métodos para simular a requisição à API
  private getCategorias() {
    return new Observable(observer => {
      setTimeout(() => {
        observer.next(['puppies', 'kittens']);
        observer.complete();
      }, 2000);
    });
  }

  private getAnimais() {
    return new Observable(observer => {
      setTimeout(() => {
        observer.next([
          {categoria: 'puppies', nome: 'Ruby', foto: 'https://ionicframework.com/dist/preview-app/www/assets/img/thumbnail-puppy-1.jpg'},
          {categoria: 'kittens', nome: 'Luna', foto: 'https://ionicframework.com/dist/preview-app/www/assets/img/thumbnail-kitten-1.jpg'}
        ]);
        observer.complete();
      }, 2000);
    });
  }
}
<ion-header>

  <ion-navbar>
    <ion-title>Exemplo Segment</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>
    <div padding>
      <ion-segment *ngIf="categorias" [(ngModel)]="pet">
        <ion-segment-button *ngFor="let categoria of categorias" [value]="categoria">
          {{categoria}}
        </ion-segment-button>
      </ion-segment>
    </div>

    <div [ngSwitch]="pet">
      <ng-container *ngFor="let categoria of categorias">
        <ion-list *ngSwitchCase="categoria">
          <ion-item *ngFor="let animal of listaAnimaisPorCategoria(categoria)">
            <ion-thumbnail item-start>
              <img [src]="animal.foto">
            </ion-thumbnail>
            <h2>{{animal.nome}}</h2>
          </ion-item>
        </ion-list>
      </ng-container>
    </div>
</ion-content>

A principal diferença entre o meu código e o seu é que eu somente defini o valor do atributo pet que é o responsável por resolver o ngSwitch após a conclusão das requisições. Fora isso eu usei a boa prática de utilizar o property bind em vez de Angular Expression em alguns casos no template da página.

Dá uma olhada aí e veja se resolve o seu problema. Se não resolver, cole aqui como ficou o seu código da classe e do template em questão.

Grande abraço e bons estudos, meu aluno!

Nossa professor, peço desculpas pela demora. A notificação por e-mail caiu no spam e como eu não estava em casa demorei 3 semanas para ver a resposta, irei testar e retorno. Sorry.