Front End

1 mar, 2019

Aplicação Angular no estilo Vue.js

Publicidade

O desafio é construir uma pequena aplicação utilizando Angular 7, porém, utilizando o estilo e a estrutura de diretórios sugeridos pelo Vue.js. Vamos ver como o Angular se comporta.

Recentemente tenho visto bastante coisa sobre Vue.js, e algumas grandes empresas estão utilizando essa biblioteca. Tenho que admitir que isso é muito legal – é difícil uma empresa adotar uma ferramenta que é a terceira na lista de top front-end frameworks, onde é dominado por Angular e React. Sem entrar no mérito de cada ferramenta, vamos ao que interessa.

Neste artigo criaremos uma pequena aplicação web com um campo de busca, uma lista de itens e um filtro por uma determinada propriedade. É bem simples a ideia, mas adiciona alguns componentes triviais no desenvolvimento web.

Neste exemplo vamos usar a API aberta do PokemonTCG, entretanto, para o app Vue.js vamos utilizar a versão JavaScript, e para o Angular, a versão TypeScript.

Links:

Aqui está a interface das duas aplicações e a estrutura de diretórios:

VueJS

Angular

Componentes

Neste exemplo eu utilizei o mesmo tipo de estrutura proposta pelo Vue.js para criar a aplicação em Angular. É possível notar que estou utilizando apenas um arquivo ts: cards.component.ts com template e estilo inline.

Dessa forma conseguimos manter tudo em um arquivo (este link mostra como utilizar o Angular-Cli para gerar sua aplicação), de acordo com a proposta do Vue.js e seus arquivos .vue.

Como podemos observar em Cards.vue, nesse arquivo nós temos o HTML, o CSS e o JavaScript em um único lugar.

Angular: cards.component.ts

import { Component, OnInit } from '@angular/core';
import { PokemonTCG } from 'pokemon-tcg-sdk-typescript';


@Component({
  selector: 'Cards',
  template: `
    <div class="container">
    <h1>Cards</h1>
    
    <form #cardsForm="ngForm" class="form-inline">
      <input class="form-control mb-2 mr-sm-2" type="text"
        placeholder="pokemon name, ex: Charizard, Pikachu" [(ngModel)]="name" name="name"
      />
      <button (click)="searchByName(name)" class="btn btn-primary mb-2">Search</button>
      <input
        class="form-control ml-3 mb-2 mr-sm-2"
        type="text"
        placeholder="filter for HP" [(ngModel)]="searchText" name="searchText"
      />
    </form>

    <p *ngIf="searchText">You filtered for:  HP</p>
    <p *ngIf="cardList?.length" class="mt-3">We found:  cards.</p>
    <ul class="mt-5 list-unstyled row">
      <li class="media col-sm-3 " *ngFor="let item of cardList | cardsFilterBy: searchText">
        <div class="media-body">
          <img src="" alt="Generic placeholder image" />
          <p class="mt-1 mb-3">
            <a (click)="handleGetDetail(item.id);"> HP: </a>
          </p>
        </div>
      </li>
    </ul>
  </div>
  `,
  styles: ['ul { list-style-type: nonet; padding: 0;} .media img { width: 150px;}']
})
export class CardsComponent implements OnInit {
  name: string;
  searchText: string;
  cardList: any[];

  constructor() { }

  handleGetDetail(i: string) {
    PokemonTCG.Card.find(i)
    .then(result => {
      alert(JSON.stringify(result));
    })
    .catch(error => {
      alert(JSON.stringify(error));
    });
  }

  searchByName(name: string) {
    let params: PokemonTCG.IQuery[] = [{ name: 'name', value: name }];
    PokemonTCG.Card.where(params)
    .then(cards => {
      this.cardList = cards;
    })
    .catch(error => {
      alert(JSON.stringify(error));
    });
  }

  getCards() {
    let params: PokemonTCG.IQuery[] = [{ name: 'name', value: 'Blastoise' }];
    PokemonTCG.Card.where(params)
    .then(cards => {
      this.cardList = cards;
    })
    .catch(error => {
      alert(JSON.stringify(error));
    });
  }

  ngOnInit() {
    this.getCards();
  }

}

VueJS: Cards.vue

<template>
  <div class="container">
    <h1>Cards</h1>
    <form class="form-inline">
      <input
        class="form-control mb-2 mr-sm-2"
        type="text"
        placeholder="pokemon name, ex: Charizard, Pikachu"
        v-model="name"
      />
      <button @click="searchByName" class="btn btn-primary mb-2">Search</button>
      <input
        class="form-control ml-3 mb-2 mr-sm-2"
        type="text"
        placeholder="filter for HP"
        v-model="searchText"
      />
    </form>
    <p v-if="searchText">You filtered for:  HP</p>
    <p class="mt-3">We found:  cards.</p>
    <ul class="mt-5 list-unstyled row">
      <li
        class="media col-sm-3 "
        v-for="item in filteredList(cardList)"
        :key="item.id"
      >
        <div class="media-body">
          <img :src="item.imageUrl" alt="Generic placeholder image" />
          <p class="mt-1 mb-3">
            <a @click="handleGetDetail(item.id);"
              > HP: </a
            >
          </p>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
import Pokemon from "pokemontcgsdk";

export default {
  name: "cards",
  components: {},
  data() {
    return {
      name: "",
      searchText: "",
      cardList: []
    };
  },
  created: function() {
    this.getCards();
  },
  methods: {
    handleGetDetail: function(i) {
      Pokemon.card
        .find(i)
        .then(result => {
          alert(JSON.stringify(result));
        })
        .catch(error => {
          alert(JSON.stringify(error));
        });
    },
    searchByName: function() {
      const self = this;
      this.cardList = [];
      Pokemon.card.all({ name: this.name, pageSize: 1 }).on("data", card => {
        self.cardList.push(card);
      });
    },
    getCards: function() {
      const self = this;
      Pokemon.card.all({ name: "Blastoise", pageSize: 1 }).on("data", card => {
        return self.cardList.push(card);
      });
    },
    filteredList(list) {
      return list.filter(item => {
        if (item.hp) {
          return item.hp.toLowerCase().includes(this.searchText.toLowerCase());
        }
        return list;
      });
    }
  }
};
</script>

<style scoped lang="scss">
ul {
  list-style-type: none;
  padding: 0;
}
.media {
  img {
    width: 150px;
  }
}
</style>

É possível notar que os dois arquivos são muito semelhantes. Destaque para o Angular seria apenas o TypeScript e a vantagem de poder tipar as variáveis. O data-binding e o loop(for) são muito similares.

Como no exemplo estou utilizando um SDK para as chamadas da API – não precisamos utilizar o HttpModule do Angular, nem o Axios do VueJS, embora nesse segundo poderíamos utilizar fetch também.

Rotas

Para as rotas também é tudo muito parecido. Em VueJS utilizamos vue-router, e para Angular, @angular/router. Ambas as bibliotecas fazem parte do core de cada framework (ok, eu sei que o VueJS é apenas uma biblioteca), e isso é um ponto positivo.

Angular: app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { CardsComponent } from './views/cards/cards.component';
import { HomeComponent } from './views/home/home.component';
import { AboutComponent } from './views/about/about.component';

const routes: Routes = [
  // {
  //   path: '',
  //   redirectTo: 'cards',
  //   pathMatch: 'full'
  // },
  {
    path: '',
    component: HomeComponent
  },
    {
    path: 'about',
    component: AboutComponent
  },
  {
    path: 'cards',
    children: [
      {
        path: '',
        component: CardsComponent
      }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

VueJS: route.js

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/about",
      name: "about",
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =>
        import(/* webpackChunkName: "about" */ "./views/About.vue")
    },
    {
      path: "/cards",
      name: "cards",
      // route level code-splitting
      // this generates a separate chunk (cards.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =>
        import(/* webpackChunkName: "cards" */ "./views/Cards.vue")
    }
  ]
});

Filtros

Acredito que a maior diferença foi a criação do filtro para os cards, que na verdade é bem simples de se criar, mas o Angular, por sua arquitetura, nos obriga a criar um novo arquivo ao invés de inserir apenas uma função para filtrar a lista, como é possível fazer com o VueJS, e também era muito simples com AngularJS.

Angular: card-filter-by.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'cardsFilterBy'
})
export class CardsFilterBy implements PipeTransform {

  transform(items: any, searchText: string, args?: any): any [] {
    if (searchText) {
      searchText = searchText.toLowerCase();
      
      return items.filter((item: any) => {
        if(item.hp) {
          return item.hp.toLowerCase().includes(searchText);
        } else {
          return;
        }
        
      });
    }
    return items;
  }
}

Template:

<li class="media col-sm-3 " *ngFor="let item of cardList | cardsFilterBy: searchText">

Além disso, é preciso injetar o novo filtro ao modulo do Angular para poder utilizá-lo – já com VueJs é muito mais simples – talvez não tão escalável, mas simples.

VueJS: Cards.vue, função:

filteredList(list) {
  return list.filter(item => {
    if (item.hp) {
      return item.hp.toLowerCase().includes(this.searchText.toLowerCase());
    }
    return list;
  });
}

Template:

<li
  class="media col-sm-3 "
  v-for="item in filteredList(cardList)"
  :key="item.id"
>

Bom, agora você pode tirar as suas conclusões, e além disso, utilizar o Angular de forma mais simples para pequenas aplicações, ou mesmo utilizar o VueJS. O importante é saber as limitações de cada ferramenta.

Segue o código fonte das apps:

VueJS