Você está na página 1de 136

Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.

com
Caelum Sumário

Sumário

1 Construindo o alicerce da nossa aplicação 1


1.1 Angular, o framework MVC da Google 1
1.2 Angular 2 e novo paradigma 1
1.3 Conhecendo um pouco da nossa aplicação, o Caelumpic 2
1.4 Instalando o Node.js em casa 3
1.5 Angular CLI 5
1.6 Exercício - Iniciando nosso projeto com Angular CLI 5
1.7 Entendendo a estrutura do projeto: O componente principal 7
1.8 Exercício - Nomeando nossa App e imagens estáticas 8
1.9 Exercício - Adicionando um pouco de CSS em nosso projeto 9

2 Criando Componentes 11
2.1 Exercício - Criando um componente para as fotos 11
2.2 Exercício - Parâmetros 13
2.3 Exercício - Lacunas em templates e data binding 14

3 Consumindo uma API 17


3.1 Http e injeção de dependências 17
3.2 TypeScript e definição de tipos estáticos 20
3.3 Exercício - Configurando o serviço HTTP do Angular 21
3.4 Chamada da API remota 22
3.5 Então Angular usa RxJS? 23
3.6 Arrow functions e escopo léxico 24
3.7 Exercício - Conectando com a API 26
3.8 A diretiva NgFor 27
3.9 Exercício - Exibindo a lista de fotos 27

4 Dividindo para conquistar! 29


4.1 Exercício - Criando um componente para exibir as fotos: PainelComponent 29
4.2 Exercício - Socorro, meu painel nada exibe! 30

Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com


Sumário Caelum

4.3 Mais de uma página 32


4.4 Exercício - Página de cadastro de fotos 32
4.5 Single Page Application (SPA) 35
4.6 Exercício - Configurando rotas 35
4.7 Exercício - Redirecionar caminhos inexistentes 38
4.8 Exercício - Links para rotas 38

5 Melhorando a experiência do usuário 41


5.1 Pipes, tubos que transformam! 41
5.2 Variáveis locais 42
5.3 Exercício: iniciando nosso pipe 43
5.4 Criando um pipe 44
5.5 Exercício: Pipe, iniciando a implementação 44
5.6 Que tal um interface type? 45
5.7 Exercício: implementando uma interface 45
5.8 Definir tipos para usar autocomplete 46
5.9 Exercício: Pipe, implementando a lógica 46
5.10 Event binding para atualizar a view 47
5.11 Exercício: fazendo o filtro funcionar 47

6 Cadastrando novas imagens 49


6.1 Exercício - Formulário de cadastro 49
6.2 Data-binding resolve? 50
6.3 Exercício - One-way data-binding 53
6.4 Associação de eventos (event-binding) 54
6.5 Exercício - Utilizando o método cadastrar 55
6.6 E o two-way data-binding? 56
6.7 Exercício - Fazendo two-way data-binding 56
6.8 ngModel 57
6.9 Exercício - Two-way data-binding com ngModel 58
6.10 Envio de dados para o servidor 59
6.11 Exercício - Gravando uma nova foto na API 62
6.12 Ciclo de Vida de um componente 63
6.13 Exercício - Manipulando propriedades de uma classe 66

7 Validando nosso formulário 67


7.1 Validação orientada a template 67
7.2 Exercício - Validando o formulário 69
7.3 Validação orientada a modelo 71

Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com


Caelum Sumário

7.4 O construtor de formulários 73


7.5 Exercício - Validando o formulário pelo modelo 74
7.6 Compondo validadores 76
7.7 Exercício - Mais validações 77

8 Encapsulando nossa view 79


8.1 Estilizando um componente 79
8.2 O problema de estilos globais 81
8.3 Estilo por componente 82
8.4 Exercício - Adicionando estilo no componente 83
8.5 View Encapsulation e seus tipos 85
8.6 Shadow DOM, podemos confiar? 85
8.7 Exercício opcional: testando a emulação do Shadow DOM 86

9 Isolando código repetido em serviços 87


9.1 Criando um serviço 87
9.2 Exercício - Criando um serviço 89
9.3 Exercício - utilizando o serviço 90
9.4 Injectable 92
9.5 Exercício - serviço como dependência 92

10 Removendo dados inconsistentes 93


10.1 Mais uma associação de eventos (event binding) 93
10.2 Exercício - um botão para remover fotos 95
10.3 Serviço de remoção 95
10.4 Exercício - implementando o serviço de remoção 96
10.5 Um pouco sobre change detection 98
10.6 Exercício - Atualizando listagem de fotos 98
10.7 Exibindo mensagens para o usuário 99
10.8 Exercício - configurando mensagens 99

11 Alterando fotos cadastradas 101


11.1 Rotas parametrizadas 101
11.2 Exercício - Ajustando nossas rotas 102
11.3 Buscando pelo parâmetro 103
11.4 Inclusão ou alteração? 105
11.5 Exercício: abrindo a foto para alterá-la 105
11.6 Exercício opcional - Navegação no lado do componente 107

12 Modificadores de acesso e Encapsulamento 110


12.1 Isolando reponsabilidades 110

Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com


Sumário Caelum

12.2 Um pouco mais sobre tipagem e o tipo any 111


12.3 Exercício - Isolando as mensagens 111
12.4 Uma classe para as mensagens 112
12.5 TypeScript e o favorecimento do encapsulamento 113
12.6 O modificador private 114
12.7 TypeScript e suas facilidades 115
12.8 Propriedades e o açúcar sintático do TypeScript 116
12.9 Exercício - Modificadores de acesse em uma classe 117

13 Apêndice: Decisão nas mãos do usuário 118


13.1 Isolando a lógica de confirmação em um componente 118
13.2 O template do nosso componente 119
13.3 Eventos customizados com EventEmitter 120
13.4 Tornando nosso componente ainda melhor 121
13.5 Exercício - Criando um botão para decisões 122
13.6 O Menu da aplicação 124
13.7 Exercício opcional - adicionando a navegação 124

14 Apêndice: aproveitando seu conhecimento sobre jQuery 126


14.1 jQuery com Angular2 é possível? 126
14.2 Exercício - Baixando o jQuery pelo npm 126
14.3 Referência de elemento através de injeção 127
14.4 Exercício - Implementando o fadeOut do painel 127
14.5 Que erro de compilação é esse? 128
14.6 Definições do jQuery para o TypeScript 128
14.7 Declarando mais uma vez variáveis no template 129
14.8 Exercício - Usando o jQuery 129

Versão: 20.9.14

Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com


CAPÍTULO 1

CONSTRUINDO O ALICERCE DA NOSSA


APLICAÇÃO

Quando desenvolvemos no server-side, organizamos nosso código em camadas para facilitar a


manutenção, o reaproveitamento e a legibilidade de nosso código. É muito comum aplicarmos o modelo
MVC (Model, View, Controller), que consiste na separação de tarefas, facilitando assim a reescrita de
alguma parte e a manutenção do código.

Porém, não é raro o mesmo desenvolvedor deixar de lado essas práticas quando codifica no client-
side. Mesmo aqueles que procuram organizar melhor seu código acabam criando soluções caseiras que
nem sempre são bem documentadas.

Tendo como base este cenário, frameworks MVC client-side foram criados. Entre eles temos o
Backbone, Ember, Knockout, entre outros.

1.1 ANGULAR, O FRAMEWORK MVC DA GOOGLE


Um framework MVC no lado do cliente que tem ganhado muita atenção da comunidade é o
Angular. Criado como um projeto interno da Google e liberado para o público em 2009, ele tem como
foco a criação de Single Page Applications (SPA’s). Este tipo de aplicação não recarrega a página durante
seu uso, dando uma experiência mais fluída para o usuário. Não se preocupe se você é novo para este
tipo de aplicação, você terá a oportunidade de entender melhor seus conceitos ao longo do treinamento.

1.2 ANGULAR 2 E NOVO PARADIGMA


Angular 2 é um salto interestelar na história do framework voltado para a criação de aplicações Web,
que rompe definitivamente com sua versão 1.X. Não importa o quão bom você seja com a primeira
versão do framework, porque o paradigma mudou.

Um paradigma, em poucas palavras, é a visão do mundo que temos e como procuramos respostas
para solucionar problemas. Quando um paradigma muda, todos começam do zero. Se você nunca
trabalhou com Angular, ótimo. Se você vem do Angular 1.X, peço que esvazie a sua mente para novas
possibilidades e caminhos deste framework.

1 CONSTRUINDO O ALICERCE DA NOSSA APLICAÇÃO 1


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
1.3 CONHECENDO UM POUCO DA NOSSA APLICAÇÃO, O CAELUMPIC
O projeto desse curso se chamará Caelumpic, e será um sistema de gerenciamento de imagens. Ele
permitirá que o usuário cadastre fotos e busque por aquelas que seguem determinado critério.

Figura 1.1: Preview da aplicação

Figura 1.2: Filtro da CaelumPic

2 1.3 CONHECENDO UM POUCO DA NOSSA APLICAÇÃO, O CAELUMPIC


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Figura 1.3: Cadastro de fotos CaelumPic

Agora que você já conhece um pouco sobre a aplicação que construiremos, mesmo com o foco do
curso em Angular, alguns recursos do framework necessitam de um servidor web. Para que você não
caia em questões de infraestrutura que dizem respeito a isso, disponibilizamos o projeto CaelumPic com
tudo necessário para subir um servidor local, com inclusive a persistência de dados sem que você tenha
que instalar um banco de dados específico.

É importante destacar que o uso do projeto inicial CaelumPic não é opcional, pois ele já registra
todos os endpoints da API que será consumida pela nossa aplicação em Angular.

A API que vamos executar está dentro da pasta do curso, server, e para fazermos funcionar a API
local, você precisa ter instalado o Node.js.

O Node.js é um ambiente JavaScript multiplataforma disponível para Linux, Mac e Windows.

1.4 INSTALANDO O NODE.JS EM CASA


Para instalá-lo, siga as instruções abaixo referentes a sua plataforma:

Em qualquer sistema operacional


Verifique antes se o Node.js já está instalado:
nodejs -v

ou

node -v

1.4 INSTALANDO O NODE.JS EM CASA 3


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Caso o comando seja desconhecido, prossiga para instruções da sua plataforma. Este comando exibe
a versão do NodeJS, é importante que tenha a versão igual ou superior a 6.5.0.

Linux (Ubuntu)
No Ubuntu, através do terminal (permissão de administrador necessária) execute o comando abaixo:

sudo apt-get install -y nodejs

Caso tenha dificuldades em atualizar a versão do Node.js, tente executar o seguinte comando:
curl -sL https://deb.nodesource.com/setup_7.x | sudo -E bash -
sudo apt-get install -y nodejs

ATENÇÃO: em algumas distribuições Linux, pode haver um conflito de nomes quando o Node é
instalado pelo apt-get. Neste caso específico, no lugar do binário ser node, ele passa a se chamar nodejs.
Isso gera problemas, pois a instrução npm start não funcionará, pois ela procura o binário node e não
nodejs . Para resolver, use a seguinte instrução no terminal para subir o servidor:

sudo ln -s /usr/bin/nodejs /usr/bin/node


node server

Windows
Baixe o instalador clicando no grande botão install diretamente da página do Node.js. Durante a
instalação, você apenas clicará botões para continuar o assistente. Não troque a pasta padrão do Node.js
durante a instalação a não ser que você saiba exatamente o que está fazendo.

Mac OSX
O homebrew é a maneira mais recomendada para instalar o Node.js em sua máquina, através do
comando:
brew update
brew install node

Não usa homebrew? Sem problema, baixe o instalador clicando no grande botão install
diretamente da página do Node.js.

Rodando o servidor local


Com o Node.js instalado, dentro da pasta da API, baixe todas as dependências do projeto através do
seu terminal favorito com o comando:
~/Desktop/js-45/server npm install

Em menos de um minuto, todas as dependências para rodar o servidor terão sido baixadas. Para
subi-lo acessamos seu diretório utilizamos o comando:

~/Desktop/js-45/server npm start

4 1.4 INSTALANDO O NODE.JS EM CASA


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Repare que seu terminal mostrará a seguinte mensagem:

Banco data.db pronto para uso


Servidor escutando na porta: 3000

A partir deste momento temos uma API local, podendo ser acessada pelo endereço
http://localhost:3000/

1.5 ANGULAR CLI


Junto com o desenvolvimento do Angular 2 a equipe começou a desenvolver em paralelo o Angular
CLI. É uma ferramenta de linha de comando que ajuda a montar a infra da sua aplicação e acelerar a
criação de componentes.

Instalando o Angular CLI globalmente


Ferramentas em linha de comando são geralmente instaladas globalmente para que possamos utilizá-
la em qualquer local da nossa aplicação. Mas muita atenção, porque instalar pacotes do Node.js requer
permissão de administrador. Tenha certeza de ter os privilégios de administrador antes de
continuar. O Angular CLI tem como dependência o NodeJS com versão v6.5.0 ou superior, e o [8]
[npm] (gerenciador de pacotes do NodeJS).

Verifique se o Node.js e o npm estão devidamente instalados e em suas versões mais recentes:

nodejs

node -v

npm

npm -v

Agora, para instalar o Angular CLI execute o comando:


npm install -g @angular/cli

Esse processo levará um tempo (cerca de 1 hora) porque várias dependências são baixadas.

1.6 EXERCÍCIO - INICIANDO NOSSO PROJETO COM ANGULAR CLI


1.Abra o terminal, dentro dele execute o comando abaixo para acessar o seu Desktop (Área de
Trabalho):
~$ cd ~/Desktop

2.Agora precisamos executar o comando para criar uma pasta com o nome de js-45, ela ficará no
Desktop (Área de Trabalho):
~/Desktop$ mkdir js-45

1.5 ANGULAR CLI 5


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
3.Já vimos o comando para entrar dentro de uma pasta, vamos utilizá-lo novamente para entrar
dentro da js-45 que acabamos de criar:
~/Desktop$ cd js-45

Show! Temos uma pasta para nossas aulas. O próximo comando que iremos executar terá a
reponsabilidade de criar a base do nosso projeto em Angular 2, para fazermos isso de uma forma feliz e
simples, utilizaremos o Angular CLI:
~/Desktop/js-45$ ng new caelumpic

Esse comando cria uma pasta com o nome do nosso projeto (caelumpic) e dentro dessa pasta que
ficará a base do nosso projeto com Angular 2. Esse processo demora um pouco e pode variar
dependendo da qualidade da internet, pois nesse momento é feito o download de várias dependências
para que o nosso framework funcione.

FAZENDO APOSTILA EM CASA


Se você não estiver utilizando uma das máquinas da Caelum é importante você fazer o passo-a-
passo da explicação que está
anterior a este exercício, nele você irá aprender a instalar o NodeJS e o Angular CLI.

4.Para verificar se tudo está certo com base do nosso projeto, vamos rodar dois comandos: primeiro,
tem a responsibilidade de entrar na pasta caelumpic; segundo, irá instalar todas as dependências do
nosso projeto e realizar a transcompilação do nosso código gerado pelo Angular CLI de Type Script para
JavaScript ES6:
~/Desktop/js-45$ cd caelumpic
~/Desktop/js-45/caelumpic$ ng server --open

5.Ah! Após executar o segundo comando, nós teremos que aguardar um tempinho bom. Mas no
final será aberto automaticamente o seu browser (navegador) com a URL (http://localhost:4200). Se no
browser que for aberto tiver a saída da imagem seguinte, significa que tudo está certo.

Figura 1.4: App Works

6 1.6 EXERCÍCIO - INICIANDO NOSSO PROJETO COM ANGULAR CLI


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
1.7 ENTENDENDO A ESTRUTURA DO PROJETO: O COMPONENTE
PRINCIPAL
Um componente é a unidade de código que encapsula os dados, o comportamento e a apresentação,
podendo coexistir com outros componentes sem bagunçá-los. Componentes podem ser combinados
entre si para criar recursos mais complexos, no entanto fáceis de manter.

Em Angular, um componente nada mais é do que uma classe (ES6). Vamos criar a classe
AppComponent no arquivo caelumpic/app/src/app.component.ts .
// caelumpic/app/src/app.component.ts
class AppComponent {}

Nossa classe não parece ter cara de componente, certo? Um componente precisa ter um template que
guarde sua apresentação, incluindo um seletor que é uma maneira de referenciá-lo no template de outros
componentes.

Para tornarmos AppComponent um componente de verdade precisamos utilizar o


decoratorComponent. Linguagens como Python possuem decorators, mas infelizmente o JavaScript,
mesmo em sua versão 2015 (ES6) ainda não possui esse recurso. Então, como usaremos um decorator
sem que a linguagem o suporte? A resposta é simples, não programaremos usando JavaScript, mas uma
linguagem que possui todos os recursos dessa e mais outros recursos fantásticos, incluindo os decorators.
Programaremos usando TypeScript.

O TypeScript é uma linguagem criada pela Microsoft e abraçada pela equipe do Angular. Não é à toa
que criamos um arquivo com a extensão .ts .

Utilizaremos a sintaxe de importação do sistema de módulos do ES6, para importarmos o decorator


Component que está no pacote @angular/core :

// caelumpic/app/src/app.component.ts
import {Component} from '@angular/core';

class AppComponent {}

Agora que importamos, vamos adicionar o decorator na definição da classe:

// caelumpic/app/src/app.component.ts
import {Component} from '@angular/core';

@Component
class AppComponent {}

Nosso decorator precisa receber duas configurações, no mínimo. A primeira é o apelido com qual
esse componente será chamado no template de outros componentes, e o segundo é o local do seu
template, isto é, do arquivo HTML que define a marcação:
// caelumpic/app/src/app.component.ts
import {Component} from '@angular/core';

1.7 ENTENDENDO A ESTRUTURA DO PROJETO: O COMPONENTE PRINCIPAL 7


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
@Component({
selector: 'app-root',
templateUrl: './app/app.component.html'
})
class AppComponent {}

Dessa forma, quando usarmos nosso componente em nossa páginas, eles ficarão como <app-root>
</app-root> , ou seja, o nome do seu seletor. Veja que também já indicamos através de templateUrl
o caminho do template utilizado pelo componente, ou seja, o código HTML que será utilizado. O
arquivo ainda não existe, vamos criá-lo:
<!-- caelumpic/app/app.component.html -->
<h1>Caelum Pic</h1>

1.8 EXERCÍCIO - NOMEANDO NOSSA APP E IMAGENS ESTÁTICAS


Com a estrutura básica da App pronta, precisamos fazer as primeiras adaptações para nosso projeto
ter a cara da CaelumPic.

1.Começando de maneira simples, vamos dar nome à nossa App armazenando um título para ela.
Para isto, faça no componente principal, com uma propriedade title e defina o valor: Caelum Pic.

// caelumpic/src/app/app.component.ts
import { Component } from '@angular/core';

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

export class AppComponent {


title = 'Caelum Pic';
}

2.Depois, alteramos o arquivo de template HTML do componente app com a seguinte estrutura:
<!-- caelumpic/src/app/app.component.html -->
<h1>{{title}}</h1>
<img src="assets/img/leao.jpg" alt="leão" style="width:50%">

Veja que fizemos referência para uma imagem estática dentro de nosso projeto. Para ela funcionar
corretamente, crie o diretório img dentro de caelumpic/src/assets/ , e salve uma imagem com o
nome leao.jpg (pegue uma imagem na web, por exemplo a imagem da página sobre Leão da Wikipedia).

O diretório assets é utilizado para guardarmos arquivos estáticos do nosso projeto, e também para
arquivos gerados automaticamente.

Perceba que estamos utilizando a propriedade title do componente app só que agora dentro de uma
estrutura HTML.

3.Verique a página em seu browser e perceba que nossas alterações já foram aplicadas sem

8 1.8 EXERCÍCIO - NOMEANDO NOSSA APP E IMAGENS ESTÁTICAS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
precisarmos recarregar. Devemos chegar neste resultado:

Figura 1.5: Imagem estática

1.9 EXERCÍCIO - ADICIONANDO UM POUCO DE CSS EM NOSSO


PROJETO
Nosso projeto vai usar Bootstrap como framework para o frontend, e nos poupar tempo com
detalhes de CSS.

1.Vamos instalar o Bootstrap usando o npm e salvá-lo como dependencia do projeto. No terminal,
dentro da pasta caelumpic, execute o comando:

~/Desktop/js-45/caelumpic$ npm install bootstrap --save

Podemos verificar se foi corretamente instalado verificando as dependências no arquivo


package.json na raiz do nosso projeto.

2.Agora, para adicionar o arquivo de CSS do Bootstrap no projeto, precisamos alterar o arquivo
.agular-cli.json. Abra o arquivo e adicione uma segunda chave no array de styles, conforme está no
código abaixo:
// .angular-cli.json
// código anterior omitido
"styles": [
"styles.css",
"../node_modules/bootstrap/dist/css/bootstrap.min.css"
],

1.9 EXERCÍCIO - ADICIONANDO UM POUCO DE CSS EM NOSSO PROJETO 9


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// código posterior omitido

3.Edite o template adicionando a estrutura HTML e classes do Bootstrap:


<!-- caelumpic/src/app/app.component.html -->
<div class="jumbotron">
<h1 class="text-center">{{title}}</h1>
</div>
<div class="container">
<img class="img-responsive center-block" src="assets/img/leao.jpg" alt="leão" style="width:50%">

</div>

Com isto nossa aplicação está com uma aparência melhor utilizando os estilos do Bootstrap:

Figura 1.6: CaelumPic com Bootstrap

10 1.9 EXERCÍCIO - ADICIONANDO UM POUCO DE CSS EM NOSSO PROJETO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 2

CRIANDO COMPONENTES

Nossa aplicação possui apenas o componente AppComponent, que renderiza a página principal da
nossa aplicação. Que tal criarmos mais um? Mas qual, nessa fase inicial do projeto?

Veja que usamos a tag <img> no template de AppComponent, com classes do Bootstrap voltadas
para criação de uma imagem responsiva, aquela que se adapta ao espaço disponível na tela. Você lembra
quais foram as classes? Se por acaso você não sabe ou não lembra, não se preocupe.

Podemos criar um componente chamado FotoComponent, que isolará a complexidade de uma


imagem responsiva, sendo assim, toda vez que precisarmos deste tipo de imagem, basta utilizarmos o
componente que esconderá seus detalhes de criação.

2.1 EXERCÍCIO - CRIANDO UM COMPONENTE PARA AS FOTOS


1.Vamos criar uma pasta foto dentro de caelumpic/src/app. Lá iremos armazenar todos os arquivos
referentes a este componente.

2.Dentro da pasta foto, crie o arquivo principal do componente: foto.component.ts com a estrutura
abaixo:

// caelumpic/src/app/foto/foto.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})

export class FotoComponent {}

3.Além do componente, temos o seu template HTML, no qual informamos na configuração do


módulo. Crie o arquivo foto.component.html com a seguinte estrutura:
<!-- caelumpic/src/app/foto/foto.component.html -->
<img src="assets/img/leao.jpg" alt="Leão" class="img-responsive center-block">

4.Por enquanto, temos apenas um componente que diz respeito à apresentação de uma foto. Mais
tarde, utilizaremos outros artefatos do Angular, como pipes e services. Sendo assim, precisamos criar um
módulo com nome foto.module.ts, que por enquanto conterá o componente FotoComponent:

// caelumpic/src/app/foto/foto.module.ts

2 CRIANDO COMPONENTES 11
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
import { NgModule } from '@angular/core';
import { FotoComponent } from './foto.component';

@NgModule({
declarations: [ FotoComponent ],
exports: [ FotoComponent ]
})

export class FotoModule {}

Veja que, diferente do módulo principal da aplicação (app.modules.ts), não precisamos definir a
propriedade bootstrap e se você continuar comparando os arquivos verá que temos mais coisas no
arquivo do módulo principal que aprenderemos com detalhes mais a frente.

5.Precisamos importar o módulo que acabamos de criar em app.module.ts:


// caelumpic/src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { FotoModule } from './foto/foto.module';

@NgModule({
imports: [ BrowserModule, FotoModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})

export class AppModule {}

Se você reparar nosso código acima tiramos uma parte do código que o angular-cli criou para nós,
mas não estamos utilizando no momento.

6.Chegou o momento de utilizarmos o componente FotoComponent no template de


AppComponent. Lembre-se que no template usamos o nome definido no seletor do componente, sendo
assim, usaremos FotoComponent como <foto></foto> :

<!-- caelumpic/src/app/app.component.html -->


<div class="jumbotron">
<h1 class="text-center">{{title}}</h1>
</div>

<div class="container">
<foto></foto>
</div>

7.Adicione mais um componente <foto></foto> :


<!-- caelumpic/src/app/app.component.html -->
<div class="jumbotron">
<h1 class="text-center">{{title}}</h1>
</div>

<div class="container">
<foto></foto>
<foto></foto>
</div>

12 2.1 EXERCÍCIO - CRIANDO UM COMPONENTE PARA AS FOTOS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Recarregando nossa página, como era esperado, duas imagens são exibidas:

Figura 2.1: CaelumPic usando 2x o componente foto

2.2 EXERCÍCIO - PARÂMETROS


1.Queremos reutilizar nosso componente para exibir diferentes imagens, mas sabemos que
atualmente isso não é possível. O endereço da imagem está fixo no template do componente, o que não
faz muito sentido.

Seria perfeito se pudermos passar o endereço da foto e o seu título desta maneira:

<!-- caelumpic/src/app/app.component.html -->


<div class="jumbotron">
<h1 class="text-center">{{title}}</h1>
</div>

<div class="container">
<foto url="assets/img/leao.jpg" titulo="Leão"></foto>
<foto url="assets/img/leao-branco.jpg" titulo="Leão branco"></foto>
</div>

E é possível fazer, para isto só precisamos preparar o nosso componente foto para receber os
parâmetros titulo e url.

2.Vamos alterar caelumpic/src/app/foto/foto.component.ts e adicionar a propriedade que receberá


os dados passados pelo usuário:

2.2 EXERCÍCIO - PARÂMETROS 13


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {
titulo;
url;
}

Aqui temos uma peculiaridade do TypeScript. Em ES2015, só podemos definir as propriedades


através do constructor, mas o TypeScript simplifica isso e podemos defini-las diretamente na classe.
Contudo, isso não é suficiente.

Nem todas as propriedades de uma classe podem receber dados externos, é por isso que por padrão
essas propriedades não recebem entrada.

3.Para que seja possível passar valores para elas, precisamos usar o decorator Input, também
importado do pacote @angular/core :

// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {

@Input() titulo;
@Input() url;
}

4.Até aqui ditamos o que nosso componente está preparado para receber, porém em nenhum
momento do seu template foto.component.html estamos usando essas informações. Primeiro, não
temos mais o endereço da imagem nem seu título fixos:

<!-- caelumpic/src/app/foto/foto.component.html -->


<img src="" alt="" class="img-responsive center-block">

E agora? Temos duas lacunas que precisam ser preenchidas em nosso template.

2.3 EXERCÍCIO - LACUNAS EM TEMPLATES E DATA BINDING


1.Veja que o nosso componente recebe titulo e url, no entanto quem precisará ler essas informações
é o seu template. Para que essa leitura seja possível, usamos uma sintaxe especial:

<!-- caelumpic/src/app/foto/foto.component.html -->


<img [src]="url" [alt]="titulo" class="img-responsive center-block">

Veja que agora os atributos src e alt estão entre colchetes. Essa sintaxe particular realiza uma
associação (binding) entre o atributo e sua respectiva fonte de dados. Sendo assim o atributo src

14 2.3 EXERCÍCIO - LACUNAS EM TEMPLATES E DATA BINDING


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
depende do dado url e o atributo alt do dado titulo. Em outras palavras, o que estamos realizando é
uma associação de dados, o famoso data binding.

Angular possui mais de um tipo de data binding, mas por enquanto só precisamos saber que essa
associação que fizemos é do tipo unidirecional, onde o dado caminha da fonte de dados para a view
(template) e nunca o caminho inverso. Qual a consequência disso? Nesse tipo de associação só lemos
dados e nunca os atualizamos.

2.Agora é só recarregarmos a nossa página e as duas imagens são exibidas através do nosso
componente FotoComponent:

Figura 2.2: CaelumPic com 2 imagens diferentes

Veja que não houve manipulação de DOM para conseguirmos este resultado, um dos pontos fortes
de frameworks que trabalham com data binding.

Exercício Opcional: Angular Expression ainda existe!


Ótimo! Tudo está funcionando, mas há uma sintaxe alternativa para o tipo de data binding que
utilizamos, que é a Angular Expression (AE), muito utilizada na versão 1.0 do Angular!

2.3 EXERCÍCIO - LACUNAS EM TEMPLATES E DATA BINDING 15


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Toda AE é caracterizada pelo uso de {{ }} e o que vier dentro das chaves duplas é o dado
necessário para completar o template.

1.Vamos alterar o template do nosso componente FotoComponent:

<!-- caelumpic/src/app/foto/foto.component.html -->


<img src="{{url}}" alt="{{titulo}}" class="img-responsive center-block">

2.Teste novamente em seu navegador e veja tudo continua funcionando como antes, apesar de
usarmos uma sintaxe alternativa para indicar o mesmo tipo de associação que fizemos antes.

(Opcional) Trabalhando com caminhos relativos para templates


Caso você não use o Angular CLI, deve ter reparado que tanto no componente AppComponent e
no FotoComponent precisamos utilizar URL's absolutas para indicar a localização de seus templates na
propriedade templateUrl. Contudo, isso não precisa ser assim.

Para resolver, basta adicionar no decorator dos dois componentes a propriedade moduleId e passar
o enigmático valor module.id. Com isto, podemos passar o caminho relativo dos templates, alterando o
valor de templateUrl " ./app/ " para " ./ ":
// caelumpic/src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
moduleId: module.id,
selector: 'app',
templateUrl: './app.component.html'
})
export class AppComponent {
title = "Caelum Pic";
}

// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';

@Component({
moduleId: module.id,
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {

@Input() titulo;
@Input() url;
}

O moduleId nos permite encontrar o template no próprio diretório do componente. Em projetos


iniciados com Angular CLI, esta questão já vem resolvida nas configurações do projeto.

16 2.3 EXERCÍCIO - LACUNAS EM TEMPLATES E DATA BINDING


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 3

CONSUMINDO UMA API

3.1 HTTP E INJEÇÃO DE DEPENDÊNCIAS


É extremamente comum que uma aplicação feita em Angular consuma dados de um servidor por meio
de uma API REST, que retorna esses dados na estrutura JSON. Nosso servidor já possui uma série de
endpoints que utilizaremos ao longo do treinamento. Bom, de todos os endpoints que o servidor possui,
há um deles que nos interessa. Vamos vê-lo.

Verifique se seu servidor local está iniciado, abra o navegador e digite o endereço:
http://localhost:3000/v1/fotos

Figura 3.1: API de fotos

Pois bem, e se no lugar de abrir o endereço pelo navegador, nossa aplicação Angular realizasse essa
tarefa para nós? Teríamos acesso a uma lista de fotos que pode substituir aquela que temos fixa em nosso
componente. Se os dados mudam, a lista que é exibida para o usuário pelo nosso template também
mudará. Perfeito, não?

Para que possamos acessar os dados do nosso servidor, precisaremos realizar requisições Ajax,
aquelas que são assíncronas por natureza. Se você vem do mundo jQuery, já deve ter usado $.ajax ou
uma de suas especializações. O Angular possui seu próprio serviço para executar este tipo de requisição,
o Http. Porém, como acessar este serviço especial do Angular?

Http o serviço de requisições Ajax do Angular


Para usar este serviço, o primeiro passo é importar a classe Http em nosso componente:

// caelumpic/src/app/app.component.ts
import {Component} from '@angular/core';
import { Http } from '@angular/http';
//código posterior omitido

3 CONSUMINDO UMA API 17


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Apenas importar o Http não é suficiente, isto porque precisamos de uma instância desta classe para
nos comunicarmos com um servidor. Em ES6, toda classe possui um constructor, que é chamado toda
vez que uma instância da classe é criada.

Sabendo disso, podemos criar uma instância de Http no construtor da nossa classe:

// caelumpic/src/app/app.component.ts
import {Component} from '@angular/core';
import { Http } from '@angular/http';

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {

title = 'Caelum Pic';


fotos = [];

constructor() {
let http = new Http(); // não funciona, você verá o motivo!
}
}

Apesar dos nossos esforços, o código que escrevemos não funcionará. Isto porque a criação de uma
instância de Http envolve muito mais do que simplesmente chamar seu construtor. Ou seja, a criação e
preparação de uma instância dessa classe não é nada simples.

VAR VS LET NO ES6

Se você ainda não conhece a versão 6 do JavaScript (ES6), deve estar pensando que está
incorreta a escrita da palavra let e que deveria ser var . Em ES6, a maneira mais recomendada é
declarar variáveis com let . A palavra reservada let declara uma variável com escopo de bloco,
que só existirá no bloco em que foi declarada.

Antes do ES6, era possível criar um escopo de bloco através de técnicas de programação, que
deixavam nosso código mais verboso e difícil de ler. A palavra reservada let foi uma adição muito
aplaudida pela comunidade.

A boa notícia é que podemos nos livrar dos detalhes de criação do serviço Http, solicitando para o
framework seu próprio serviço HTTP. Para fazer esta solicitação, podemos adicionar o HTTP como
dependência no construtor da classe de AppComponent:

// caelumpic/src/app/app.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';

@Component({
selector: 'app-root',

18 3.1 HTTP E INJEÇÃO DE DEPENDÊNCIAS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
templateUrl: './app.component.html'
})
export class AppComponent {

title = 'Caelum Pic';


fotos = [];

constructor(Http) {}
}

Será que funciona? Não, pois ao recarregarmos a página nossos componentes não são exibidos. E se
abrirmos o console, vemos a seguinte mensagem de erro:
Error: Can't resolve all parameters for AppComponent: (?).(…)

O Angular está dizendo que não consegue resolver o parâmetro do construtor. Faz todo sentido,
porque é ele que cria a instância de AppComponent e quando vê o parâmetro http não sabe o que
fazer com ele.

Decorator @Inject
Para deixar claro para o Angular que precisamos que ele busque esta dependência usamos o
decorator Inject :
// caelumpic/src/app/app.component.ts
import {Component, Inject} from '@angular/core';
import { Http } from '@angular/http';

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {

title = 'Caelum Pic';


fotos = [];

constructor(@Inject(Http) http) { }
}

Repare que @Inject recebe como parâmetro o tipo (classe) do serviço que importamos, o Http do
módulo @angular/http. Como usamos a anotação antes do parâmetro do construtor, a variável http
receberá o serviço Http . Não nos importa como o framework consegue instanciar este serviço, o que
nos interessa é que ele está prontinho para uso.

Recarregando a página, recebemos outra mensagem de erro no console do navegador. Olhando a


segunda linha, vemos esta mensagem:
ORIGINAL EXCEPTION: No provider for Http!

Apesar de termos pedido para o Angular criar uma instância de Http para nós, parece que ele
também não sabe criá-la. Vamos entender o que houve: a mensagem de erro indica que não há um
provedor para Http; Provedores são serviços especializados na construção de objetos e que auxiliam o

3.1 HTTP E INJEÇÃO DE DEPENDÊNCIAS 19


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
framework no processo de criação de objetos injetados (como o nosso). Neste caso, parece não existir
nenhum provider que saiba construir Http para nós.

HttpModule e Providers
Podemos resolver isto importando o módulo HttpModule em AppModule. Este módulo já traz um
provider configurado, que será usado pelo Angular toda vez que um objeto do tipo Http for injetado
com o decorator Inject .
// caelumpic/src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { FotoModule } from './foto/foto.module';
// importou o módulo que já possui um provider configurado
import { HttpModule } from '@angular/http';

// HttpModule adicionadno no array de imports!


@NgModule({
imports: [ BrowserModule, FotoModule, HttpModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

Um novo teste demonstra que o título da nossa aplicação é exibido, o que é uma garantia de que
nossa aplicação está funcionando.

Apesar de funcionar, podemos injetar nossa dependência de uma maneira menos verbosa com o uso
do TypeScript.

3.2 TYPESCRIPT E DEFINIÇÃO DE TIPOS ESTÁTICOS


O TypeScript não possui esse nome à toa. Podemos definir tipos de maneira estática em nossas
variáveis, e um dos benefícios desse processo é que a checagem de tipos nos ajuda a detectar problemas
em tempo de desenvolvimento, inclusive auxiliar editores de textos e IDE's a autocompletarem nosso
código. O Angular pode se beneficiar com o sistema de tipos do TypeScript para identificar nossas
dependências, evitando assim o uso de @Inject , que aliás, recebe o tipo de quem queremos injetar
como parâmetro.

A definição de tipos com TypeScript consiste em adicionar : seguido do tipo da variável, ou seja,
sua classe.

Mais simples do que a forma anterior com @Inject que utilizamos. A partir deste capítulo
usaremos o sistema de tipos do TypeScript para nos auxiliar, e evitar erros.

Como podemos definir o tipo da propriedade fotos da nossa classe AppComponent? Sabemos que
ela é um array que contém objetos, sendo assim, podemos fazer:

20 3.2 TYPESCRIPT E DEFINIÇÃO DE TIPOS ESTÁTICOS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// caelumpic/src/app/app.component.ts
import {Component, Inject} from '@angular/core';
import { Http } from '@angular/http';

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {

title: string = 'Caelum Pic';


fotos: Array<Object> = [];

constructor(http: Http) { }
}

Temos um atributo de visibilidade pública do tipo Array, onde cada elemento é do tipo Object.
Contudo, podemos usar uma sintaxe menos verbosa para indicarmos que temos um array do tipo
Object:
// caelumpic/src/app/app.component.ts
import {Component, Inject} from '@angular/core';
import { Http } from '@angular/http';

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {

title: string = 'Caelum Pic';


fotos: Object[] = [];

constructor(http: Http) { }
}

Sabendo como injetar dependências na definição de classes dos nossos componentes com a ajuda do
framework, em uma sintaxe mais enxuta e com o uso do TypeScript, vamos usar o serviço Http.

3.3 EXERCÍCIO - CONFIGURANDO O SERVIÇO HTTP DO ANGULAR


1.Importe HttpModule em caelumpic/src/app/app.module.ts . Se você usou Angular CLI, este
já deve estar feito!

// caelumpic/src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { FotoModule } from './foto/foto.module';
import { HttpModule } from '@angular/http';

@NgModule({
imports: [ BrowserModule, FotoModule, HttpModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

3.3 EXERCÍCIO - CONFIGURANDO O SERVIÇO HTTP DO ANGULAR 21


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
2.Em AppComponent, adicione os tipos das propriedades title e fotos, crie um construtor passando
http como parâmetro e informando seu tipo. Não esqueça de importar Http:
// caelumpic/src/app/app.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {

title: string = 'Caelum Pic';


fotos: Object[] = [];

constructor(http: Http) { }
}

3.4 CHAMADA DA API REMOTA


Quem cria o back-end disponibiliza uma série de endereços, ou seja, uma série de API's para serem
consumidas por uma aplicação, seja Angular ou Android. É o criador da API que nos informa quais
URL's estão disponíveis e qual verbo utilizar. Em nosso caso, sabemos que é o endereço
localhost:3000/v1/fotos que retorna uma lista de fotos. O navegador obtém essa lista por meio de
um pedido especial, utilizando um verbo padrão do mundo HTTP, o verbo GET.

O serviço Http possui uma função de mesmo nome:

// caelumṕic/src/app/app.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';

@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {

title: string = 'Caelum Pic';


fotos: Object[] = [];

constructor(http: Http) {
http.get('http://localhost:3000/v1/fotos')
}
}

A função http.get recebe como parâmetro o endereço do servidor que desejamos consumir. E o
resultado da função, será nossa lista de fotos? Ainda não, será um fluxo que nos levará até ela! No lugar
de declarar a variável como fluxo, usaremos o termo em inglês, stream:
// caelumpic/src/app/app.component.ts
//código anterior omitidos
export class AppComponent {

22 3.4 CHAMADA DA API REMOTA


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
title: string = 'Caelum Pic';
fotos: Object[] = [];

constructor(http: Http) {
let stream = http.get('http://localhost:3000/v1/fotos')
}
}

3.5 ENTÃO ANGULAR USA RXJS?


Mas o que realmente é esse stream (fluxo)? Ele é proveniente da Reactive Extensions for JavaScript
(RxJS), criadas pela Microsoft Open Technologies Inc em parceria com a comunidade open source. O
RxJS é um conjunto de bibliotecas para compor programas assíncronos e baseados em eventos, usando
coleções observáveis.

Na prática, interagimos com um fluxo observável (observable stream) do RxJS através de suas
funções. Usamos a função subscribe para que possamos "acompanhar", como uma assinatura, os dados
que são retornados. Em nosso caso, a resposta do servidor:
// caelumpic/src/app/app.component.ts
// código anterior omitido
constructor(http: Http) {
let stream = http.get('http://localhost:3000/v1/fotos');
stream.subscribe(function(res){

});
}
// código posterior omitido

A resposta que recebemos ainda não é nossa lista de fotos, mas um objeto no qual solicitamos essa
lista no formato que for interessante para nós. Por exemplo, há a função res.text(), que retorna os dados
como string e a res.json(), que realiza automaticamente o parse para nós do JSON retornado em um
array de objetos:

// caelumpic/src/app/app.component.ts
// código anterior omitido
constructor(http: Http) {
let stream = http.get('http://localhost:3000/v1/fotos');
stream.subscribe(function(res){
this.fotos = res.json();
});
}
// código posterior omitido

Perceba que no exemplo, estamos armazenando a lista de fotos no formato JSON em this.fotos ,
ou seja, na propriedade fotos de uma instância da nossa Foto. Porém, se você é estudioso de
JavaScript, já sabe que isso não funcionará. Cada função em JavaScript define o contexto de seu this, ou
seja, seu valor durante sua execução. Quando acessamos o this no contexto da função subscribe, ele
referenciará Subscriber e não a instância da classe Foto. É por isso que this.foto não existe. Uma
maneira de resolver esse problema do dinamismo do this é a seguinte:
// caelumpic/src/app/app.component.ts

3.5 ENTÃO ANGULAR USA RXJS? 23


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
//código anterior omitido
export class AppComponent {

title: string = 'Caelum Pic';


fotos: Object[] = [];

constructor(http: Http) {

var that = this; // aqui o this do construtor é a instância da classe Foto

let stream = http.get('http://localhost:3000/v1/fotos');


stream.subscribe(function(res){
that.fotos = res.json(); // that é a instância da classe Foto
});
}
}

Veja que guardamos o this da função constructor em uma variável chamada that, poderia ser
qualquer nome. Nesse caso, o contexto de execução é a nossa classe AppComponent. Agora, dentro da
função subscribe, acessamos a variável that, que temos certeza que aponta para a instância da classe
AppComponent.

Vamos testar? Perfeito, nossa lista é exibida! Mas podemos usar um recurso do ES6 que pode nos
poupar essas linhas de código, uma arrow function.

3.6 ARROW FUNCTIONS E ESCOPO LÉXICO


Veja que você consegue enxergar uma flecha (no inglês, arrow) abaixo:

=>

Agora que você já sabe de onde as Arrow Functions tiraram seu nome, vamos utilizá-las em nosso
código:

// caelumpic/src/app/app.component.ts
// código anterior omitido
export class AppComponent {

title: string = 'Caelum Pic';


fotos: Object[] = [];

constructor(http: Http) {

let stream = http.get('http://localhost:3000/v1/fotos');


stream.subscribe(res => {
this.fotos = res.json();
});
}
}

Uma arrow function é uma função anônima que possui uma sintaxe mais curta, quando comparada
com a function expressions que usamos antes. Porém, o seu diferencial não é apenas a sintaxe enxuta:
toda arrow function compartilha o mesmo this léxico de seu escopo pai.

24 3.6 ARROW FUNCTIONS E ESCOPO LÉXICO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Qual é o this de constructor? A instância da classe AppComponent. Qual o this de subscribe agora?
O de constructor. Sendo assim, o this de subscribe será o da instância da classe AppComponent, o
mesmo da sua função pai.

Podemos enxugar ainda mais nosso código, evitando a declaração da variável stream:

// caelumpic/src/app/app.component.ts
// código anterior omitido
constructor(http: Http) {

http.get('http://localhost:3000/v1/fotos')
.subscribe(res => {
this.fotos = res.json();
});
}

Nosso código é funcional, mas o RxJS permite realizar uma série de operações sobre esse fluxo de
maneira encadeada. Que tal já disponibilizarmos para a função subscribe a lista de fotos já parseada?
Podemos fazer isso usando a extensão map:

// caelumpic/src/app/app.component.ts
// código anterior omitido
export class AppComponent {

title: string = 'Caelum Pic';


fotos: Object[] = [];

constructor(http: Http) {

http.get('http://localhost:3000/v1/fotos')
.map(res => res.json())
.subscribe(fotos => {
this.fotos = fotos;
});
}
}

Veja que na função map estamos executando a instrução res.json() . Apesar de não retornarmos
um valor através de return , a lista já parseada está disponível como o primeiro parâmetro da função
subscribe. É por isso que seu parâmetro foi renomeado para fotos, tornando nosso código mais legível.

Porém, um teste revelará que nosso código não funciona. Abrindo o console do browser temos a
mensagem de erro:

ORIGINAL EXCEPTION: http.get(...).map is not a function

A função map não existe em nosso observable stream. Isso acontece porque apenas o core do RxJS é
carregado e se quisermos usar outras extensões, precisamos importá-las em nosso código. Para isso,
vamos temos que adicionar em AppModule a extensão. Esta é um pouco diferente do que fizemos até
agora pois precisamos importar apenas a extensão, sem precisarmos adicioná-la à propriedade imports
do ngModule:

Veja como adicionar a extensão map em AppModule:

3.6 ARROW FUNCTIONS E ESCOPO LÉXICO 25


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// caelumpic/src/app/app.module.ts
import 'rxjs/add/operator/map'; // importou a extensão map!

import { NgModule } from '@angular/core';


import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { FotoModule } from './foto/foto.module';
import { HttpModule } from '@angular/http';

@NgModule({
imports: [ BrowserModule, FotoModule, HttpModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

Agora, se um erro acontecer na conexão com a API? Não saberemos de nada. Por isso é uma boa
prática fazer um log de qualquer erro que aconteça em nossa aplicação. É por essa razão que o segundo
parâmetro da função subscribe é uma função que será chamada apenas quando ocorrer um erro
durante a requisição. Por enquanto vamos apenas exibir no console do navegador, mas futuramente
podemos exibir uma mensagem amigável para o usuário:

// caelumpic/src/app/app.component.ts
// código anterior omitido
constructor(http: Http) {

http.get('http://localhost:3000/v1/fotos')
.map(res => res.json())
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}

3.7 EXERCÍCIO - CONECTANDO COM A API


1.Adicione o Map de RxJS em caelumpic/src/app/app.module.ts:
// caelumpic/src/app/app.module.ts
import 'rxjs/add/operator/map'; // importou a extensão map!
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { FotoModule } from './foto/foto.module';
import { HttpModule } from '@angular/http';

@NgModule({
imports: [ BrowserModule, FotoModule, HttpModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

2.No construtor de AppComponent chame a API usando subscribe:

// caelumpic/src/app/app.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';

26 3.7 EXERCÍCIO - CONECTANDO COM A API


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {

title: string = 'Caelum Pic';


fotos: Object[] = [];

constructor(http: Http) {

http.get('http://localhost:3000/v1/fotos')
.map(res => res.json())
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}
}

3.8 A DIRETIVA NGFOR


Para criar um FotoComponent para cada item da nossa lista de fotos, usamos a diretiva ngFor do
Angular. Atenção, veja que ela começa com asterisco * :
<foto *ngFor="let foto of fotos" url="{{foto.url}}" titulo="{{foto.titulo}}"></foto>

Agora nossa app deve estar neste estágio:

3.9 EXERCÍCIO - EXIBINDO A LISTA DE FOTOS


1.Adicione a diretiva *ngFor em caelumpic/src/app/app.component.html:
<!-- caelumpic/src/app/app.component.html -->
<div class="jumbotron">
<h1 class="text-center">{{title}}</h1>
</div>
<div class="container">
<foto *ngFor="let foto of fotos" url="{{foto.url}}" titulo="{{foto.titulo}}"></foto>
</div>

3.8 A DIRETIVA NGFOR 27


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Figura 3.2: Home da CaelumPic com a lista de fotos da API

28 3.9 EXERCÍCIO - EXIBINDO A LISTA DE FOTOS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 4

DIVIDINDO PARA CONQUISTAR!

Nossa aplicação está ganhando cada vez mais forma, mas ainda temos muito trabalho pela frente. Mas
antes de avançarmos, que tal exibir cada foto dentro de um painel do Bootstrap? A marcação deste
painel não é muito trivial, veja o exemplo abaixo:
<!-- código não entra em nossa aplicação por enquanto, apenas um exemplo -->

<div class="panel panel-default">


<div class="panel-heading">
<h3 class="panel-title text-center">Aqui vem o título do painel</h3>
</div>
<div class="panel-body">
<!-- conteúdo do painel aqui -->
</div>
</div>

Se tornarmos essa marcação um componente, poderemos ter algo do tipo:

<!-- código não entra em nossa aplicação por enquanto, apenas ilustrativo -->
<painel titulo="Titulo do Meu Painel">
<!-- aqui vem o conteúdo do painel -->
</painel>

Já sabemos fazer isso, inclusive é uma boa hora para lembrarmos do que aprendemos.

4.1 EXERCÍCIO - CRIANDO UM COMPONENTE PARA EXIBIR AS FOTOS:


PAINELCOMPONENT
1.Vamos criar um novo componente, o PainelComponent:
// caelumpic/src/app/painel/painel.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent {

@Input() titulo: string;


}

2.E claro, seu template:


<!-- caelumpic/src/app/painel/painel.component.html -->
<div class="panel panel-default">

4 DIVIDINDO PARA CONQUISTAR! 29


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<div class="panel-heading">
<h3 class="panel-title text-center">{{titulo}}</h3>
</div>
<div class="panel-body">
</div>
</div>

3.Lembre-se que é uma boa prática criarmos um módulo para este componente. Vamos criá-lo:

// caelumpic/src/app/painel/painel.module.ts
import { NgModule } from '@angular/core';
import { PainelComponent } from './painel.component';

@NgModule({
declarations: [ PainelComponent ],
exports: [PainelComponent]
})
export class PainelModule { }

4.Agora vamos importar o módulo em AppModule:

// caelumpic/src/app/app.module.ts
import 'rxjs/add/operator/map'; // importou a extensão map!

import { NgModule } from '@angular/core';


import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';


import { FotoModule } from './foto/foto.module';
import { PainelModule } from './painel/painel.module';

@NgModule({
imports: [
BrowserModule,
FotoModule,
HttpModule,
PainelModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

5.Por fim, precisamos alterar caelumpic/src/app/app.component.html para que faça uso do painel.
Aliás, quando formos iterar na lista de fotos, criaremos um painel para cada foto. Sendo assim, a diretiva
ngFor sai de <foto> e passa para <painel> :

<!-- caelumpic/src/app/app.component.html -->


<div class="jumbotron">
<h1 class="text-center">{{title}}</h1>
</div>
<div class="container">
<painel *ngFor="let foto of fotos" titulo="{{foto.titulo}}">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div>

4.2 EXERCÍCIO - SOCORRO, MEU PAINEL NADA EXIBE!

30 4.2 EXERCÍCIO - SOCORRO, MEU PAINEL NADA EXIBE!


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Ops! Ao testar, vemos que apenas o título do painel é preenchido e que todo seu conteúdo é
removido.

Figura 4.1: Paineis da Caelumpic vazios

1.Para indicar em que parte do componente Painel precisa ser exibido o conteúdo, que neste caso é o
componente Foto, adicione a tag <ng-content> no seu template:
<!-- caelumpic/src/app/painel/painel.component.html -->

<div class="panel panel-default">


<div class="panel-heading">
<h3 class="panel-title text-center">{{titulo}}</h3>
</div>
<div class="panel-body">
<ng-content></ng-content>
</div>
</div>

Agora sim, nosso painel mantém seu conteúdo, exibindo todos os dados das fotos. Porém, podemos
melhorar ainda mais o visual da página.

2.Para melhorar ainda mais a apresentação das nossas fotos usando o componente Painel vamos
usar o sistema de grid do Bootstrap, adicione a classe col-md-2 no componente.

<!-- caelumpic/src/app/app.component.html -->


<div class="jumbotron">
<h1 class="text-center">{{title}}</h1>
</div>
<div class="container">
<div class="row">
<painel *ngFor="let foto of fotos" titulo="{{foto.titulo}}" class="col-md-2">

4.2 EXERCÍCIO - SOCORRO, MEU PAINEL NADA EXIBE! 31


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div>
</div>

Recarregando nossa página, temos um visual muito mais interessante!

Figura 4.2: Caelumpic com painel e grid do Bootstrap

4.3 MAIS DE UMA PÁGINA


Muito bem, até agora criamos o componente AppComponent, que nada mais é do que a página
principal da nossa aplicação, aquela que lista todas as nossas fotos. Talvez você ainda não tenha se
acostumado com a ideia de uma página ser um componente, mas até o final da apostila essa ideia vai se
acomodando.

Falando em página, cedo ou tarde precisaremos criar a página de cadastro de fotos, ou seja, um novo
componente. Vamos criá-lo logo, mas exibindo apenas o título da página, ainda sem qualquer
formulário.

4.4 EXERCÍCIO - PÁGINA DE CADASTRO DE FOTOS


1.Vamos criar o componente Cadastro dentro de uma nova pasta cadastro:

// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent { }

2.E claro, o seu template:


<!-- caelumpic/src/app/cadastro/cadastro.component.html -->

32 4.3 MAIS DE UMA PÁGINA


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<h1 class="text-center">Cadastro de Fotos</h1>

Um ponto que vamos melhorar também é que extrairemos de AppComponent todo o seu código, e
passaremos ele para o novo componente ListagemComponent. A ideia é que AppComponent fique
com seu template vazio por enquanto.

3.Criar o componente Listagem, em uma pasta listagem, com o constructor que fizemos em
AppComponent, injetando o Http :
// caelumpic/src/app/listagem/listagem.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';

@Component({
selector: 'listagem',
templateUrl: './listagem.component.html'
})
export class ListagemComponent {

fotos: Object[] = [];

constructor(http: Http) {

http.get('http://localhost:3000/v1/fotos')
.map(res => res.json())
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}

3.O template do componente Listagem recebe o conteúdo que estava no template do


AppComponent:
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<div class="jumbotron">
<h1 class="text-center">Caelum Pic</h1>
</div>
<div class="container">

<div class="row">
<painel *ngFor="let foto of fotos" titulo="{{foto.titulo}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}">
</foto>
</painel>
</div><!-- fim row -->
</div>

4.Remover todo o conteúdo do template caelumpic/src/app/app.component.html, deixando-o


vazio.

5.Em AppComponent manter apenas a estrutura básica:


// caelumpic/src/app/app.component.ts
import { Component } from '@angular/core';

4.4 EXERCÍCIO - PÁGINA DE CADASTRO DE FOTOS 33


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
}

Outro ponto a destacar é que desta vez não criamos o CadastroModule e ListagemModule. A razão
disso é que os componentes serão importados direto no AppModule. Nós apenas criamos as subpastas
cadastro e listagem dentro da pasta app para evitar ficar com muitos arquivos em sua raiz.

6.E por fim, precisamos adicionar CadastroComponent e ListagemComponent nas


declarations de AppModule, para que o componente faça parte do módulo:

// caelumpic/src/app/app.module.ts
import 'rxjs/add/operator/map';

import { NgModule } from '@angular/core';


import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';


import { FotoModule } from './foto/foto.module';
import { PainelModule } from './painel/painel.module';
import { CadastroComponent } from './cadastro/cadastro.component';
import { ListagemComponent } from './listagem/listagem.component';

@NgModule({
imports: [
BrowserModule,
FotoModule,
HttpModule,
PainelModule ],
declarations: [
AppComponent,
CadastroComponent,
ListagemComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

Agora, a pergunta que não quer calar: hoje, em AppModule, o primeiro componente que
carregamos no bootstrap do módulo é o AppComponent, mas veja que seu template se encontra
vazio!
Se trocarmos este componente por CadastroComponent, iremos exibir a página de cadastro assim que
nossa página for carregada, e não é isso que queremos.
Aliás, queremos que o usuário acesse determinada URL e ora carregue o componente
CadastroComponent, ora ListagemComponent!

Como fazer isso? "Houston, we have a problem"!

Para podermos resolver esse problema, precisamos entender o motivo pelo qual o Angular é um
framework voltado para a criação de Single Page Applications.

34 4.4 EXERCÍCIO - PÁGINA DE CADASTRO DE FOTOS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
4.5 SINGLE PAGE APPLICATION (SPA)
Uma Single Page Application (SPA) ou "Aplicação de Página Única" no português, é aquela que não
recarrega durante o uso e geralmente já carrega todos os scripts e estilos (CSS) que precisa no seu
primeiro carregamento. O usuário pode realizar uma série de tarefas sem a necessidade da página
recarregar.

Se você é novo nessa abordagem, deve estar pensando: "Como assim não recarrega? Quer dizer que
colocamos o conteúdo de todas as páginas em uma só? Não ficará confuso?". Sim, seria confuso se fosse
isto, porém em uma SPA não colocamos tudo em uma página apenas, criamos algo semelhante a
páginas para realizar essa separação.

Em SPA, a página index.html é carregada com todos os scripts e estilos, mas mediante as ações do
usuário, outras páginas são inseridas dinamicamente no corpo de index.html, através de Ajax. Quando
o usuário acessar a página B.html, por exemplo, realizamos uma requisição Ajax para essa página e
manipulamos o DOM de index.html para inserir o conteúdo de B.html. Se acessarmos a página C.html,
removemos o conteúdo de B.html e inserimos o conteúdo de C.html.
Por que dizemos que ela é "semelhante"? Porque essas páginas não possuem as tags head , nem body ,
e, para serem exibidas precisam ser incluídas como conteúdo da página index.html.

Os exemplos de SPA mais populares são o Gmail e o Inbox: quando você apaga um e-mail ou inicia a
escrita de um novo, a sua página recarrega (fica em branco aguardando carregamento)? Claro que não.

Pode parecer trabalhoso realizar todo esse trabalho, mas SPA's fornecem uma experiência do usuário
parecida com aplicativos nativos, como o Gmail. Dependendo da sua aplicação, esse comportamento
pode ser interessante. Bom, falar é fácil, vamos ver como criar uma SPA com Angular então!

4.6 EXERCÍCIO - CONFIGURANDO ROTAS


Angular possui um módulo criado para ajudar nesse processo de mudança de página, o
RouterModule.

1.O primeiro passo será criar o arquivo caelumpic/src/app/app.routes.ts . É nesse arquivo que
teremos centralizadas as configurações de rotas da nossa aplicação. Vamos aproveitar e importar os
componentes ListagemComponent e CadastroComponent :
// caelumpic/src/app/app.routes.ts
import { RouterModule } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';

Mas onde está a configuração de nossas rotas?

2.Vamos declarar um array com duas configurações. Essas configurações são objetos do tipo
Routes . Sendo assim, vamos importar a classe Routes e tipar o array:

4.5 SINGLE PAGE APPLICATION (SPA) 35


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// caelumpic/src/app/app.routes.ts
import { RouterModule, Routes } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';

const appRoutes: Routes = [


{ path: '', component: ListagemComponent },
{ path: 'cadastro', component: CadastroComponent }
];

Veja que cada objeto passado para o array appRoutes possui a mesma estrutura, isto é, as mesmas
propriedades. No primeiro, quando usamos '' como valor de path , estamos indicando que
responderemos à URL localhost:4200/ . Para esse caminho, o componente ListagemComponent
será carregado. Já para o caminho cadastro , o componente CadastroComponente será carregado
quando a URL acessada for localhost:4200/cadastro . Só que essa configuração ainda não é
suficiente.

3.Precisamos pedir ao módulo RouterModule que construa nossas rotas com base na configuração
definida em appRoutes. É o resultado dessa operação que exportaremos. Faremos isso pelo método
RouterModule.forRoot :

// caelumpic/src/app/app.routes.ts
import { RouterModule, Routes } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';

const appRoutes: Routes = [


{ path: '', component: ListagemComponent },
{ path: 'cadastro', component: CadastroComponent }
];

export const routing = RouterModule.forRoot(appRoutes);

4.Agora precisamos importar o routing de app.routing.ts como um módulo em AppModule:


// caelumpic/src/app/app.module.ts
import 'rxjs/add/operator/map';

import { NgModule } from '@angular/core';


import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';


import { FotoModule } from './foto/foto.module';
import { PainelModule } from './painel/painel.module';
import { CadastroComponent } from './cadastro/cadastro.component';
import { ListagemComponent } from './listagem/listagem.component';
import { routing } from './app.routes';

@NgModule({
imports: [
BrowserModule,
FotoModule,
HttpModule,
PainelModule,
routing ],
declarations: [

36 4.6 EXERCÍCIO - CONFIGURANDO ROTAS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
AppComponent,
CadastroComponent,
ListagemComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

Bom após uma série de alterações, qual será o papel de AppComponent, já que ele ficou desprovido
de qualquer template? Seu template que servirá como receptáculo para os componentes carregados
condicionalmente através das rotas da aplicação.

5.Para isso, precisamos adicionar em caelumpic/src/app/app.component.html a diretiva


router-outlet :

<!-- caelumpic/src/app/app.component.html -->


<router-outlet></router-outlet>

Outro detalhe importante para que o sistema de rotas funcione é ter a tag base no index.html,
detalhe que o Angular CLI já resolveu para nós.

CASO VOCÊ NÃO TENHA USADO O ANGULAR CLI

Ao abrir os endereços, você verá que nada carrega.


http://localhost:4200/listagem
http://localhost:4200/cadastro

Acessando o log do console do navegador, o próprio Angular reclama da ausência da tag base :
EXCEPTION: No base href set. Please provide a value for the
APP_BASE_HREF token or add a base element to the document.

A tag base é importante quando usamos o sistema de rotas do Angular 2, e deve ser
adicionada no index.html:
<!-- caelumpic/src/index.html -->

<!doctype html>
<html>
<head>
<base href="/"> <!-- novidade aqui -->
<title>Caelumpic</title>
<meta charset="UTF-8">
<!-- código posterior omitido -->

Após isso, salve e teste novamente.

Muito bem! vamos recapitular:

1. Quando nossa aplicação Angular iniciar, o primeiro componente a ser carregado será o
AppComponent.

4.6 EXERCÍCIO - CONFIGURANDO ROTAS 37


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
2. O AppComponent que configura as rotas da aplicação, aponta os endereços aos componentes
ListagemComponent e CadastroComponent.
3. Quando determinada URL for acessada pelo navegador, seu respectivo componente deve ser
carregado e exibido para o usuário.

4.7 EXERCÍCIO - REDIRECIONAR CAMINHOS INEXISTENTES


Veja que, para cada endereço acessado, seu respectivo componente é renderizado. Mas o que
acontece se acessarmos um endereço que não existe? Por exemplo:
http://localhost:4200/teste

Nada é exibido, porém isso não é o ideal. Podemos adicionar uma rota que só será ativada caso o
endereço acessado não exista. Neste caso, podemos redirecionar a rota para / . Com isso, o usuário
sempre será levado para ListagemComponent quando acessar uma rota que não existe:

1.Adicione o mais um objeto em appRoutes , usando a sintaxe de path especial ** e, no lugar de


usarmos o atributo component para indicar qual componente deve ser renderizado, usaremos
redirectTo. Veja que a propriedade recebe o valor '' . Esse valor é uma rota já existente, aquela que
carrega o componente ListagemComponent .

// caelumpic/src/app/app.routes.ts
import { RouterModule, Routes } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';

const appRoutes: Routes = [


{ path: '', component: ListagemComponent },
{ path: 'cadastro', component: CadastroComponent },
{ path: '**', redirectTo: ''}
];

export const routing = RouterModule.forRoot(appRoutes);

4.8 EXERCÍCIO - LINKS PARA ROTAS


Muito bem, até agora estávamos acessando as rotas digitando-as diretamente no browser. Como
fazemos isso através, por exemplo, de um link em nossa página?

1.Vamos adicionar o botão Nova foto , na verdade, uma tag <a> com classes do Bootstrap:
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<div class="jumbotron">
<h1 class="text-center">Caelumpic</h1>
</div>

<div class="container">

<div class="row">
<div class="col-md-12">
<form>

38 4.7 EXERCÍCIO - REDIRECIONAR CAMINHOS INEXISTENTES


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<div class="input-group">
<span class="input-group-btn">
<a href="/cadastro" class="btn btn-primary">
Nova foto
</a>
</span>
</div>
</form>
</div> <!-- fim col-md-12 -->
</div> <!-- fim row -->
<br>
<div class="row">
<painel *ngFor="let foto of fotos" titulo="{{foto.titulo}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div><!-- fim row -->

</div><!-- fim container -->

Figura 4.3: Botão para acessar o cadastro de fotos

Excelente. Quando recarregamos nossa página, nosso botão é exibido. Quando ele é clicado, o
componente Cadastro é recarregado, porém com um detalhe. Veja que a mensagem Carregando...
é exibida. Isso não deveria acontecer, pois ela só é exibida quando estamos carregando nossa aplicação
pela primeira vez. Ao que tudo indica, nosso link está fazendo com que a aplicação inteira seja
recarregada, para daí carregar o componente.

2.Para resolver esse problema, basta usarmos a diretiva RouterLink . Como ela é somente leitura,
fica entre colchetes:

<!-- caelumpic/src/app/listagem/listagem.component.html -->


<div class="jumbotron">
<h1 class="text-center">Caelumpic</h1>
</div>

<div class="container">

<div class="row">
<div class="col-md-12">

4.8 EXERCÍCIO - LINKS PARA ROTAS 39


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<form>
<div class="input-group">
<span class="input-group-btn">
<a [routerLink]="['/cadastro']" class="btn btn-primary">
Nova foto
</a>
</span>
</div>
</form>
</div> <!-- fim col-md-12 -->
</div> <!-- fim row -->
<br>
<div class="row">
<painel *ngFor="let foto of fotos" titulo="{{foto.titulo}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div><!-- fim row -->

</div><!-- fim container -->

40 4.8 EXERCÍCIO - LINKS PARA ROTAS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 5

MELHORANDO A EXPERIÊNCIA DO
USUÁRIO

As fotos vêm do nosso servidor, mas na hora de exibi-las quero que o título seja exibido em caixa-alta,
em uppercase. Como resolver? Alterar os dados que chegaram através de http em nosso componente?
Não, porque se alterarmos lá, estaremos mudando o dado e não queremos mudar a informação, apenas
sua apresentação. Queremos que o título da foto passe por um tubo e dentro dele seja processado para
que na outra extremidade tenhamos o título em caixa alta.

5.1 PIPES, TUBOS QUE TRANSFORMAM!


No mundo da programação, o tubo (pipe) geralmente é representado pelo caractere | .

Para usarmos o pipe, adicionamos ele na Angular Expression {{foto.titulo}} no template do


nosso componente ListagemComponent da seguinte maneira:
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<!-- código anterior omitido -->
<div class="row">
<painel *ngFor="let foto of fotos" titulo="{{foto.titulo |}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

Adicionamos um pipe que gerará uma transformação no título da foto, porém não informamos qual
transformação queremos que ocorra dentro do tubo (pipe). O Angular já vem por padrão com algumas
transformações, e a que vamos usar é a uppercase , palavra que deve ser adicionada logo após nosso
pipe:
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<!-- código anteriorr omitido -->
<div class="row">
<painel *ngFor="let foto of fotos" titulo="{{foto.titulo | uppercase}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

Veja o resultado:

5 MELHORANDO A EXPERIÊNCIA DO USUÁRIO 41


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Figura 5.1: Uppercase - Transformação do texto

5.2 VARIÁVEIS LOCAIS


Hoje nosso componente ListagemComponent exibe uma lista de fotos, uma lista não tão grande.
Contudo, imagine que com o tempo o número de fotos vá aumentando.

Como faremos para encontrar determinada foto? Tudo bem que podemos procurar uma a uma, mas
podemos melhorar nossa experiência permitindo filtrar a exibição das fotos pelo título. Eu quero ser
capaz de digitar Fute e apenas as fotos que contenham como parte do seu título o texto Fute sejam
exibidas.

Queremos filtrar a nossa lista de fotos, por isso vamos adicionar um | para filtrar a lista recebida
em nosso ngFor :

<!-- caelumpic/src/app/listagem/listagem.component.html -->


<!-- código anterior omitido -->
<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo" titulo="{{foto.titulo}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

Utilizaremos o pipe filtroPorTitulo , que será aplicado na lista utilizada para diretiva ngFor .
Porém, o filtro precisa saber pelo o que filtrar, para isto precisamos informar os parâmetros do filtro:
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<!-- código anterior omitido -->
<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo:textoProcurado.value" titulo="{{foto.titulo}}"
class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

Nosso filtroPorTitulo filtrará a lista de fotos, exibindo aquelas que contenham parte do texto
definido na variável textoProcurado.value . Entretanto, nem nosso filtro, nem a variável existem!
Precisamos criá-los.

No Angular, podemos criar variáveis locais diretamente no template da diretiva da seguinte maneira:

<!-- caelumpic/src/app/listagem/listagem.component.html -->


<!-- código anterior omitido -->
<input class="form-control" #textoProcurado placeholder="filtrar pelo título da foto">

42 5.2 VARIÁVEIS LOCAIS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<!-- código posterior omitido -->

É por meio do prefixo # que criamos variáveis locais. A variável #textoProcurado armazena o
elemento no qual ela foi adicionada, ou seja, ela guarda nosso input . Sendo uma variável local,
podemos acessá-la em qualquer lugar do nosso template. Veja que ela é diferente da variável criada com
let na diretiva ngFor , que possui como escopo apenas o elemento no qual a diretiva foi associada.

5.3 EXERCÍCIO: INICIANDO NOSSO PIPE


1.Vamos adicionar um campo de busca já com a variável #textoProcurado que guarda nosso
elemento, ao lado do botão:
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<div class="jumbotron">
<h1 class="text-center">Caelum Pic</h1>
</div>

<div class="container">

<div class="row">
<div class="col-md-12">
<form>
<div class="input-group">
<span class="input-group-btn">
<a [routerLink]="['/cadastro']" class="btn btn-primary">
Nova foto
</a>
</span>

<!-- campo para filtrar pelo titulo -->


<input #textoProcurado class="form-control" placeholder="filtrar pelo título da f
oto">
</div>
</form>
</div> <!-- fim col-md-12 -->
</div> <!-- fim row -->
<br>
<div class="row">
<painel *ngFor="let foto of fotos" titulo="{{foto.titulo}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div><!-- fim row -->
</div>

2.Agora vamos adicionar em *ngFor o pipe filtroPorTitulo que vai filtrar a lista de paineis,
pelo valor passado em #textoProcurado :
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<!-- código anterior omitido -->
<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo:textoProcurado.value" titulo="{{foto.titulo}}"
class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

5.3 EXERCÍCIO: INICIANDO NOSSO PIPE 43


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
5.4 CRIANDO UM PIPE
Vamos criar um novo arquivo que guarda a lógica do nosso filtro, mas em qual pasta da nossa
aplicação? Como esse filtro só faz sentido ser aplicado em uma lista de Fotos vamos criá-lo dentro da
pasta caelumpic/src/app/foto/foto.pipes.ts .
// caelumpic/src/app/foto/foto.pipes.ts
import {Pipe} from '@angular/core';
@Pipe({
name: 'filtroPorTitulo'
})
export class FiltroPorTitulo {

transform(fotos, digitado) {
console.log(fotos); // quem deve ser filtrado
console.log(digitado); // o que deve ser usado como filtro
}
}

Todo pipe criado por nós deve conter o método transform, nosso caso este método receberá a lista
de fotos que desejamos filtrar e como segundo parâmetro o critério do filtro. Ainda não implementamos
nosso método totalmente, estamos apenas exibindo os dados via console para entendermos seus
parâmetros.

5.5 EXERCÍCIO: PIPE, INICIANDO A IMPLEMENTAÇÃO


1.Crie a classe com a estrutura básica do filtro:

// caelumpic/src/app/foto/foto.pipes.ts
import { Pipe } from '@angular/core';
@Pipe({
name: 'filtroPorTitulo'
})
export class FiltroPorTitulo {

transform(fotos, digitado) {
console.log(fotos);
console.log(digitado);
}
}

2.Agora, precisamos declarar nosso FiltroPorTitulo como fazendo parte do módulo


FotoModule . Inclusive, se quisermos que outros módulos tenham acesso ao nosso pipe também
precisamos adicioná-la no array de exports :
// caelumpic/src/app/foto/foto.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FotoComponent } from './foto.component';
import { FiltroPorTitulo } from './foto.pipes';

@NgModule({
imports: [ CommonModule ],
declarations: [ FotoComponent, FiltroPorTitulo ],
exports: [FotoComponent, FiltroPorTitulo]

44 5.4 CRIANDO UM PIPE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
})
export class FotoModule { }

Excelente, nossa lista de fotos ainda não está sendo exibida, porém conseguimos ver no console do
browser as informações que enviamos. Contudo, o que acontece se tivéssemos escrito o nome do método
errado? Só descobrimos um bug em nosso código depois dele estar rodando. Como o TypeScript poder
nos ajudar neste tipo de situação?

5.6 QUE TAL UM INTERFACE TYPE?


O TypeScript não sabe que a classe do nosso Pipe deve implementar o método transform do
Angular, muito menos que este método deve receber dois parâmetros.

Para isto usamos interface type para ajudar a implementar uma classe do tipo Pipe corretamente, ou
seja, com o método transform e seus dois parâmetros.

// caelumpic/src/app/foto/foto.pipes.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filtroPorTitulo'
})
export class FiltroPorTitulo implements PipeTransform {

transform(fotos, digitado) {
console.log(fotos); // quem deve ser filtrado
console.log(digitado); // o que deve ser usado como filtro
}
}

Importamos em nosso código a interface PipeTransform , inclusive fizemos com que nossa classe
implemente essa interface. Uma interface no TypeScript é um arquivo que indica as obrigações
(contratos) que uma classe deve ter. Na definição da interface PipeTransform, criada pela equipe do
Angular, toda classe que assinar esse contrato, isto é, que implemente esta interface, obrigatoriamente
deve ter o método transform , caso contrário o TypeScript não compilará o arquivo e exibirá no seu
console que ferimos essa obrigatoriedade (quebra de um contrato).

5.7 EXERCÍCIO: IMPLEMENTANDO UMA INTERFACE


1.Importe o PipeTransform , implemente ele na classe FiltroPorTitulo , e informe os tipos dos
parâmetros e do retorno:
// caelumpic/src/app/foto/foto.pipes.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filtroPorTitulo'
})
export class FiltroPorTitulo implements PipeTransform {
// defininado os tipos de todos parâmetros, inclusive o retorno de transform
transform(fotos: any, digitado: string): any {
console.log(fotos); // quem deve ser filtrado
console.log(digitado); // o que deve ser usado como filtro

5.6 QUE TAL UM INTERFACE TYPE? 45


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
}
}

2.(Opcional) Mude propositalmente o nome do método para transformA e confira o erro de


implementação de interface retornado pelo transpilador do TypeScript:
// caelumpic/src/app/foto/foto.pipes.ts(5,14): error TS2420: Class 'FiltroPorTitulo' incorrectly impl
ements interface 'PipeTransform'.
Property 'transform' is missing in type 'FiltroPorTitulo'.

Veja que o TypeScript está nos informando que nossa classe FiltroPorTitulo incorretamente
implementa PipeTransform e que transform está faltando. Depois deste teste, volte o nome do
método para transform .

5.8 DEFINIR TIPOS PARA USAR AUTOCOMPLETE


Para ativarmos o recurso do autocomplete durante a escrita do nosso código, é necessário definir os
tipos das variáveis, métodos e retornos em nosso código. Por isto vamos vamos fazer este trabalho de
definir os tipos dos parâmetros do método transform .

O primeiro parâmetro deve ser um array de FotoComponent e o segundo uma string. Como o
primeiro tipo foi criado por nós, precisamos importar sua classe.

Como o nosso compilador TypeScript conhece os tipos, será possível usar o autocomplete. Porém, o
mesmo não será possível para foto.titulo . Como não definimos tipos dos atributos da classe Foto ,
o TypeScript não poderá ajudar nosso editor.

5.9 EXERCÍCIO: PIPE, IMPLEMENTANDO A LÓGICA


1.Altere o método transform de FiltroPorTitulo:

// caelumpic/src/app/foto/foto.pipes.ts
import { Pipe, PipeTransform } from '@angular/core';
import { FotoComponent } from './foto.component';
@Pipe({
name: 'filtroPorTitulo'
})
export class FiltroPorTitulo implements PipeTransform {

transform(fotos: FotoComponent[], digitado: string) {


digitado = digitado.toLowerCase();
return fotos.filter( foto => foto.titulo.toLowerCase().includes(digitado));
}
}

Com poucas linhas de código conseguimos implementar a seguinte lógica:

1. Se temos a lista de fotos e o filtro, podemos filtrar a lista retornando apenas as fotos que tenham
como título parte do que digitamos.
2. Se textoProcurado possui valor (usuário digitou algo), filtramos a lista por este valor, mas se

46 5.8 DEFINIR TIPOS PARA USAR AUTOCOMPLETE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
textoProcurado estiver em branco (sem valor definido), retornamos a lista de fotos completa.

2.Agora ajuste os tipos de FotoComponent para o autocomplete funcionar:

// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {

@Input() titulo: string;


@Input() url: string;
}

5.10 EVENT BINDING PARA ATUALIZAR A VIEW


Elaboramos todo nosso código, mas quando digitamos no campo de filtro a lista de fotos não é
filtrada. O que está acontecendo? Os dados estão chegando no filtro e ele parece estar funcionando
perfeitamente. Certamente é um problema na atualização da view.

Isso acontece porque o Angular só atualizará a view em resposta a um evento assíncrono. Quando
digitamos no campo, não estamos disparando evento algum. Para resolver isto podemos disparar um
evento sem callback mas mas que sinalizará para o Angular atualizar a view.

<!-- caelumpic/src/app/listagem/listagem.component.html -->


<!-- código anterior omitido -->
<input class="form-control" #textoProcurado (keyup)="0" placeholder="filtrar pelo título da foto">
<!-- código posterior omitido -->

Veja que estamos envolvendo o atributo keyup entre parênteses, a sintaxe do Angular 2 para
associação de eventos, ou no inglês event binding. Toda vez que digitarmos no campo, o evento keyup ,
será disparado e mesmo executando nenhuma lógica (seu valor está com 0 ), fará com que o Angular
atualize nossa view e assim nosso filtro começa a funcionar.

5.11 EXERCÍCIO: FAZENDO O FILTRO FUNCIONAR


1.Adicione o evento keyup no input :

<!-- caelumpic/src/app/listagem/listagem.component.html -->


<!-- código anterior omitido -->
<input #textoProcurado (keyup)="0" class="form-control" placeholder="filtrar pelo título da foto">
<!-- código posterior omitido -->

2.Adicione um tipo no retorno do método transform :

// caelumpic/src/app/foto/foto.pipes.ts
import {Pipe, PipeTransform} from '@angular/core';
import { FotoComponent } from './foto.component';

5.10 EVENT BINDING PARA ATUALIZAR A VIEW 47


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
@Pipe({
name: 'filtroPorTitulo'
})
export class FiltroPorTitulo implements PipeTransform {

transform(fotos: FotoComponent[], digitado: string): FotoComponent[] {

digitado = digitado.toLowerCase();
return fotos.filter( foto => foto.titulo.toLowerCase().includes(digitado));
}
}

É recomendável definir um tipo para o retorno, para caso em algum momento necessitar guardar o
retorno do método transform em uma variável, e se esta não for do tipo Array de fotos, o compilador
do TypeScript nos avisará.

48 5.11 EXERCÍCIO: FAZENDO O FILTRO FUNCIONAR


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 6

CADASTRANDO NOVAS IMAGENS

Figura 6.1: tela de cadastro

6.1 EXERCÍCIO - FORMULÁRIO DE CADASTRO


1.Substitua todo conteúdo do template de CadastroComponent adicionando o formulário
completo:
<!--caelumpic/src/app/cadastro/cadastro.component.html-->
<div class="container">
<form class="row">
<div class="col-md-6">
<div class="form-group">
<label>Título</label>
<input class="form-control">
</div>
<div class="form-group">
<label>URL</label>
<input class="form-control">
</div>
<div class="form-group">
<label>Descrição</label>
<textarea class="form-control">
</textarea>
</div>

<button type="submit" class="btn btn-primary">

6 CADASTRANDO NOVAS IMAGENS 49


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Salvar
</button>
<a [routerLink]="['']" class="btn btn-primary">Voltar</a>
</div>
</form>
</div>

6.2 DATA-BINDING RESOLVE?


Com o formulário pronto, precisamos capturar a entrada do usuário para então pensar na solução de
envio para o servidor. Na definição de CadastroComponent vamos adicionar a propriedade foto , um
objeto que receberá os dados inseridos pelo usuário:
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

// tipando a propriedade como Object


foto: Object = {};
}

Precisamos agora realizar uma associação dos input's do formulário com CadastroComponent, mais
notadamente na propriedade foto . Já aprendemos a realizar data-binding, tanto com Expression
Language ou com a sintaxe especial (que usa o atributo entre colchetes). Para poluir menos a marcação
da nossa página, usaremos segunda forma. Mas em qual atributo dos input's? Sabemos essas tag's de
entrada guardam seu valor no atributo value . Achamos o alvo da nossa associação:
<!--caelumpic/src/app/cadastro/cadastro.component.html-->
<div class="container">
<form class="row">
<div class="col-md-6">
<div class="form-group">
<label>Título</label>
<input [value]="foto.titulo" class="form-control">
</div>
<div class="form-group">
<label>URL</label>
<input [value]="foto.url" class="form-control">
</div>
<div class="form-group">
<label>Descrição</label>
<textarea [value]="foto.descricao" class="form-control">
</textarea>
</div>

<button type="submit" class="btn btn-primary">


Salvar
</button>
<a [routerLink]="['']" class="btn btn-primary">Voltar</a>
</div>
</form>
</div>

50 6.2 DATA-BINDING RESOLVE?


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Quando recarregamos nossa página, nosso formulário mostra em todos os campos o texto
undefined :

Figura 6.2: undefined em todos os campos do formulário

Quando nosso formulário é carregado, através da associação de dados ele tenta ler as propriedade
titulo , url e descricao do objeto foto do componente Cadastro . No entanto, este objeto não
possui essas propriedades o que resulta no valor undefined . Como resolver?

Podemos adicionar essas propriedades no objeto foto de Cadastro e preencher com uma string
em branco todos os seus valores:

// caelumpic/src/app/cadastro/components/cadastro.ts
import { Component } from '@angular/core';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: Object = {
titulo: '',
url: '',
descricao: ''
};
}

Muito bom, agora nosso formulário é exibido com todos os campos em branco, mas vamos refletir
nessa solução. Primeiro, nosso editor de texto não consegue autocompletar foto por que seu tipo é

6.2 DATA-BINDING RESOLVE? 51


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Object . Além disso, em todo lugar que precisarmos que uma foto tenha sempre um título, url e
descrição teremos que lembrar de garantir essa estrutura.

E se conseguíssemos o autocomplete e ainda deixarmos de ter a preocupação de sempre garantir uma


estrutura comum para todos os objetos que representam uma foto? Saiba que já temos tudo no lugar
para conseguir isso.

Primeiro, veja a definição do nosso componente Foto :


// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {

@Input() titulo: string;


@Input() url: string;
descricao: string;
}

E se no lugar de adicionarmos no objeto foto de CadastroComponent todas as propriedades que


uma foto deve ter e passarmos a usar em seu lugar uma instância de FotoComponent ? Não
precisaríamos nos preocupar com adicionar as propriedades, e ainda poderíamos pegar carona no
tipagem do TypeScript e usufruir do autocomplete do nosso editor.

Já adicionamos nos capítulos anteriores a propriedade descricao em FotoComponent . Contudo


ela não usou o decorator Input pois não queremos que a descrição seja passada para nosso
componente através de <foto descricao="descricao qualquer"></foto> .
// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {

@Input() titulo: string = '';


@Input() url: string = '';
descricao: string = '';
}

Veja que já inicializamos com uma string vazia cada propriedade para evitar que undefined seja
exibida no formulário. Agora, vamos alterar CadastroComponent e alterar o tipo da propriedade foto
de Object para FotoComponent :
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component, Input } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';

52 6.2 DATA-BINDING RESOLVE?


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


}

Perfeito! Usamos uma instância de Foto que receberá a entrada do usuário pelo nosso formulário.
Se, mais tarde, for necessário adicionar mais uma propriedade na classe Foto , só precisaremos
acrescentá-la em um lugar e todas as instâncias da classe terão a nova propriedade.

A questão agora é saber se nosso objeto foto em Cadastro está sendo atualizado com os dados
inseridos no formulário. Isso é importante, pois precisamos do objeto atualizado para enviá-lo para o
servidor.

Sabemos que o envio de dados para o servidor só pode ser feito quando o formulário for submetido.
Neste momento, podemos imprimir os dados de foto no console do browser e verificar se foram
atualizados para depois pensarmos no seu envio. Mas onde escreveremos esse código?

Lembre-se que um componente é dotado de dados, apresentação e comportamento. Chegou a hora


de criarmos o primeiro comportamento do nosso componente, o de cadastrar.

6.3 EXERCÍCIO - ONE-WAY DATA-BINDING


1.Altere CadastroComponent para receber uma instância de FotoComponent

// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component, Input } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();

cadastrar() {

console.log(this.foto);
}
}

2.Ajuste os tipos das propriedades de FotoComponent e adicione a propriedade descricao :

// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html'

6.3 EXERCÍCIO - ONE-WAY DATA-BINDING 53


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
})
export class FotoComponent {

@Input() titulo: string = '';


@Input() url: string = '';
descricao: string = '';
}

3.Adicione o atributo value com a notação do one-way data-binding e seu valor acessando as
propriedades do objeto foto:
<!--caelumpic/src/app/cadastro/cadastro.component.html-->
<div class="container">
<form class="row">
<div class="col-md-6">
<div class="form-group">
<label>Título</label>
<input [value]="foto.titulo" class="form-control">
</div>
<div class="form-group">
<label>URL</label>
<input [value]="foto.url" class="form-control">
</div>
<div class="form-group">
<label>Descrição</label>
<textarea [value]="foto.descricao" class="form-control">
</textarea>
</div>

<button type="submit" class="btn btn-primary">


Salvar
</button>
<a [routerLink]="['']" class="btn btn-primary">Voltar</a>
</div>
</form>
</div>

Adicionamos o método cadastrar na definição da classe CadastroComponent que apenas exibe o


valor da propriedade foto quando chamado. Porém, como nossa aplicação saberá quando chamar o
método?

6.4 ASSOCIAÇÃO DE EVENTOS (EVENT-BINDING)


Bem, toda vez que o formulário for submetido, ou seja, quando botão do tipo submit de um
formulário for clicado. Se estivéssemos lidando com vanilla Javascript (Javascript puro) poderíamos
adicionar o código que exibe os dados da foto no evento submit . Em Angular, podemos ter acesso ao
mesmo evento, mas envolvendo-o com parênteses:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<form (submit)="cadastrar()" class="row">
<!-- código posterior omitido -->

Estamos também realizando uma associação aqui, mas não de dados, mas de eventos (event
binding). Uma diferença desse tipo de associação para o anterior com colchetes é que a primeira flui da

54 6.4 ASSOCIAÇÃO DE EVENTOS (EVENT-BINDING)


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
view para a fonte de dados e a segunda da fonte de dados para a view. Apesar de possuírem um fluxo
diferente, ambas são unidirecionais.

Mas como nosso template saberá que deve chamar o método cadastrar do nosso componente a
partir do evento submit ? Como o template está associado ao componente, ele procurará na classe
CadastroComponent, ou seja, o componente é o seu contexto.

Recarregando a página e realizando um teste vemos que a submissão do formulário faz com que ele
recarregue, e não queremos isso. Queremos que o método cadastrar seja chamado sem submeter o
formulário. Para isso, precisamos alterar nosso template para:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<form (submit)="cadastrar($event)" class="row">
<!-- código posterior omitido -->

O $event é um objeto do Angular que encapsula o evento que esta sendo disparado. Agora, em
CadastroComponent recebemos o objeto como parâmetro e executamos event.preventDefault()
para cancelar a submissão do formulário, até porque ela será feita através do JavaScript:
// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
cadastrar(event) {
event.preventDefault();
console.log(this.foto);
}
// código posterior omitido

6.5 EXERCÍCIO - UTILIZANDO O MÉTODO CADASTRAR


1.Adicione o evento submit no formulário passando em seu valor o método cadastrar :
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<form (submit)="cadastrar($event)" class="row">
<!-- código posterior omitido -->

2.Altere o método cadastrar para que receba o evento como parâmetro, e que exponha no console
o objeto Foto:
// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
cadastrar(event) {
event.preventDefault();
console.log(this.foto);
}
// código posterior omitido

Recarregando a página e realizando um teste vemos que o método cadastrar de


CadastroComponent é chamado, e como o objeto não foi preenchido, todos os campos continuam em
branco.

6.5 EXERCÍCIO - UTILIZANDO O MÉTODO CADASTRAR 55


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Figura 6.3: objeto não atualizado

6.6 E O TWO-WAY DATA-BINDING?


Nosso objeto foto não foi atualizado porque fizemos uma associação unidirecional dos dados para
a view. Por mais que digitamos um novo título, essa informação nunca será enviada para nosso
componente.

Precisamos de alguma maneira fazer a medida que digitarmos com os dados associados à nossa view
também sejam atualizados em nosso componente. O problema é que não há data-binding bidirecional,
aquele que atualiza os dados quando a view é alterada, e que altera a view quando os dados são alterados.
E agora?

Que tal disparamos a atualização dos dados toda vez que o evento input do nosso elemento for
disparado? Poderíamos pegar o valor atual e atualizar os dados da foto. Para isso, existe um outro tipo de
data-binding, unidirecional também, mas que flui da view para os dados, é fazendo a associação de
dados por eventos (event data-binding). Ela é caracterizada por parênteses que envolvem o nome do
evento que desejamos executar.
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<input
(input)="foto.titulo = $event.target.value"
[value]="foto.titulo"
class="form-control">
<!-- código posterior omitido -->

Veja que essa sintaxe é um tanto enigmática. Toda vez que o evento input for disparado, dizemos
que o dado foto.titulo em nosso componente receberá como valor $event.target.value . O
$event é um objeto do Angular que encapsula o evento original do JavaScript e que sabe tudo sobre o
evento que foi disparado. Através desse objeto podemos saber quem foi o alvo ( target ) do evento. Se
sabemos o target , podemos consultar o seu valor e capturar o que o usuário digitou.

6.7 EXERCÍCIO - FAZENDO TWO-WAY DATA-BINDING


1.Adicione o evento input que preencherá o valor dos campos com o valor digitado:
<!--caelumpic/src/app/cadastro/cadastro.component.html-->
<div class="container">
<form (submit)="cadastrar($event)" class="row">
<div class="col-md-6">
<div class="form-group">
<label>Título</label>
<input
(input)="foto.titulo = $event.target.value"
[value]="foto.titulo"

56 6.6 E O TWO-WAY DATA-BINDING?


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
class="form-control">
</div>
<div class="form-group">
<label>URL</label>
<input
(input)="foto.url = $event.target.value"
[value]="foto.url"
class="form-control">
</div>
<div class="form-group">
<label>Descrição</label>
<textarea
(input)="foto.descricao = $event.target.value"
[value]="foto.descricao"
class="form-control">
</textarea>
</div>

<button type="submit" class="btn btn-primary">


Salvar
</button>
<a [routerLink]="['']" class="btn btn-primary">Voltar</a>
</div>
</form>
</div>

Recarregando a página e testando vemos que à medida que vamos digitando nossa variável local é
atualizada.

6.8 NGMODEL
Apesar de funcional, a sintaxe que usamos é um tanto verbosa principalmente para quem veio do
Angular 1.X, acostumando com o two-way data-binding da diretiva ng-model (com hífen).

No Angular 2 a diretiva ngModel (sem hífen) é um atalho que, por debaixo dos panos, faz
exatamente o que fizemos só que com menos verbosidade. Há um detalhe que precisa ser esclarecido
antes: toda vez que você usa a diretiva ngModel dentro de um formulário, o elemento ao qual ela for
adicionada precisa ter o atributo name ou utilizar a diretiva [ngModelOptions]="{standalone:
true}" . Aqui vamos optar pelo atributo name , pois ele também proporciona o uso de recursos
avançados de validação do Angular.

Exemplo da notação do ngModel:

<input name="titulo" [(ngModel)]="foto.titulo">

Veja que a diretiva ngModel envolvida por um parênteses e colchetes, justamente para indicar que
ela pode enviar a receber dados.

Por padrão a diretiva ngModel não está disponível diretamente para uso, para usarmos precisamos
importa-la em AppModule seu componente FormsModule de @angular/form , por exemplo:

// caelumpic/src/app/app.module.ts
// codigo anterior omitido

6.8 NGMODEL 57
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
import { FormsModule } from '@angular/forms';

@NgModule({
imports: [
BrowserModule,
HttpModule,
PainelModule,
FotoModule,
routing,
FormsModule
],
declarations: [ AppComponent, ListagemComponent, CadastroComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

6.9 EXERCÍCIO - TWO-WAY DATA-BINDING COM NGMODEL


1.Remova [value] e o evento (input) , adicione o atributo name e [(ngModel)] nos inputs do
formulário:
<!--caelumpic/src/app/cadastro/cadastro.component.html-->
<div class="container">
<form class="row" (submit)="cadastrar($event)">
<div class="col-md-6">
<div class="form-group">
<label>Título</label>
<input name="titulo" [(ngModel)]="foto.titulo" class="form-control">
</div>
<div class="form-group">
<label>URL</label>
<input name="url" [(ngModel)]="foto.url" class="form-control">
</div>
<div class="form-group">
<label>Descrição</label>
<textarea name="descricao" [(ngModel)]="foto.descricao" class="form-control">
</textarea>
</div>

<button type="submit" class="btn btn-primary">


Salvar
</button>
<a [routerLink]="['']" class="btn btn-primary">Voltar</a>
</div>
</form>
</div>

Quando recarregamos nossa página nada é exibido. Verificando o console do navegador vemos a
mensagem:
Can't bind to 'ngModel' since it isn't a known property of 'input'.

2.O problema é que a diretiva ngModel não está disponível. Para tal, precisamos importar o módulo
'FormsModule' em AppModule :
// caelumpic/src/app/app.module.ts
import 'rxjs/add/operator/map';
import { NgModule } from '@angular/core';

58 6.9 EXERCÍCIO - TWO-WAY DATA-BINDING COM NGMODEL


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';
import { HttpModule } from '@angular/http';
import { RouterModule, Routes } from '@angular/router';
import { FotoModule } from './foto/foto.module';
import { PainelModule } from './painel/painel.module';
import { routing } from './app.routes';

// Importa o módulo. Não esqueça de adicioná-lo no array de imports!


import { FormsModule } from '@angular/forms';

@NgModule({
imports: [
BrowserModule,
HttpModule,
PainelModule,
FotoModule,
routing,
FormsModule
],
declarations: [ AppComponent, ListagemComponent, CadastroComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

Refaça o teste e veja que nosso objeto é atualizado com os dados do formulário!

6.10 ENVIO DE DADOS PARA O SERVIDOR


Agora que temos acesso aos dados da foto com base nos valores do formulário, precisamos enviá-la
para nosso servidor. Até agora usamos apenas o verbo GET para obter uma lista de fotos para o recurso
http://localhost:3000/v1/fotos . Dessa vez, usaremos o mesmo identificador do recurso, mas
empregaremos o verbo POST , muito usado quando queremos incluir dados. Você ainda lembra como
temos acesso ao serviço HTTP? Você aprendeu que podemos receber por injeção baseada em tipo, a
dependência do tipo Http no constructor da nossa classe:

// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component, Input } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http } from '@angular/http';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();

constructor(http: Http) {

// o que eu faço agora?


}

cadastrar(event) {

6.10 ENVIO DE DADOS PARA O SERVIDOR 59


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
event.preventDefault();
console.log(this.foto);
}
}

Como faremos para ter acesso ao Http em nosso método cadastrar ? Uma solução é criar uma
nova propriedade em nossa classe, que receberá o serviço Http injetado no construtor. Propriedades de
classe podem ser acessadas em qualquer método da classe:
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http } from '@angular/http';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


http: Http;

constructor(http: Http) {

this.http = http;
}

cadastrar(event) {

event.preventDefault();
console.log(this.foto);
// agora posso acessar http através de this.http
}
}

Mais um problema resolvido. Agora, no método cadastrar , vamos solicitar ao nosso serviço
Http que realize uma requisição do tipo POST :

// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
cadastrar(event) {
event.preventDefault();
console.log(this.foto);
this.http.post('http://localhost:3000/v1/fotos', JSON.stringify(this.foto));
}

O primeiro parâmetro da função http.post é o endereço do recurso do nosso servidor, no caso o


mesmo endereço que usamos para obter uma lista de fotos. Qual a diferença então? A diferença é que
nosso servidor está preparado para executar uma ação específica para os verbos POST e GET , mesmo
que o endereço seja o mesmo.

O segundo parâmetro é nossa foto. Porém, não podemos simplesmente pegar esse objeto e enviá-lo
como parâmetro. O padrão JSON é um formato somente texto e é exatamente uma string (JSON) o
segundo parâmetro. Precisamos antes transformar o objeto em JSON (texto) através de

60 6.10 ENVIO DE DADOS PARA O SERVIDOR


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
JSON.stringify . Mas não é apenas isso. Além do dado que estamos enviando, precisamos adicionar
uma informação especial no header da requisição dizendo o tipo de conteúdo que estamos enviado para
o servidor. Essa informação é importante, porque o servidor pode estar preparado para lidar com
diferentes tipos.

Há uma classe do Angular que representa os Headers do HTTP. Vamos importá-la do módulo
angular2/http , criar um objeto a partir dela para que possamos adicionar nossas configurações e
passá-la como parâmetro para a função http.post :

// caelumpic/src/app/cadastro/cadastro.component.ts
import {Component} from '@angular/core';
import {Http, Headers} from '@angular/http'; // importou a classe Header também
import { FotoComponent } from '../foto/foto.component';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


http: Http;

constructor(http: Http) {
this.http = http;
}

cadastrar() {
console.log(this.foto);

// cria uma instância de Headers


let headers = new Headers();
// Adiciona o tipo de conteúdo application/json
headers.append('Content-Type', 'application/json');

this.http.post('http://localhost:3000/v1/fotos', JSON.stringify(this.foto), { headers: header


s });
}
}

Muita atenção, porque não passamos nosso objeto headers diretamente. Passamos um objeto com
a propriedade headers que o contém como valor.

Agora precisamos adicionar a função .subscribe :


// caelumpic/src/app/cadastro/cadastro.component.ts
import {Component} from '@angular/core';
import {Http, Headers} from '@angular/http';
import { FotoComponent } from '../foto/foto.component';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();

6.10 ENVIO DE DADOS PARA O SERVIDOR 61


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
http: Http;

constructor(http: Http) {
this.http = http;
}

cadastrar() {
console.log(this.foto);

var headers = new Headers();


headers.append('Content-Type', 'application/json');

this.http.post('http://localhost:3000/v1/fotos', JSON.stringify(this.foto), { headers: header


s })
.subscribe(() => {
this.foto = new FotoComponent();
console.log('Foto salva com sucesso');
}, erro => console.log(erro));
}
}

As arrow functions estão de volta! Veja que na primeira função, executada quando a operação é
efetuada com sucesso, recebemos a foto de volta do servidor, com seu ID preenchido. Podemos até
imprimir seu ID no console. Em seguida, apagamos os dados da foto para que o formulário fique em
branco, o que faz todo sentido. Por fim, exibimos uma mensagem de sucesso ( Foto gravada com
sucesso ) e logamos qualquer erro que possa ocorrer. Mais tarde aprenderemos a dar uma mensagem
mais amigável para os usuário.

6.11 EXERCÍCIO - GRAVANDO UMA NOVA FOTO NA API


1.Adicione no CadastroComponent o código necessário para envio da foto para a API:

// caelumpic/src/app/cadastro/cadastro.component.ts
import {Component} from '@angular/core';
import {Http, Headers} from '@angular/http';
import { FotoComponent } from '../foto/foto.component';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


http: Http;

constructor(http: Http) {
this.http = http;
}

cadastrar() {
console.log(this.foto);

var headers = new Headers();


headers.append('Content-Type', 'application/json');

this.http.post('http://localhost:3000/v1/fotos', JSON.stringify(this.foto), { headers: header

62 6.11 EXERCÍCIO - GRAVANDO UMA NOVA FOTO NA API


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
s })
.subscribe(() => {
this.foto = new FotoComponent();
console.log('Foto salva com sucesso');
}, erro => console.log(erro));
}
}

Figura 6.4: Nova foto cadastrada

6.12 CICLO DE VIDA DE UM COMPONENTE


Já podemos cadastrar quantas fotos quisermos, mas o que acontecerá se uma delas tiver título com
mais de 10 caracteres? As chances de ele estourar na exibição do PainelComponent são grandes. No
lugar de limitar a quantidade de caracteres no cadastro, vamos alterar no PainelComponent para que
exiba apenas os sete primeiros caracteres do título. Para dar uma pista visual de que o título da foto é
maior do que o apresentado, adicionaremos reticências no final.

Sabemos que o construtor da classe do nosso componente é sempre chamado quando ele é
instanciado. Sendo assim, faremos o ajuste do parâmetro passado para o titulo no construtor:

// caelumpic/src/app/painel/painel.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent {

@Input() titulo: string;

constructor() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}
}

6.12 CICLO DE VIDA DE UM COMPONENTE 63


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Quando recarregamos nossa página, nada é exibido. Olhando o console do browser temos a
mensagem:
ORIGINAL EXCEPTION: TypeError: Cannot read property 'length' of undefined

Ao que tudo indica, o valor da propriedade titulo ainda não foi passado para nosso componente,
porque ela é undefined . E agora?

Componentes criados pelo Angular passam por etapas específicas durante sua construção. O
conjunto dessas etapas é chamado de ciclo de vida. Podemos adicionar "ganchos" para que possamos
interagir com essas fases. Por exemplo, há a fase OnInit executada sempre que um valor de entrada ou
de saída acontece. Há outros como OnDestroy executado quando o componente é destruído, entre
outros.

Para resolver nosso problema, vamos interagir com a fase OnInit , porque temos a garantia de que
os parâmetros do nosso componente já foram passados. Basta adicionarmos o método ngOnInit em
nosso componente para interagirmos com esta fase:
// caelumpic/src/app/painel/painel.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent {

@Input() titulo: string;

ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}

Conseguimos o resultado esperado. No entanto, o que acontece se digitarmos incorretamente o


nome do método? Por exemplo:
// caelumpic/src/app/painel/painel.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent {

@Input() titulo: string;

// perceba que o "i" esta em minúsculo!


ngOninit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :

64 6.12 CICLO DE VIDA DE UM COMPONENTE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
this.titulo;
}
}

Nada acontece. É como se esse gancho não existisse para o Angular. Para evitar cairmos nesse erro,
podemos implementar uma interface, uma espécie de contrato que nos obriga a escrever o nome do
método corretamente, caso contrário o compilador do TypeScript nos avisará. Ainda sem corrigir o
nome do método, vamos importar a interface OnInit também do módulo @angular2/core :
// caelumpic/src/app/painel/components/painel.component.ts
import { Component, Input, OnInit } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent implements OnInit {

@Input() titulo: string;

ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}

No Visual Studio Code, o nome da classe PainelComponent ganha um sublinhado vermelho e


quando passamos o mouse por cima temos a mensagem:

Class 'PainelComponent' incorrectly implements interface 'OnInit'. Property 'ngOnInit' is missing in


type 'PainelComponent'.
class PainelComponent

E se olharmos no terminal do compilador do TS temos a mensagem:

app/painel/painel.component.ts(8,14): error TS2420: Class 'PainelComponent' incorrectly implements in


terface 'OnInit'.
Property 'ngOnInit' is missing in type 'PainelComponent'.

Veja que enquanto não implementar corretamente o método ngOnInit nosso código não
compilará. Isso é interessante, porque evita que nosso código entre em produção e só depois de algum
tempo descobrirmos que a troca de uma simples letra comprometeu nossa solução.

Por fim, temos a versão final do nosso PainelComponent , agora atendendo o contrato da interface:

// caelumpic/src/app/painel/components/painel.component.ts
import { Component, Input, OnInit } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent implements OnInit {

@Input() titulo: string;

6.12 CICLO DE VIDA DE UM COMPONENTE 65


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}

6.13 EXERCÍCIO - MANIPULANDO PROPRIEDADES DE UMA CLASSE


1.Implemente o onInit em PainelComponent:
// caelumpic/src/app/painel/components/painel.component.ts
import { Component, Input, OnInit } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent implements OnInit {

@Input() titulo: string;

ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}

2.(Opcional) Se você souber um pouquinho mais de ES6 pode usar template string no lugar da
concatenação:
// caelumpic/src/app/painel/components/painel.component.ts
ngOnInit() {
this.titulo = this.titulo.length > 7
? `${this.titulo.substr(0, 7)}...`
: this.titulo;
}

66 6.13 EXERCÍCIO - MANIPULANDO PROPRIEDADES DE UMA CLASSE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 7

VALIDANDO NOSSO FORMULÁRIO

Um ponto importante é validarmos a entrada do usuário impedindo a inserção de qualquer dado


inconsistente. Por exemplo, não é incomum termos campos obrigatório como por exemplo, o título da
foto. A boa notícia é que o Angular já possui uma infraestrutura pronta para nos ajudar com validações.

7.1 VALIDAÇÃO ORIENTADA A TEMPLATE


O primeiro passo para usarmos validação orientada a template é importarmos FormsModule em
AppModule . Contudo, já importamos esse módulo para que a diretiva ngModel funcionasse em nossos
templates.

Se quisermos tornar nosso campo obrigatório, podemos usar o atributo required do próprio
HTML5:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<div class="form-group">
<label>Título</label>
<input required name="titulo" [(ngModel)]="foto.titulo" class="form-control">
</div>
<!-- código posterior omitido -->

E se o campo estiver inválido, ou seja, vazio? Vamos adicionar uma mensagem para o usuário, logo
abaixo do input do nome:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<div class="form-group">
<label>Título</label>

<input required name="titulo" [(ngModel)]="foto.titulo" class="form-control">

<span class="form-control alert-danger">


Título obrigatório
</span>
</div>
<!-- código posterior omitido -->

Se recarregarmos nossa página podemos verificar um problema logo de cara. A mensagem "Título
obrigatório" é exibida prontamente, e mesmo se completarmos o campo, ela continua sendo exibida. Na
prática, só podemos ver a mensagem se o campo estiver inválido. Há outro problema também: como o
Angular saberá consultar se o campo é válido ou inválido? Ele baterá na porta do HTML5 para saber?

7 VALIDANDO NOSSO FORMULÁRIO 67


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
A solução dos dois problemas é a mesma: precisamos ter acesso a um objeto especial que representa
o elemento do formulário e via Angular consultá-lo para saber se ele é válido ou não. Para criar esse
objeto, usamos a seguinte sintaxe:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<div class="form-group">
<label>Título</label>

<input
required
name="titulo"
#titulo = "ngModel"
[(ngModel)]="foto.titulo"
class="form-control">

</div>
<!-- código posterior omitido -->

Veja que adicionamos #titulo = "ngModel" ao input do título. Essa sintaxe cria uma variável
local, como já vimos antes, porém seu valor é a diretiva ngModel . Essa sintaxe um tanto estranha
permite acessarmos o objeto que representa a validação com campo através do nome da variável:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<div class="form-group">
<label>Título</label>

<input
required
name="titulo"
#titulo = "ngModel"
[(ngModel)]="foto.titulo"
class="form-control">

<span *ngIf="titulo.invalid" class="form-control alert-danger">


Título obrigatório
</span>
</div>
<!-- código posterior omitido -->

A diretiva ngIf exibe o elemento no qual foi adicionada se o seu valor for true e o esconde
quando for false . É por isso que seu valor é a expressão titulo.invalid , isto é, só exibiremos a tag
<span> quando o título for inválido. Mas isso ainda não é suficiente.

Já podemos recarregar o formulário e testar o resultado:

68 7.1 VALIDAÇÃO ORIENTADA A TEMPLATE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Figura 7.1: Validação de campo

Um olhar atento dirá que o botão Salvar continua habilitado e podemos salvar nossa foto. Será
que podemos desativá-lo enquanto nosso formulário estiver inválido? Veja, não quero saber de um
campo específico, mas do formulário como um todo. Felizmente podemos fazer isso.

Precisamos primeiro declarar uma variável de template local, para nosso formulário:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<form #meuForm="ngForm" (submit)="cadastrar($event)" class="row">

Veja que inicializamos a variável #meuForm com o valor ngForm . E agora, no botão:

<!-- caelumpic/src/app/cadastro/cadastro.component.html -->


<!-- código anterior omitido -->
<button type="submit" class="btn btn-primary" [disabled]="meuForm.form.invalid">Salvar</button>
<!-- código posterior omitido -->

7.2 EXERCÍCIO - VALIDANDO O FORMULÁRIO


1.Adicione ao input do título o atributo required e a variável #titulo="ngModel" :

<!-- caelumpic/src/app/cadastro/cadastro.component.html -->


<!-- código anterior omitido -->
<div class="form-group">
<label>Título</label>
<input

7.2 EXERCÍCIO - VALIDANDO O FORMULÁRIO 69


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
required
name="titulo"
#titulo = "ngModel"
[(ngModel)]="foto.titulo"
class="form-control">
</div>
<!-- código posterior omitido -->

2.Adicione uma tag <span> embaixo do <input> com a mensagem de validação:


<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<div class="form-group">
<label>Título</label>

<input
required
name="titulo"
#titulo = "ngModel"
[(ngModel)]="foto.titulo"
class="form-control">

<span *ngIf="titulo.invalid" class="form-control alert-danger">


Título obrigatório
</span>
</div>
<!-- código posterior omitido -->

3.(Opcional) Para desabilitar o botão salvar quando o formulário estiver inválido. Precisamos
primeiro declarar uma variável de template local, para nosso formulário:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<form #meuForm="ngForm" (submit)="cadastrar($event)" class="row">

Agora, em nosso botão:


<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<button type="submit" class="btn btn-primary" [disabled]="meuForm.form.invalid">Salvar</button>
<!-- código posterior omitido -->

Já podemos testar nosso formulário.

70 7.2 EXERCÍCIO - VALIDANDO O FORMULÁRIO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Figura 7.2: Botão Salvar desabilitado

Nosso formulário e sua validação segue o padrão validação orientada a template (template-driven
form). Porém, existe outra forma de validação configurada em nosso componente.

7.3 VALIDAÇÃO ORIENTADA A MODELO


Para que possamos realizar uma validação baseada em modelo (model-driven form) precisamos
realizar alguns ajustes no código do template que escrevemos até agora.

O primeiro deles é importarmos o módulo ReactiveFormsModule em AppModule para habilitar


essa funcionalidade:
// caelumpic/src/app/app.module.ts
// outros imports omitidos
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
imports: [
BrowserModule,
HttpModule,
PainelModule,
FotoModule,
routing,
FormsModule,
ReactiveFormsModule
],
declarations: [ AppComponent, ListagemComponent, CadastroComponent ],

7.3 VALIDAÇÃO ORIENTADA A MODELO 71


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
bootstrap: [ AppComponent ]
})
export class AppModule { }

O segundo passo é remover a declaração da variável local #meuForm . Como ela não existe mais, não
temos um valor para usar na diretiva disabled para desabilitar o botão "salvar" caso o formulário
esteja inválido. Temporariamente deixaremos um valor vazio:

<!-- código posterior omitido -->


<button type="submit" class="btn btn-primary" [disabled]= " " >Salvar</button>
<!-- código posterior omitido -->

Também precisamos remover a declaração da variável local #titulo . Com sua remoção, também
não temos mais um valor para colocar na diretiva ngIf que decide se exibe ou não a mensagem de erro
de validação para o usuário. Também vamos deixar seu valor vazio por enquanto:
<form (submit)="cadastrar($event)" class="row">
<div class="col-md-6">
<div class="form-group">
<label>Título</label>
<input
name="titulo"
[(ngModel)]="foto.titulo"
class="form-control">

<span *ngIf= " " class="form-control alert-danger">


Título obrigatório
</span>
</div>
<!-- código posterior omitido -->

Agora que fizemos esses pequenos ajustes, vamos criar uma nova propriedade em
CadastroComponent que seja do tipo FormGroup . Instâncias de FormGroup podem gerenciar um ou
mais inputs de controle. Para isso, precisamos importar a classe do módulo @angular/forms .

// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup } from '@angular/forms';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


http: Http;
meuForm: FormGroup;

constructor(http: Http) {
// restante do código omitido

Agora que vamos associar o atributo do nosso componente meuForm que é um FormGroup ao
formulário do nosso template através da diretiva formGroup:

72 7.3 VALIDAÇÃO ORIENTADA A MODELO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<form [formGroup]="meuForm" (submit)="cadastrar($event)" class="row">

Agora que temos tudo encaixado, vamos construir nosso FormGroup .

7.4 O CONSTRUTOR DE FORMULÁRIOS


Angular nos permite agrupar vários campos (FormControl) dentro de um grupo (FormGroup). Essa
maneira de agrupar controles é interessante, porque podemos perguntar ao grupo se ele esta válido (se
todos os controles estão válidos) ou se é inválido (se um dos controles for inválido).

Angular possui a classe FormBuilder que nos ajuda a criar uma instância de FormGroup . Vamos
importar essa classe e injetá-la no construtor do nosso componente. É por meio do seu método group
que criamos uma validação para um ou mais campos:
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


http: Http;
meuForm: FormGroup;

constructor(http: Http, fb: FormBuilder) {

this.http = http;

this.meuForm = fb.group({

titulo: ['', /* qual validação usar? */],


url: ['', /* qual validação usar? */],
descricao: ['', /* qual validação usar? */]
});
}
// código posterior omitido

Veja que o método group recebe um objeto JavaScript onde a chave é o identificador do campo e
seu valor um array com configurações de validação. Usamos a chave titulo para indicar que estamos
validando o campo título e assim por diante. Ainda não indicamos que tipo de validação utilizaremos,
mas precisamos ligar o input do template do nosso componente através dessa chave. Fazemos isso
através da diretiva formControlName , veja o exemplo:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<div class="form-group">
<label>Título</label>
<input
name="titulo"
[(ngModel)]="foto.titulo"

7.4 O CONSTRUTOR DE FORMULÁRIOS 73


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
formControlName="titulo"
class="form-control">
<span *ngIf="meuForm.controls.titulo.invalid" class="form-control alert-danger">
Título obrigatório
</span>
</div>

Veja também que para os campos titulo e url voltamos a preencher a condição ngIf , só que
dessa vez consultando o elemento dentro do meuForm.controls .

Chegou a hora de definirmos nosso primeiro validador, aquele que torna o preenchimento do campo
obrigatório. Para isso, precisamos importar Validators do módulo @angular/forms :
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


http: Http;
meuForm: FormGroup;

constructor(http: Http, fb: FormBuilder) {

this.http = http;

this.meuForm = fb.group({
titulo: ['', Validators.required],
url: ['', Validators.required],
descricao: [''],
});
}
// código posterior omitido

Apenas os controles titulo e url serão validados, e ainda assim somos obrigados a adicionar o
controle descricao sem nenhum validador. Caso contrário não teremos acesso a esta informação
quando formos submeter o formulário.

Por fim, só precisamos de mais uma alteração. Lembra do botão que desabilitamos se o formulários
estiver inválido? Ele agora acessará meuForm ; não a variável que removemos, mas a propriedade do
nosso componente:
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<button type="submit" class="btn btn-primary" [disabled]="meuForm.invalid">
Salvar
</button>

7.5 EXERCÍCIO - VALIDANDO O FORMULÁRIO PELO MODELO

74 7.5 EXERCÍCIO - VALIDANDO O FORMULÁRIO PELO MODELO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
1.Importe ReactiveFormsModule do Angular em AppModule:

// caelumpic/src/app/app.module.ts
// outros imports omitidos
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
imports: [
BrowserModule,
HttpModule,
PainelModule,
FotoModule,
routing,
FormsModule,
ReactiveFormsModule
],
declarations: [ AppComponent, ListagemComponent, CadastroComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

2.Importe FormGroup, FormBuilder e Validators do Angular Forms, crie a propriedade


meuForm, e edite o constructor como o modelo abaixo:
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


http: Http;
meuForm: FormGroup;

constructor(http: Http, fb: FormBuilder) {

this.http = http;

this.meuForm = fb.group({
titulo: ['', Validators.required],
url: ['', Validators.required],
descricao: [''],
});
}
// código posterior omitido

3.Altere a variável local meuForm na tag <form> para associar com a propriedade do componente
meuForm através da diretiva formGroup:

<!-- caelumpic/src/app/cadastro/cadastro.component.html -->


<form [formGroup]="meuForm" (submit)="cadastrar($event)" class="row">

4.Adicione o atributo formControlName nos campos de título, URL e descrição, e atualize a

7.5 EXERCÍCIO - VALIDANDO O FORMULÁRIO PELO MODELO 75


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
verificação de *ngIf nos seus respectivos <span> 's:
<div class="form-group">
<label>Título</label>
<input
name="titulo"
[(ngModel)]="foto.titulo"
formControlName="titulo"
class="form-control">

<span *ngIf="meuForm.controls.titulo.invalid" class="form-control alert-danger">


Título obrigatório
</span>
</div>

<div class="form-group">
<label>URL</label>
<input
name="url"
[(ngModel)]="foto.url"
formControlName="url"
class="form-control">

<span *ngIf="meuForm.controls.url.invalid" class="form-control alert-danger">


URL obrigatória
</span>
</div>

<div class="form-group">
<label>Descrição</label>
<textarea name="descricao" [(ngModel)]="foto.descricao" formControlName="descricao" class="form-c
ontrol">
</textarea>
</div>

5.Ajuste a validação do botão de salvar:


<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<!-- código anterior omitido -->
<button type="submit" class="btn btn-primary" [disabled]="meuForm.invalid">
Salvar
</button>

7.6 COMPONDO VALIDADORES


Caso seja necessário adicionar mais um tipo de validação no campo titulo , além de campo
obrigatório, tenha também como tamanho mínimo de dígitos, podemos usar o Validators.compose
para definir um conjunto de validações para determinado campo. E para validar o mínimo de digitos
podemos usar Validators.minLength que já vem com o Angular. Exemplo:

// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
titulo: ['', Validators.compose(
[Validators.required, Validators.minLength(4)]
)]
// código posterior omitido

Para ficar tudo redondo, temos que dar uma mensagem diferente para erro de validação e outra para

76 7.6 COMPONDO VALIDADORES


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
o tamanho do campo. Podemos consultar as variáveis locais que criamos que apontam para nossos
Control s. Se houver algum erro de validação, o objeto erros será criado e nele adicionadas
propriedades que equivalem a cada validador que adicionamos. O valor dessa propriedade será true
caso tenha ocorrido uma violação da validação e false caso o campo seja válido.
<!-- caelumpic/src/app/cadastro/cadastro.component.html -->
<div class="container">

<form [formGroup]="meuForm" (submit)="cadastrar($event)" class="row">


<div class="col-md-6">
<div class="form-group">
<label>Título</label>
<input [(ngModel)]="foto.titulo" formControlName="titulo" name="titulo" class="form-contr
ol">

<div *ngIf="!meuForm.controls.titulo.valid">

<span *ngIf="meuForm.controls.titulo.errors.required" class="form-control alert-d


anger">
Título obrigatório
</span>

<span *ngIf="meuForm.controls.titulo.errors.minlength" class="form-control alert-


danger">
Título deve ter no mínimo 4 caracteres
</span>

</div>
</div>
<!-- código posterior omitido -->

7.7 EXERCÍCIO - MAIS VALIDAÇÕES


1.Ajuste o constructor de CadastroComponent para usar o compose:
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

http: Http;
meuForm: FormGroup;

constructor(http: Http, fb: FormBuilder) {

this.http = http;

this.meuForm = fb.group({
titulo: ['', Validators.compose(
[Validators.required, Validators.minLength(4)]
)],
url: ['', Validators.required],

7.7 EXERCÍCIO - MAIS VALIDAÇÕES 77


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
descricao: [''],
});
}
// código posterior omitido

2.Adicione mais um <span> para a mensagem de validação por tamanho. E vamos melhorar um
pouco nossa página: exiba o título da imagem no lugar do título da página e mostre a imagem que será
cadastrada.
<div class="container">
<h1 class="text-center">{{foto.titulo}}</h1><!-- titulo da foto -->
<form [formGroup]="meuForm" (submit)="cadastrar($event)" class="row">
<div class="col-md-6">
<div class="form-group">
<label>Título</label>
<input name="titulo" formControlName="titulo" [(ngModel)]="foto.titulo" class="form-c
ontrol">
<div *ngIf="!meuForm.controls.titulo.valid">
<span *ngIf="meuForm.controls.titulo.errors.required" class="form-control alert-d
anger">
Título obrigatório
</span>
<!-- mensagem de validação por tamanho -->
<span *ngIf="meuForm.controls.titulo.errors.minlength" class="form-control alert-
danger">
Título deve ter no mínimo 4 caracteres
</span>
</div>
</div>
<div class="form-group">
<label>URL</label>
<input name="url" formControlName="url" [(ngModel)]="foto.url" class="form-control">
<span *ngIf="!meuForm.controls.url.valid" class="form-control alert-danger">
URL obrigatória
</span>
</div>
<div class="form-group">
<label>Descrição</label>
<textarea name="descricao" formControlName="descricao"
[(ngModel)]="foto.descricao" class="form-control">
</textarea>
</div>

<button type="submit" class="btn btn-primary" [disabled]="!meuForm.valid">


Salvar
</button>
<a [routerLink]="['']" class="btn btn-primary">Voltar</a>
<hr>
</div>
<!-- imagem a ser cadastrada -->
<div class="col-md-6">
<foto url="{{foto.url}}" titulo="{{foto.titulo}}"></foto>
</div>
</form>
</div>

78 7.7 EXERCÍCIO - MAIS VALIDAÇÕES


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 8

ENCAPSULANDO NOSSA VIEW

8.1 ESTILIZANDO UM COMPONENTE


Excelente, já conseguimos cadastrar fotos, inclusive aprendemos a validar nosso formulário. Agora
temos a seguinte situação: precisamos colocar uma sombra no painel de cada imagem, inclusive realizar
um zoom na imagem do painel quando passarmos o mouse sobre ela. Mesmo usando o Bootstrap, para
algumas coisas específicas são necessárias implementação.

Vamos começar estilizando nosso painel. Que tal eu criar um arquivo CSS específico para esse
componente? E que tal salvá-lo dentro da mesma pasta do componente? Dessa maneira, quando
olharmos a pasta do componente saberemos com clareza onde está a sua folha de estilo. Por exemplo
podemos criar uma folha de estilos assim:

/* caelumpic/src/app/painel/painel.component.css */
.efeito {
box-shadow: 2px 2px 15px;
}

Agora para usarmos a folha de estilo criada, importarmos o arquivo css no head da página
index.html :

<!-- caelumpic/src/index.html -->


<!doctype html>
<html>
<head>
<base href="/">
<title>Caelumpic</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">

<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">


<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap-theme.min.css">

<!-- importando novo CSS -->


<link rel="stylesheet" href="app/painel/painel.component.css">

Esta é uma maneira de adicionar estilos à um componente. Este seria o resultado:

8 ENCAPSULANDO NOSSA VIEW 79


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Figura 8.1: Painel sombreado

Para criamos estilos para o componente foto, temos que repetir o mesmo processo. Criar o arquivo
caelumpic/src/app/foto/foto.component.css e adicionar os estilos que realizam o zoom da
imagem através do ponteiro do mouse:
/* caelumpic/src/app/foto/foto.component.css */
.efeito:hover {
transition: all 0.5s;
transform: scale(1.15);
}

.efeito {
transition: all 1s;
}

E novamente, precisamos lembrar de importá-lo no index.html :

<!-- caelumpic/src/index.html -->


<!doctype html>
<html>
<head>
<base href="/">
<title>Caelumpic</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">

<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">


<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap-theme.min.css">

<link rel="stylesheet" href="app/painel/painel.component.css">


<link rel="stylesheet" href="app/foto/foto.component.css">

Adicionando a classe no template do componente:

<!-- caelumpic/src/app/foto/foto.component.html -->

80 8.1 ESTILIZANDO UM COMPONENTE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<img class="img-responsive center-block efeito" src="{{url}}" alt="{{titulo}}">

8.2 O PROBLEMA DE ESTILOS GLOBAIS


Ao recarregar a página e ver o resultado percebemos um problema! O painel e foto receberam o
mesmo estilo da sombra, inclusive o painel também cresce quando passamos o mouse por cima, não
apenas a foto! O problema é que cada componente usa mesma classe efeito e como usamos o mesmo
seletor CSS para criar efeitos diferentes, os dois são aplicados. Podemos resolver isso qualificando nosso
seletor:
/* caelumpic/src/app/painel/painel.component.css */
.painel.efeito {
box-shadow: 2px 2px 15px;
}

Adicionando a classe painel :

<!--caelumpic/src/app/painel/painel.component.html-->
<div class="panel panel-default painel efeito">
<div class="panel-heading">
<h3 class="panel-title text-center">{{titulo}}</h3>
</div>
<div class="panel-body">
<ng-content></ng-content>
</div>
</div>

/* caelumpic/src/app/foto/foto.component.css */
.foto.efeito:hover {
transition: all 0.5s;
transform: scale(1.15);
}

.foto.efeito {
transition: all 1s;
}

Adicionando a classe foto :


<!-- caelumpic/src/app/foto/foto.component.html -->
<img class="img-responsive center-block foto efeito" src="{{url}}" alt="{{titulo}}">

8.2 O PROBLEMA DE ESTILOS GLOBAIS 81


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Figura 8.2: Zoom imagem

O problema é que estes estilos possuem um escopo global, no qual podem ser aplicados em qualquer
elemento da página que faça uso destas classes. Essa grande vantagem do CSS pode se tornar uma dor de
cabeça, quando queremos que um estilo seja aplicado em um elemento da nossa página apenas. Por mais
que qualifiquemos nossos seletores nada impede que o mesmo seletor qualificado já esteja sendo usado
em outro arquivo CSS que muitas vezes sequer podemos alterar.

Uma solução para esse problema seria reduzir o escopo do estilo de um componente ao próprio
componente e não à página como um todo. Com isso, poderíamos usar nossos componentes em
qualquer lugar da aplicação sem termos que nos preocupar com a colisão de seletores ou que qualificar
cada um deles. A boa notícia é que os componentes do Angular permitem a aplicação de estilos que tem
como escopo o próprio elemento que estilizam.

8.3 ESTILO POR COMPONENTE


Através da configuração styleUrls no componente indicamos em um array todos os CSS's
utilizados por eles, em o nosso caso, temos apenas um arquivo. Aguarde o TypeScript compilar o
arquivo para ver o resultado. Veja o exemplo abaixo:
//código anterior omitido
@Component({
selector: 'painel',
templateUrl: './painel.component.html',
styleUrls: ['./painel.component.css']
})
//código posterior omitido

A primeira forma dessa abordagem é que não precisamos lembrar de importar a folha de estilo do

82 8.3 ESTILO POR COMPONENTE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
nosso componente. Simplesmente importamos o componente onde queremos utilizá-lo e todo seu estilo
será carregado automaticamente. Em segundo lugar, não houve conflito de seletores, algo que
conseguimos sem a necessidade de uma classe auxiliar. Porém, como o Angular consegue fazer isto?

Inspecionando o index.html pelo navegador, vemos que na tag <head> da página é adicionada a
tag's <style> para cada componente que tem estilos próprios, exemplo:
<!--o código a seguir é o head no DOM sendo inspecionado no browser,
não entra em nenhum lugar no código da nossa aplicação-->
<style>
.efeito[_ngcontent-irr-5] {
box-shadow: 2px 2px 15px;
}

.efeito[_ngcontent-gwo-4]:hover {
transition: all 0.5s;
transform: scale(1.15);
}

.efeito[_ngcontent-gwo-4] {
transition: all 1s;
}
</style>

Um ponto curioso é que o Angular simplesmente não copiou e colou o estilo dos nossos arquivos, ele
também altera os seletores que usamos! Veja que foi adicionado para cada seletor um seletor de atributo,
porém um atributo um tanto estranho. Este atributo é gerado aleatoriamente e adicionado em cada uma
dos nossos componentes no DOM pelo Angular.

Por exemplo, se tivermos cinco componentes Foto, todos terão o mesmo atributo porque são o
mesmo componente, se tivermos outro componente ele terá seu próprio atributo. Com essa alteração,
temos certeza que o estilo será aplicado apenas ao tipo de componente para o qual foi criado. Vendo o
elemento que representa o componente Painel no DOM, veja seus atributos:

<div _ngcontent-irr-5 class="panel panel-default efeito">

8.4 EXERCÍCIO - ADICIONANDO ESTILO NO COMPONENTE


1.Crie o arquivo painel.component.css com o seguinte estilo:
/* caelumpic/src/app/painel/painel.component.css */
.efeito {
box-shadow: 2px 2px 15px;
}

Crie também um arquivo para foto.component.css :

/* caelumpic/src/app/foto/foto.component.css */
.efeito:hover {
transition: all 0.5s;
transform: scale(1.15);
}

8.4 EXERCÍCIO - ADICIONANDO ESTILO NO COMPONENTE 83


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
.efeito {
transition: all 1s;
}

2.Altere o template de painel adicionando a classe efeito na div do painel:


<!-- caelumpic/src/app/painel/painel.component.html -->
<div class="panel panel-default efeito">
<div class="panel-heading">
<h3 class="panel-title text-center">{{titulo}}</h3>
</div>
<div class="panel-body">
<ng-content></ng-content>
</div>
</div>

No template de foto adicione a classe efeito :

<!-- caelumpic/src/app/foto/foto.component.html -->


<img class="img-responsive center-block efeito" src="{{url}}" alt="{{titulo}}">

3.Por último adicione a configuração styleUrls nos componentes painel e foto:

// caelumpic/src/app/painel/painel.component.ts
import { Component, Input, OnInit } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html',
styleUrls: ['./painel.component.css']
})
export class PainelComponent implements OnInit {

@Input() titulo: string;

ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}
}

foto.component.ts:
// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html',
styleUrls: ['./foto.component.css']
})
export class FotoComponent {

@Input() titulo: string = '';


@Input() url: string = '';
descricao: string = '';
_id: string = '';
}

84 8.4 EXERCÍCIO - ADICIONANDO ESTILO NO COMPONENTE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
8.5 VIEW ENCAPSULATION E SEUS TIPOS
O que acabamos de ver é a estratégia padrão de encapsulamento de view do Angular, a estratégia
ViewEncapsulation.Emulated. Podemos até explicitar essa estratégia usando a configuração
encapsulation :

//codigo anterior omitido


@Component({
selector: 'foto',
templateUrl: './foto.component.html',
styleUrls: ['./foto.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
//codigo posterior omitido

A palavra Emulated significa emulado . Quem emula, emula alguma coisa, certo? O alvo da
emulação é o Shadow DOM.

8.6 SHADOW DOM, PODEMOS CONFIAR?


Shadow DOM é recurso dos Web Components, uma tecnologia emergente proposta pela Google à
W3C que permite encapsular Javascript, CSS e template em um componente. Apesar do Shadow DOM
já existir antes dos Web Componentes, não podíamos manipulá-lo. Um exemplo disso é o uso do
<input type="date"> do HTML5. Você nunca se perguntou em que lugar fica definida marcação, o
estilo e o comportamento do calendário que é exibido?

Perceba que o Shadow DOM oferece justamente o que acabamos de atingir: encapsular nosso estilo
em nosso componente. Porém, nem todo navegador suporta completamente todas as características de
um Web Component e a possibilidade de manipular ou criar o Shadow DOM de um elemento, é por
isso que o Angular emula esta funcionalidade alterando nossos seletores e adicionando atributos
aleatórios que não se repetem.

Veja que nosso layout deixa de funcionar. Qual motivo? Quando habilitarmos o modo nativo, o
componente só terá acesso àqueles CSS's que foram explicitados no atributo styleUrls . Veja que
nosso componente depende do bootstrap e não conseguiu enxergá-lo. Se quisermos que nosso
componente enxergue o Bootstrap precisaremos adicioná-lo na lista de estilos de que depende.

Na versão emulada, o problema da aplicabilidade única por componente é resolvida, mas o elemento
ainda consegue acessar estilos globais fora do seu escopo como os definidos pelo bootstrap. Para nosso
projeto, a versão emulada nos é conveniente, mas o ideal era termos cada componente do bootstrap em
um estilo em separado para em seguida importarmos cada um desses estilos em nosso componente.

Por fim, existe ViewEncapsulation.None , que apenas "joga" os estilos para dentro da tag head
usando a tag style , mas sem qualquer preocupação em garantir que esses estilos sejam aplicados nos
componentes para os mais foram criados. Acabamos caindo no mesmo problema do início do capítulo.

8.5 VIEW ENCAPSULATION E SEUS TIPOS 85


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Sendo assim, vamos manter ViewEncapsulation.Emulated . Mesmo sendo o padrão, é bom
explicitar essa configuração em cada componente para garantir.

8.7 EXERCÍCIO OPCIONAL: TESTANDO A EMULAÇÃO DO SHADOW


DOM
1.Adicione a configuração encapsulation em FotoComponent e mude seu valor para
ViewEncapsulation.Native, depois teste com ViewEncapsulation.None e
ViewEncapsulation.Emulated:
// caelumpic/src/app/foto/foto.component.ts
import { Component, Input, ViewEncapsulation } from '@angular/core';

@Component({
selector: 'foto',
templateUrl: './foto.component.html',
styleUrls: ['./foto.component.css'],
encapsulation: ViewEncapsulation.Native
})
export class FotoComponent {

@Input() titulo: string = '';


@Input() url: string = '';
descricao: string = '';
_id: string = '';
}

Inspecione o código fonte para entender os impactos.

86 8.7 EXERCÍCIO OPCIONAL: TESTANDO A EMULAÇÃO DO SHADOW DOM


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 9

ISOLANDO CÓDIGO REPETIDO EM


SERVIÇOS

Aos poucos nossa aplicação vai ganhando forma, mas ainda há algo que podemos melhorar
internamente. Veja que nos componentes ListagemComponent e CadastroComponent usamos o
serviço Http para consumirmos dados do nosso servidor. No Cadastro,foi necessário configurar o
header da requisição e tanto nele quando em ListagemComponent, declaramos o endereço
http://localhost:3000/v1/fotos . E se o endereço do recurso mudar? Precisaremos lembrar de
alterar nesses dois lugares, e a change de erros acontecerem aumenta muito.

9.1 CRIANDO UM SERVIÇO


Podemos isolar todo o código que interage com o servidor em uma classe. Todos os componentes
que quiserem se integrar com o servidor terão como dependência essa classe. Como centralizamos toda a
lógica de acesso em um lugar apenas, qualquer modificação será propagada em todos que fizerem uso da
nossa classe.

Em um arquivo separado, criamos a classe FotoService, onde construtor do serviço terá como
dependência Http , que será injetado pelo Angular. Inclusive, no próprio construtor já vamos
configurar uma instância de Headers para que possamos utilizá-la com Http nos métodos lista e
cadastra :

// caelumpic/src/app/foto/foto.service.ts
import { Http, Headers } from '@angular/http';
import { FotoComponent } from './foto.component';

export class FotoService {

http: Http;
headers: Headers;
url: string = 'http://localhost:3000/v1/fotos';

constructor(http: Http) {

this.http = http;
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
}

lista() {}

9 ISOLANDO CÓDIGO REPETIDO EM SERVIÇOS 87


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
cadastra(foto: FotoComponent) {}
}

Veja que temos uma propriedade que guarda a URL para não termos que repeti-la nos métodos
lista e cadastra . Repare também que o método cadastra recebe como parâmetro o tipo
FotoComponent , aqui estamos utilizando o sistema de tipos do TypeScript para garantir que apenas
instâncias desse componente podem ser passadas como parâmetro.

Agora vamos adicionar os métodos lista e cadastra :


// caelumpic/src/app/foto/foto.service.ts
import { Http, Headers } from '@angular/http';
import { FotoComponent } from './foto.component';

export class FotoService {

http: Http;
headers: Headers;
url: string = 'http://localhost:3000/v1/fotos';

constructor(http: Http) {

this.http = http;
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
}

lista() {

return this.http.get(this.url)
.map(res => res.json());
}

cadastra(foto: FotoComponent) {

return this.http.post(this.url, JSON.stringify(foto),


{ headers: this.headers });
}
}

Veja que as implementações são idênticas às que fizemos nos componentes ListagemComponent e
CadastroComponent . No entanto, já aprendemos a definir o tipo do retorno de métodos, mas qual tipo
é retornado nos dois casos? Lembre-se tanto http.get quanto http.post retornam um observable
stream, ou seja, ambos retornam o tipo Observable . Não podemos simplesmente indicar esse tipo de
retorno, primeiro precisamos importar sua definição. Mas de qual módulo importá-la? Precisamos
importá-la do módulo rxjs :
// caelumpic/src/app/foto/foto.service.ts
import {Http, Headers} from '@angular/http';
import {Foto} from '../components/foto';
import {Observable} from 'rxjs';

export class FotoService {

http: Http;
headers: Headers;
url: string = 'http://localhost:3000/v1/fotos';

88 9.1 CRIANDO UM SERVIÇO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
constructor(http: Http) {

this.http = http;
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
}

lista(): Observable {

return this.http.get(this.url)
.map(res => res.json());
}

cadastra(foto): Observable {

return this.http.post(this.url, JSON.stringify(foto),


{ headers: this.headers });
}
}

No entanto, temos uma mensagem de erro, parece que o tipo que usamos é incompatível:

Generic type 'Observable<T>' requires 1 type argument(s).


import Observable

A classe Observable é genérica demais e precisamos indicar que tipo de dados ele está observando.
No caso do método lista , o resultado da função map é uma lista de fotos, sendo assim podemos
fazer:
// caelumpic/src/app/foto/services/foto-service.ts
// código anterior omitido
lista(): Observable<Array<FotoComponent>> {
// código posterior omitido

Ou na versão mais enxuta:

// caelumpic/src/app/foto/foto-service.ts
// código anterior omitido
lista(): Observable<FotoComponent[]> {
// código posterior omitido

Por fim, o http.post no final das contas retorna um Observable . Será que é do tipo
FotoComponent também? Que tal fazermos um teste e verificarmos se o compilador do TypeScript
aprova? Bem, ele não aprova este tipo e diz que o tipo esperado era Response . Para usar o tipo
Response precisamos importar sua classe do pacote angular/http .

9.2 EXERCÍCIO - CRIANDO UM SERVIÇO


1.Crie um arquivo para o serviço caelumpic/src/app/foto/foto.service.ts :
// caelumpic/src/app/foto/foto.service.ts
import { Http, Headers, Response } from '@angular/http';
import { FotoComponent } from './foto.component';
import {Observable} from 'rxjs';

9.2 EXERCÍCIO - CRIANDO UM SERVIÇO 89


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
export class FotoService {

http: Http;
headers: Headers;
url: string = 'http://localhost:3000/v1/fotos';

constructor(http: Http) {

this.http = http;
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
}

lista(): Observable<FotoComponent[]> {

return this.http.get(this.url)
.map(res => res.json());
}

cadastra(foto: FotoComponent): Observable<Response> {

return this.http.post(this.url, JSON.stringify(foto),


{ headers: this.headers });
}
}

2.Agora adicione FotoService ao módulo FotoModule . Utilizaremos a propriedade providers


para registrar nosso serviço:
// caelumpic/src/app/foto/foto.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FotoComponent } from './foto.component';
import { FiltroPorTitulo } from './foto.pipes';
import { FotoService } from './foto.service';

@NgModule({
imports: [ CommonModule ],
declarations: [ FotoComponent, FiltroPorTitulo ],
exports: [FotoComponent, FiltroPorTitulo ],
providers: [ FotoService ]
})
export class FotoModule { }

O próximo passo é usarmos o serviço que acabamos de criar em ListagemComponent e


CadastroComponent .

9.3 EXERCÍCIO - UTILIZANDO O SERVIÇO


1.Primeiro vamos alterar ListagemComponent, substitua a dependência de Http por
FotoService , mas para isso precisamos importar FotoService antes. Mude o tipo da propriedade
fotos que estava Object[] para FotoComponent[] já que o método FotoService.lista
retornará uma lista do tipo FotoComponent :

// caelumpic/src/app/listagem/listagem.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';

90 9.3 EXERCÍCIO - UTILIZANDO O SERVIÇO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
import { FotoService } from '../foto/foto.service';

@Component({
selector: 'listagem',
templateUrl: './listagem.component.html'
})
export class ListagemComponent {

fotos: FotoComponent[] = [];

constructor(service: FotoService) {

service.lista()
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}
}

2.Agora use o FotoService em CadastroComponent também:


// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { FotoService } from '../foto/foto.service';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


service: FotoService;
meuForm: FormGroup;

constructor(service: FotoService, fb: FormBuilder) {

this.service = service;

this.meuForm = fb.group({
titulo: ['', Validators.compose(
[Validators.required, Validators.minLength(4)]
)],
url: ['', Validators.required],
descricao: [''],
});
}

cadastrar(event) {
event.preventDefault();
console.log(this.foto);

this.service.cadastra(this.foto)
.subscribe(() => {
this.foto = new FotoComponent();
console.log('Foto salva com sucesso');
}, erro => {
console.log(erro);
});

9.3 EXERCÍCIO - UTILIZANDO O SERVIÇO 91


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
}
}

Quando rodamos nossa aplicação, recebemos a seguinte mensagem de erro:

Error: Can't resolve all parameters for FotoService: (?).

9.4 INJECTABLE
Este erro indica que o sistema de injeção do Angular não conseguiu buscar a dependência do nosso
serviço. Ele só conseguirá fazer isso se usarmos o decorator @Injectable do Angular na classe do
serviço, por exemplo:
//codigo anterior omitido
import { Injectable } from '@angular/core';

@Injectable()
export class FotoService {
// código posterior omitido

9.5 EXERCÍCIO - SERVIÇO COMO DEPENDÊNCIA


1.Altere // caelumpic/src/app/foto/foto.service.ts, importe Injectable do Angular e use seu
decorator antes da classe FotoService:
// caelumpic/src/app/foto/foto.service.ts
import { Http, Headers, Response } from '@angular/http';
import { FotoComponent } from './foto.component';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';

@Injectable()
export class FotoService {
// código posterior omitido

Agora sim! Nossa aplicação continua funcionando. Agora que temos tudo no lugar, podemos
implementar a exclusão e alteração do nosso cadastro.

92 9.4 INJECTABLE
Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 10

REMOVENDO DADOS INCONSISTENTES

Durante a criação do cadastro de fotos realizamos vários testes, muitas vezes gravando fotos com nomes
esquisitos e muitas vezes com a URL da imagem inválida. Vamos implementar a funcionalidade de
exclusão de fotos começando pela adição do botão Remover que ficará dentro de PainelComponent e
logo abaixo de FotoComponent :
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<!-- código anterior omitido -->
<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo: textoProcurado.value"
titulo="{{foto.titulo | uppercase}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
<button class="btn btn-danger btn-block" >Remover</button>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

Sabemos que em algum momento esse botão terá que chamar algum método do componente
ListagemComponent para realizar a operação de exclusão. Vamos criá-lo:

// caelumpic/src/app/listagem/listagem.component.ts
// código anterior omitido
export class ListagemComponent {
// código anterior omitido
remove(): void {
console.log('Olá, você acabou de chamar o método remove :)');
}
}

10.1 MAIS UMA ASSOCIAÇÃO DE EVENTOS (EVENT BINDING)


É claro que ainda falta implementarmos o método remove do componente ListagemComponent ,
mas primeiro precisamos resolver outro problema, a associação do clique do botão com o método
remove . Lembre-se que o Angular possui diferentes tipos de associação (binding), desta vez, queremos
realizar uma associação de evento (event binding).

Já aprendemos a realizar esse tipo de associação que flui da nossa view para o componente quando
implementamos o filtro da nossa lista. Neste tipo de associação, adicionamos o nome do evento entre
parênteses como atributo do elemento. Sendo assim, vamos adicionar o atributo (click) no botão de
"Remover". A diferença desta vez é que o valor do atributo será uma expressão e não apenas "0" como
fizemos com o evento input do nosso filtro.

10 REMOVENDO DADOS INCONSISTENTES 93


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
A expressão que utilizaremos é "remove()" , a chamada o método remove . Angular entende que o
escopo da nossa view é o componente por isso encontrará o método em ListagemComponent :
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<!-- código anterior omitido -->
<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo: textoProcurado.value" titulo="{{foto.titulo
| uppercase}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
<button class="btn btn-danger btn-block" (click)="remove()" >Remover</button>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->'

Já podemos testar e verificar se o método é chamado verificando a saída no console do browser.


Agora, precisamos implementar o método para que realmente remova a nossa foto, porém como ele
saberá qual foto estamos removendo?

Lembre-se que a diretiva estrutural ngFor declara uma variável local chamada foto . Sendo uma
variável, pode ser acessada dentro de qualquer expressão. Que tal se passarmos a variável como
parâmetro do método remove ?:

<!-- código anterior omitido -->


<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo: textoProcurado.value" titulo="{{foto.titulo
| uppercase}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
<button class="btn btn-danger btn-block" (click)="remove(foto)">Remover</button>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

Quando clicarmos no botão "Remover" da terceira foto, por exemplo, a função remove receberá o
objeto foto usado para construir o terceiro painel e assim por diante. Contudo, nosso método ainda não
recebe como parâmetro uma foto. Vamos adicioná-lo, inclusive já definindo seu tipo:

// caelumpic/src/app/listagem/listagem.component.ts
// código anterior omitido
export class ListagemComponent {

fotos: FotoComponent[] = [];

constructor(service: FotoService) {

service.lista()
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}

remove(foto: FotoComponent): void {


console.log(foto.titulo);
}
}

Veja que agora estamos imprimindo o título da foto no console. Um teste mostra pefeitamente que

94 10.1 MAIS UMA ASSOCIAÇÃO DE EVENTOS (EVENT BINDING)


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
se clicarmos para remover a segunda foto, será impresso o título da segunda foto no console.

10.2 EXERCÍCIO - UM BOTÃO PARA REMOVER FOTOS


1.Adicione no template o código do botão:
<!-- código anterior omitido -->
<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo: textoProcurado.value" titulo="{{foto.titulo
| uppercase}}" class="col-md-2">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
<button class="btn btn-danger btn-block" (click)="remove(foto)">Remover</button>
</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

2.Em ListagemComponent crie um novo método para remover uma foto:

// caelumpic/src/app/listagem/listagem.component.ts
// código anterior omitido
export class ListagemComponent {

fotos: FotoComponent[] = [];

constructor(service: FotoService) {

service.lista()
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}

remove(foto: FotoComponent): void {


console.log(foto.titulo);
}
}

10.3 SERVIÇO DE REMOÇÃO


Excelente, agora que já temos a foto em nosso método podemos solicitar ao nosso servidor que
remova a foto.

A boa notícia é que já temos um FotoService disponível para uso, contudo ele ainda não possui
um método específico para solicitar ao nosso servidor a remoção de fotos. É necessário implementar o
método remove , exemplo:
// caelumpic/src/app/foto/foto.service.ts
import { Http, Headers, Response } from '@angular/http';
import { FotoComponent } from './foto.component';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';

@Injectable()
export class FotoService {
// código anterior omitido

10.2 EXERCÍCIO - UM BOTÃO PARA REMOVER FOTOS 95


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
remove(foto: FotoComponent): Observable<Response> {
return this.http.delete(this.url);
}
}

Usamos o http.delete que utiliza por baixo dos panos o verbo DELETE. Veja que também
passamos a this.url como parâmetro, a URL que aponta para nosso recurso de fotos no servidor.
Contudo temos um problema. Em nenhum momento estamos indicando nessa URL a foto que
desejamos que o servidor apague para nós. Faremos indicação completando a URL com o ID da foto que
desejamos remover. A razão disso é que nosso servidor está pronto para lidar com requisições do tipo
DELETE com essa estrutura:
http://localhost:3000/v1/fotos/3
http://localhost:3000/v1/fotos/15

Quando o verbo DELETE é empregado, nosso servidor entende que tudo que vem depois de
http://localhost:3000/v1/fotos/ é o ID da foto que ele deve remover. Sendo assim, precisamos
concatenar o ID da foto que recebemos em nosso método com a URL.
remove(foto: FotoComponent): Observable<Response> {
return this.http.delete(this.url + '/' + foto._id);
}

Como adicionamos o tipo FotoComponent no parâmetro foto , só teremos acesso à _id se ela
for uma propriedade de FotoComponent . Toda foto trazida do servidor vem com esse ID gerado
automaticamente pelo servidor. Temos que ter a propriedade _id em foto.component.ts , exemplo:

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


@Component({
selector: 'foto',
templateUrl: './foto.component.html',
styleUrls: ['./foto.component.css']
})
export class FotoComponent {

@Input() titulo: string;


@Input() url: string;
descricao: string;
_id: string;
}

Agora que já temos o método implementado, vamos voltar para componente ListagemComponent
e concluir o método remove . Só não podemos nos esquecer de guardar o serviço injetado em uma
propriedade da classe para que possamos utilizá-la em remove .

10.4 EXERCÍCIO - IMPLEMENTANDO O SERVIÇO DE REMOÇÃO


1.Vamos alterar caelumpic/src/app/foto/foto.service.js e implementar o método remove :
// caelumpic/src/app/foto/foto.service.ts
import { Http, Headers, Response } from '@angular/http';
import { FotoComponent } from './foto.component';
import { Observable } from 'rxjs';

96 10.4 EXERCÍCIO - IMPLEMENTANDO O SERVIÇO DE REMOÇÃO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
import { Injectable } from '@angular/core';

@Injectable()
export class FotoService {
// código anterior omitido
remove(foto: FotoComponent): Observable<Response> {
return this.http.delete(this.url + '/' + foto._id);
}
}

2.Adicione uma propriedadede _id na classe FotoComponent:

//caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'foto',
templateUrl: './foto.component.html',
styleUrls: ['./foto.component.css']
})
export class FotoComponent {

@Input() titulo: string;


@Input() url: string;
descricao: string;
_id: string;
}

3.Altere ListagemComponent para usar o serviço de remoção:


// caelumpic/src/app/listagem/listagem.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';
import { FotoService } from '../foto/foto.service';
import { FotoComponent } from '../foto/foto.component';

@Component({
selector: 'listagem',
templateUrl: './listagem.component.html'
})
export class ListagemComponent {

fotos: FotoComponent[] = [];


service: FotoService;

constructor(service: FotoService) {
this.service = service;
this.service.lista()
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}

remove(foto: FotoComponent): void {

this.service.remove(foto)
.subscribe(
fotos => console.log('foto removida com sucesso'),
erro => console.log(erro)
);
}
}

10.4 EXERCÍCIO - IMPLEMENTANDO O SERVIÇO DE REMOÇÃO 97


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
10.5 UM POUCO SOBRE CHANGE DETECTION
Com nosso código em TypeScript compilado sem erros e com o servidor rodando vamos realizar um
novo teste. Uma coisa estranha acontece: quando clicamos botão a foto não é removida, mas se não
forçamos o recarregamento da página, a foto que acabamos de remover continua lá. Curioso.

O problema é que estamos removendo corretamente a foto do servidor, mas não estamos
atualizando a lista de fotos que alimenta nosso template. Uma solução é remover o item da lista apenas
quando a operação de deleção no servidor for bem sucedida. Para isso, podemos usar o boa e velha
conhecida função splice para remover a foto deletada da lista. Exemplo:
remove(foto: FotoComponent): void {

this.service.remove(foto)
.subscribe(
fotos => {
let indiceDaFoto = this.fotos.indexOf(foto);
this.fotos.splice(indiceDaFoto, 1);
console.log('Foto removida com sucesso');
},
erro => console.log(erro));
}

Porém, o Angular apenas monitora a referência de this.fotos do componente. Incluir ou


remover um novo item da lista não é o suficiente para que a mudança seja percebida. Para isso,
precisamos criar uma nova lista e atribuir essa lista em this.fotos . Quando reatribuimos o valor de
uma variável o Angular ativa seu mecanismo de detecção de mudança e renderiza novamente a view.

10.6 EXERCÍCIO - ATUALIZANDO LISTAGEM DE FOTOS


1.Atualize o método remove de ListagemComponent para que atualize a lista de fotos após remover
uma foto:
// caelumpic/src/app/listagem/listagem.component.ts
import { Component } from '@angular/core';
import { FotoService } from '../foto/foto.service';
import { FotoComponent } from '../foto/foto.component';

@Component({
selector: 'listagem',
templateUrl: './listagem.component.html'
})
export class ListagemComponent {

fotos: FotoComponent[] = [];


service: FotoService;

constructor(service: FotoService) {

this.service = service;
this.service
.lista()
.subscribe(fotos => {

98 10.5 UM POUCO SOBRE CHANGE DETECTION


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
this.fotos = fotos;
}, erro => console.log(erro));
}

remove(foto) {

this.service.remove(foto)
.subscribe(
(fotos) => {
let novasFotos = this.fotos.slice(0);
let indice = novasFotos.indexOf(foto);
novasFotos.splice(indice, 1);
this.fotos = novasFotos;
console.log('Foto removida com sucesso');
},
erro => console.log(erro)
);

}
}

10.7 EXIBINDO MENSAGENS PARA O USUÁRIO


Nossa aplicação já remove fotos, mas que tal exibirmos uma mensagem para o usuário dizendo que a
foto de nome tal foi removida?

Vamos adicionar parágrafo no template de ListagemComponent que exibirá a mensagem apenas se


ela for definida. Aprendemos a utilizar a diretiva ngIf para exibição condicional.

Na direiva ngIf usamos a expressão "mensagem.length". Em JavaScript, 0 é avaliado como falso,


sendo assim, se nenhuma mensagem for definida na propriedade mensagem ela terá como valor uma
string vazia e seu length será 0. Dessa forma, só exibiremos o parágrafo quando mensagem conter
uma string diferente de branco.

10.8 EXERCÍCIO - CONFIGURANDO MENSAGENS


1.O primeiro passo é adicionarmos a propriedade mensagem em ListagemComponent que
guardará a mensagem de remoção:
// caelumpic/src/app/listagem/listagem.component.ts
// código anterior omitido
export class ListagemComponent {

fotos: FotoComponent[] = [];


service: FotoService;
mensagem: string = '';

constructor(service: FotoService) {

this.service = service;
this.service
.lista()
.subscribe(fotos => {
this.fotos = fotos;

10.7 EXIBINDO MENSAGENS PARA O USUÁRIO 99


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
}, erro => console.log(erro));

remove(foto) {

this.service
.remove(foto)
.subscribe(
() => {

let novasFotos = this.fotos.slice(0);


let indice = novasFotos.indexOf(foto);
novasFotos.splice(indice, 1);
this.fotos = novasFotos;
this.mensagem = 'Foto removida com sucesso';
},
erro => {
console.log(erro);
this.mensagem = 'Não foi possível remover a foto';
}
);
}
}

2.Alteramos o template da ListagemComponent adicionando o elemento que conterá as mensagem


junto a diretiva *ngIf:

<!-- caelumpic/src/app/listagem/listagem.component.html -->


<div class="jumbotron">
<h1 class="text-center">CaelumPic</h1>
</div>
<div class="container">
<p *ngIf="mensagem.length" class="alert alert-info">{{mensagem}}</p>
<!-- código posterior omitido -->

100 10.8 EXERCÍCIO - CONFIGURANDO MENSAGENS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 11

ALTERANDO FOTOS CADASTRADAS

Hoje temos CadastroComponent que possui um formulário para incluirmos novas fotos em nossos
sistema. Contudo, apenas incluir não é suficiente, precisamos também alterar fotos. No lugar de
criarmos um novo componente com a finalidade de alteração, utilizaremos o mesmo.

A ideia é selecionarmos uma foto em ListagemComponent através de um click e seus dados serem
exibidos em CadastroComponent para que possamos realizar as alterações necessárias. Em
ListagemComponent , vamos tornar nosso FotoComponent clicável envolvendo-o pela tag <a> ,
inclusive já utilizaremos a diretiva routerLink para realizar uma navegação para Cadastro .

Porém, se recarregamos nossa página e clicarmos no link somos levados para o componente
CadastroComponent , e nosso formulário não é preenchido com os dados da foto que selecionamos.
Uma maneira de preencher o formulário é buscarmos do servidor a foto que foi clicada, mas precisamos
pelo menos de algum identificador. Nosso servidor esta preparado para retornar uma foto dado o seu ID
através de requisições como:

http://localhost:3000/v1/fotos/10
http://localhost:3000/v1/fotos/7

Veja que é uma URL idêntica a que usamos para remover fotos, só que dessa vez nosso servidor está
preparado para responder ao verbo GET e não DELETE.

11.1 ROTAS PARAMETRIZADAS


Precisamos fazer com que CadastroComponent receba o ID da foto que clicamos em
ListagemComponent , mas se digitarmos no navegador endereços como
localhost:4200/cadastro/10 somos redirecionados para ListagemComponent , porque o endereço
não é uma rota válida. Apesar disso, podemos alterar a configuração da nossa rota fazendo com que ela
aceite parâmetro. Veja um exemplo:

//codigo anterior omitido


const appRoutes: Routes = [
{ path: '', component: ListagemComponent },
{ path: 'cadastro/:id', component: CadastroComponent },
{ path: '**', redirectTo: ''}
];
//codigo posterior omitido

Veja que alteramos o path na configuração de nossas rotas. Temos o valor cadastro/:id . O

11 ALTERANDO FOTOS CADASTRADAS 101


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
:id é um coringa como aqueles de baralho que podem assumir diferentes valores. Sendo assim, rotas
como /cadastro/1 ou /cadastro/10 serão aceitas, inclusive seremos capazes de acessar o valor do
"coringa" :id em CadastroComponent .

É possível testar digitando diretamente no browser a URL:


localhost:4200/cadastro/5

Com isso página de cadastro é exibida, porém resolvemos um problema e criamos outro. Se
acessarmos a URL localhost:4200/cadastro ou clicamos no botão "Nova foto", somos direcionados
para o componente Principal , porque a rota que acessamos não é mais válida. Precisamos que ela
continue existindo, porque quando estamos cadastrando uma nova foto, não temos ID ainda e
precisamos acessar CadastroComponent sem passar um ID.

Enfim, a configuração das nossas rotas devem conter o dois cenários, veja o exemplo:
//codigo anterior omitido
const appRoutes: Routes = [
{ path: '', component: ListagemComponent },
{ path: 'cadastro', component: CadastroComponent },
{ path: 'cadastro/:id', component: CadastroComponent },
{ path: '**', redirectTo: ''}
];
//codigo posterior omitido

Agora conseguimos acessar CadastroComponent tanto para a inclusão quanto para alteração de
fotos, mas ainda temos um problema a resolver. Como teremos acesso ao ID da foto quando este for
passado para o componente CadastroComponent ? Lembre-se que ele precisa dessa informação para
buscar a foto que desejamos alterar do servidor.

11.2 EXERCÍCIO - AJUSTANDO NOSSAS ROTAS


1.Adicione a tag <a> com a diretiva [routerLink] para acessarmos o Cadastro:
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<!-- código anterior omitido -->
<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo: textoProcurado.value" titulo="{{foto.titulo
| uppercase}}" class="col-md-2">
<a [routerLink]="['/cadastro', foto._id]">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</a>

<button class="btn btn-danger btn-block" (click)="remove(foto)">Remover</button>


</painel>
</div><!-- fim row -->
<!-- código posterior omitido -->

2.Precisamos alterar nossa configuração de rotas em app.routes.ts :


// caelumpic/src/app/app.routes.ts
import { RouterModule, Routes } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';

102 11.2 EXERCÍCIO - AJUSTANDO NOSSAS ROTAS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
import { CadastroComponent } from './cadastro/cadastro.component';

const appRoutes: Routes = [


{ path: '', component: ListagemComponent },
{ path: 'cadastro', component: CadastroComponent },
{ path: 'cadastro/:id', component: CadastroComponent },
{ path: '**', redirectTo: ''}
];

export const routing = RouterModule.forRoot(appRoutes);

11.3 BUSCANDO PELO PARÂMETRO


Podemos ter acesso aos parâmetros da rota através do serviço ActivatedRoute. Para perdirmos ao
framework este serviço temos que importar ActivatedRoute e indicá-lo como dependência em nossa
classe CadastroComponent , fazendo com que o sistema de injeção de dependências do Angular injete-o
para nós. Inclusive vamos adicionar a propriedade mensagem assim como fizemos em
CadastroComponent para exibirmos notificações para o usuário.

// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { FotoService } from '../foto/foto.service';
import { ActivatedRoute } from '@angular/router';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


service: FotoService;
meuForm: FormGroup;
route: ActivatedRoute; // nova propriedade
mensagem: string = ''; // nova propriedade

constructor(service: FotoService, fb: FormBuilder, route: ActivatedRoute) {

this.route = route;

// código posterior omitido

Através da instância de ActivatedRoute conseguimos obter o parâmetro passado pela rota da


seguinte maneira:
this.route.params.subscribe(params => console.log(params['id']));

Testando no browser a URL localhost:4200/cadastro/10 . Um olhar atento no console mostra


que o ID que passamos como parâmetro para a URL é exibido. Perfeito, mas precisamos alterar o nosso
routerLink que utilizamos na tag <a> que envolve nossa foto. A ideia é que ao ser clicado, ele
construa o endereço para nós levando em consideração o ID da foto:

11.3 BUSCANDO PELO PARÂMETRO 103


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<div class="row">
<painel *ngFor="let foto of fotos | filtroPorTitulo: textoProcurado.value" titulo="{{foto.titulo
| uppercase}}" class="col-md-2">

<a [routerLink]="['/cadastro', foto._id]">


<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</a>

<button class="btn btn-danger btn-block" (click)="remove(foto)">Remover</button>


</painel>
</div><!-- fim row -->

Agora, para cada foto clicada, seremos levados para CadastroComponent e este terá acesso ao ID da
foto que clicamos. Precisamos agora buscar a foto no servidor.

Só podemos buscar a foto apenas se Cadastro recebeu o ID de alguma foto, claro. O código é
muito parecido com o que já fizemos.
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { FotoService } from '../foto/foto.service';
import { ActivatedRoute } from '@angular/router';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


service: FotoService;
meuForm: FormGroup;
route: ActivatedRoute;
mensagem: string = '';

constructor(service: FotoService, fb: FormBuilder, route: ActivatedRoute) {

this.route = route;
this.service = service;

this.route.params.subscribe(params => {

let id = params['id'];

if(id) {
this.service.buscaPorId(id)
.subscribe(
foto => this.foto = foto,
erro => console.log(erro));
}
});

// código posterior omitido

Nosso código não compila porque o método buscaPorId ainda não existe em FotoService .
Vamos criá-lo:

104 11.3 BUSCANDO PELO PARÂMETRO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
buscaPorId(id: string): Observable<FotoComponent> {

return this.http
.get(this.url + '/' + id)
.map(res => res.json());
}

// código posterior omitido

Pronto. Para cada foto que acessarmos em ListagemComponent , seus dados são exibidos em
CadastroComponent . No entanto, podemos melhorar a legibilidade do nosso código. Para não deixar
um monte de código perdido dentro do nosso construtor, vamos criar um método com um nome que
deixa bem claro o que o código faz. Vamos chamá-lo de buscaPorId e fazer com que ele receba o ID da
foto que desejamos buscar.

11.4 INCLUSÃO OU ALTERAÇÃO?


Ainda não acabamos. FotoService só está preparado para incluir fotos e não alterá-las.

ALterando o método cadastra de FotoService realizando uma verificação. Caso a foto recebida
como parâmetro não tenha um ID, uma inclusão será realizada, mas caso ela já tenha essa informação,
será realizada uma alteração:

// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
@Injectable()
export class FotoService {
// código anterior omitido
cadastra(foto: FotoComponent): Observable<Response> {

if (foto._id) {
return this.http.put(this.url + '/' + foto._id, JSON.stringify(foto),
{ headers: this.headers });
} else {
return this.http.post(this.url, JSON.stringify(foto),
{ headers: this.headers });
}
}

// código posterior omitido

Quando usamos http.put , passamos como parâmetro a URL


http://localhost:3000/v1/foto/ concatenada com o ID da foto que o método cadastra recebeu.
O segundo parâmetro é um JSON com os dados que queremos atualizar no servidor.

11.5 EXERCÍCIO: ABRINDO A FOTO PARA ALTERÁ-LA


1.Adicione ActivatedRoute como dependência, e crie uma propriedade para acessarmos ele.
Adicione também a propriedade mensagem para exibirmos notificações para o usuário.

11.4 INCLUSÃO OU ALTERAÇÃO? 105


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { FotoService } from '../foto/foto.service';
import { ActivatedRoute } from '@angular/router';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


service: FotoService;
meuForm: FormGroup;
route: ActivatedRoute; // nova propriedade
mensagem: string = ''; // nova propriedade

constructor(service: FotoService, fb: FormBuilder, route: ActivatedRoute) {

this.route = route;
this.service = service;

this.route.params.subscribe(params => console.log(params['id']));

// código posterior omitido

2.Adicione a mensagem condicional usando *ngIf em CadastroComponent :

<!-- caelumpic/src/app/cadastro/cadastro.component.html -->


<div class="container">
<h1 class="text-center">{{foto.titulo}}</h1>
<p *ngIf="mensagem.length" class="alert alert-info">{{mensagem}}</p>
<!-- código posterior omitido -->

Teste uma rota como localhost:4200/cadastro/10 , veja que no console do navegador já


conseguimos obter o ID passado pela rota.
3.Vamos criar o método buscaPorId em FotoService para poder abrir a foto no template de
Cadastro:
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
buscaPorId(id: string): Observable<FotoComponent> {

return this.http
.get(this.url + '/' + id)
.map(res => res.json());
}

// código posterior omitido

4.Utilize o método buscaPorId no CadastroComponent:

// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

106 11.5 EXERCÍCIO: ABRINDO A FOTO PARA ALTERÁ-LA


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
import { FotoService } from '../foto/foto.service';
import { ActivatedRoute } from '@angular/router';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


service: FotoService;
meuForm: FormGroup;
route: ActivatedRoute;
mensagem: string = '';

constructor(service: FotoService, fb: FormBuilder, route: ActivatedRoute) {

this.route = route;
this.service = service;

this.route.params.subscribe(params => {

let id = params['id'];

if(id) {
this.service.buscaPorId(id)
.subscribe(
foto => this.foto = foto,
erro => console.log(erro));
}
});

// código posterior omitido

5.Em FotoService altere o método cadastra para que decida quando criar uma foto nova ou alterar a
atual:

// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
@Injectable()
export class FotoService {
// código anterior omitido
cadastra(foto: FotoComponent): Observable<Response> {

if (foto._id) {
return this.http.put(this.url + '/' + foto._id, JSON.stringify(foto),
{ headers: this.headers });
} else {
return this.http.post(this.url, JSON.stringify(foto),
{ headers: this.headers });
}
}

// código posterior omitido

Pronto! Todas as alterações que fizemos devem ser aplicadas em nosso servidor. Quando voltamos
para ListagemComponent vemos a foto com seus dados alterados.

11.6 EXERCÍCIO OPCIONAL - NAVEGAÇÃO NO LADO DO

11.6 EXERCÍCIO OPCIONAL - NAVEGAÇÃO NO LADO DO COMPONENTE 107


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
COMPONENTE
Podemos melhorar a experiência do usuário voltando automaticamente para ListagemComponent
toda vez que uma foto for alterada. Teremos que realizar uma navegação através do método cadastrar
de CadastroComponent . Mas como realizaremos essa navegação uma vez que o routerLink é usado
apenas em nosso template? Para isso podemos usar o Router :

1.Importe Router no CadastroComponent e use ele no construtor:


// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
import { Http, Headers } from '@angular/http';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { FotoService } from '../foto/foto.service';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {

foto: FotoComponent = new FotoComponent();


service: FotoService;
meuForm: FormGroup;
route: ActivatedRoute;
mensagem: string = '';
router: Router;

constructor(service: FotoService, fb: FormBuilder, route: ActivatedRoute, router: Router) {

this.router = router;
this.route = route;
this.service = service;

// código posterior omitido

Agora que temos uma instância de Router podemos solicitar ao método navigate uma
navegação. O método recebe como parâmetro um array com um string que é nossa rota configurado em
app.routes.ts :

2.Insira o método navigate de Router no método Cadastrar de CadastroComponent:

// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
cadastrar(event) {
event.preventDefault();
console.log(this.foto);

this.service.cadastra(this.foto)
.subscribe(() => {
this.foto = new FotoComponent();
this.router.navigate(['']); //Método navigate aqui!!!
}, erro => {
console.log(erro);
});
}

108 11.6 EXERCÍCIO OPCIONAL - NAVEGAÇÃO NO LADO DO COMPONENTE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
}

Experimente alterar algumas fotos. Você deve ter reparado que tanto na alteração quanto na inclusão
estamos sendo direcionados para ListagemComponent e que isso deveria acontecer apenas com a
alteração. Não se preocupe com isso por agora, pois veremos uma solução no próximo capítulo.

11.6 EXERCÍCIO OPCIONAL - NAVEGAÇÃO NO LADO DO COMPONENTE 109


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 12

MODIFICADORES DE ACESSO E
ENCAPSULAMENTO

Nossa aplicação consegue incluir, alterar e remover, porém ainda não está 100%. Veja que, tanto para
uma inclusão quanto para uma alteração estamos navegando para ListagemComponent . No entanto,
queremos que o usuário continue em CadastroComponent caso ele tenha feito uma inclusão
permitindo assim que ele cadastre mais fotos.

Outro ponto é que a mensagem de inclusão ou alteração são definidas em CadastroComponent . Se


usarmos FotoService em outro local da nossa aplicação teremos que repetir as mesmas mensagens.

12.1 ISOLANDO REPONSABILIDADES


Uma solução é fazer com que o método cadastra de FotoService passe a retornar um objeto
que contenha duas propriedades para guardar a mensagem e um boleano para indicar se foi inclusão ou
não. Hoje nosso método retorna um Observable<Response> , com a nossa alteração ele passará a
retorna um Observable<Object> :
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
cadastra(foto: FotoComponent): Observable<Object> {

if(foto._id) {
return this.http.put(this.url + '/' + foto._id, JSON.stringify(foto),
{ headers: this.headers })
.map(() => ({mensagem: 'Foto alterada com sucesso', inclusao: false}));

} else {
return this.http.post(this.url, JSON.stringify(foto),
{ headers: this.headers })
.map(() => ({mensagem: 'Foto incluída com sucesso', inclusao: true}));
}
}

Tanto na alteração quanto na inclusão a função map agora retorna um objeto com as propriedades
mensagem e inclusao . O primeiro contém a mensagem da operação e o segundo true se for uma
inclusão e false se for uma alteração. Você deve ter achado um tanto estranho envolver nosso objeto
entre parênteses, mas essa é uma exigência quando retornamos em uma única linha um objeto {} . Se
não envolvermos com parênteses nossa arrow function confundirá as chaves do objeto com a chave de
um bloco resultando em um erro de compilação.

110 12 MODIFICADORES DE ACESSO E ENCAPSULAMENTO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
cadastrar() {

this.service.cadastra(this.foto)
.subscribe(res => {
this.mensagem = res.mensagem;
this.foto = new FotoComponent();
if(!res.inclusao) this.router.navigate(['']);
}, erro => {
console.log(erro);
this.mensagem = 'Não foi possível salvar a foto';
});
}

12.2 UM POUCO MAIS SOBRE TIPAGEM E O TIPO ANY


Temos um erro de compilação no método cadastra de CadastroComponent . O TypeScript é bem
enfático e nos diz que res.mensagem e res.inclusao não existem para o tipo Object . Realmente, o
tipo Object não possui qualquer um desses métodos. Como resolver?

Em TypeScript podemos usar o tipo any para indicar tipo retornado é qualquer tipo. Na verdade ele
foi adicionados aos tipos deste superset do ES6 para possibilitar sua introdução em sistema legados,
quando não há uma maneira clara de especificar o tipo. Vamos alterar o tipo de retorno de cadastro
em FotoService :
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
cadastra(foto: FotoComponent): Observable<any> {
// código omitido
}

Com essa alteração nosso código compila perfeitamente, no entanto não temos recurso de
autocomplete muito menos checagem de tipos.

12.3 EXERCÍCIO - ISOLANDO AS MENSAGENS


1.Adicione um retorno no método cadastra de FotoService, e adicione um .map com o objeto de
retorno:
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
cadastra(foto: FotoComponent): Observable<any> {

if(foto._id) {
return this.http.put(this.url + '/' + foto._id, JSON.stringify(foto),
{ headers: this.headers })
.map(() => ({mensagem: 'Foto alterada com sucesso', inclusao: false}));

} else {
return this.http.post(this.url, JSON.stringify(foto),
{ headers: this.headers })
.map(() => ({mensagem: 'Foto incluída com sucesso', inclusao: true}));
}

12.2 UM POUCO MAIS SOBRE TIPAGEM E O TIPO ANY 111


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
}

2.Altere o método cadastrar de CadastroComponent usando nosso objeto de resposta:


// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
cadastrar() {
this.service.cadastra(this.foto)
.subscribe(
res => {
this.mensagem = res.mensagem;
this.foto = new FotoComponent();
if(!res.inclusao) this.router.navigate(['']);
},
erro => {
console.log(erro);
this.mensagem = 'Não foi possível savar a foto';
});
}

12.4 UMA CLASSE PARA AS MENSAGENS


Uma das grandes vantagem do TypeScript é justamente o que acabamos de perder. Será que
podemos resolver nosso problema e ainda usar essas vantagens da linguagem? Sim podemos, mas para
isso precisaremos criar um novo tipo, isto é, uma nova classe.

No final do arquivo caelumpic/src/app/foto/foto.service.ts vamos criar e exportar outra


classe chamada MensagemCadastro .
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
// veja que `foto.service.ts` agora exporta duas classes, FotoService e MensagemCadastro!
export class MensagemCadastro {

mensagem: string;
inclusao: boolean;

constructor(mensagem: string, inclusao: boolean) {


this.mensagem = mensagem;
this.inclusao = inclusao;
}
}

Nossa classe possui as propriedades mensagem e inclusao e um construtor. Agora, vamos utilizá-
la como retorno do método cadastra de FotoService :
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
cadastra(foto: FotoComponent): Observable<MensagemCadastro> {

if(foto._id) {
return this.http.put(this.url + '/' + foto._id, JSON.stringify(foto),
{ headers: this.headers })
.map(() => new MensagemCadastro('Foto alterada com sucesso', false));

} else {
return this.http.post(this.url, JSON.stringify(foto),

112 12.4 UMA CLASSE PARA AS MENSAGENS


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
{ headers: this.headers })
.map(() => new MensagemCadastro('Foto incluída com sucesso', true));
}
}

Tanto para a inclusão e alteração passamos para a função map do nosso observable stream uma
instância da classe MensagemCadastro que recebe a mensagem específica de cada operação, inclusive
indicado se é uma inclusão ou não.

Perfeito! Agora em CadastroComponent , podemos usar o autocomplete porque o TypseScript


agora sabe o tipo. Veja que também usamos a mensagem retornada pelo objeto MensagemCadastro .

12.5 TYPESCRIPT E O FAVORECIMENTO DO ENCAPSULAMENTO


A solução funciona, mas faz sentido podemos alterar a mensagem de MensagemCadastro ? Em
nosso sistema não faz, mas ninguém impede do programador fazer algo do tipo:
// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
cadastrar() {

this.service.cadastra(this.foto)
.subscribe(
res => {
res.mensagem = 'Esta é minha nova mensagem'; // não deveria permitir adultera
r a mensagem que veio do servidor
this.mensagem = res.mensagem;
this.foto = new FotoComponent();
if(!res.inclusao) this.router.navigate(['']);
},
erro => {
console.log(erro);
this.mensagem = 'Não foi possível savar a foto';
});
}

Isso acontece porque o modificador de acesso padrão de toda propriedade de uma classe é public,
podemos até deixar implícito esse modificador padrão em nosso código se assim desejarmos:
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
export class MensagemCadastro {

public mensagem: string; // tornando explícito o modificador de acesso public


public inclusao: boolean; // tornando explícito o modificador de acesso public

constructor(mensagem: string, inclusao: boolean) {


this.mensagem = mensagem;
this.inclusao = inclusao;
}
}

O primeiro passo para impedirmos que uma mensagem devolvida por FotoService seja
adulterada é escondermos as propriedades mensagem e inclusao de todas as outras classes que não
seja a própria classe `FotoService. Conseguimos isso através do modificador de acesso private:

12.5 TYPESCRIPT E O FAVORECIMENTO DO ENCAPSULAMENTO 113


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
export class MensagemCadastro {

private mensagem: string;


private inclusao: boolean;

constructor(mensagem: string, inclusao: boolean) {


this.mensagem = mensagem;
this.inclusao = inclusao;
}
}

Excelente, ninguém mais poderá alterar as propriedades mensagem e inclusao de uma instância
de MensagemCadastro , contudo isso nos trouxe um problema. Nosso CadastroComponent precisa
consultar as duas propriedades e agora como elas estão encapsuladas em MensagemCadastro o
compilador do TypeScript nos dá a seguinte mensagem de erro:
File change detected. Starting incremental compilation...
app/cadastro/cadastro.component.ts(56,37): error TS2341: Property 'mensagem' is private and only acce
ssible within class 'MensagemCadastro'.
app/cadastro/cadastro.component.ts(58,25): error TS2341: Property 'inclusao' is private and only acce
ssible within class 'MensagemCadastro'.

12.6 O MODIFICADOR PRIVATE


Queremos proibir a escrita nessas propriedades, mas a leitura tem que ser possível, caso contrário
todo nosso trabalho foi por água a baixo. Como solucionar? Por padrão, todo método de uma classe
também possui modificador public . Que tal criarmos dois métodos públicos que ao serem chamados
retornam os valores de mensagem e inclusao respectivamente? Se os métodos fazem parte de
MensagemCadastro eles terão acesso às propriedades encapsuladas. Vamos aproveitar e deixar explícito
o modificador de acesso public :
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
export class MensagemCadastro {

private mensagem: string;


private inclusao: boolean;

constructor(mensagem: string, inclusao: boolean) {


this.mensagem = mensagem;
this.inclusao = inclusao;
}

public obterMensagem(): string {


return this.mensagem;
}

public ehInclusao(): boolean {


return this.inclusao;
}
}

Agora, precisamos alterar nosso componente CadastroComponent para que faça uso desses

114 12.6 O MODIFICADOR PRIVATE


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
métodos no lugar de acessar as propriedades diretamente:
// caelumpic/src/app/cadastro/cadastro.service.ts
// código anterior omitido
cadastrar() {

this.service
.cadastra(this.foto)
.subscribe(res => {
this.mensagem = res.obterMensagem();
this.foto = new FotoComponent();
if(!res.ehInclusao()) this.router.navigate(['']);
}, erro => {
console.log(erro);
this.mensagem = 'Não foi possível savar a foto';
});
}

12.7 TYPESCRIPT E SUAS FACILIDADES


Perfeito, nosso código compila e funciona perfeitamente. Problema resolvido, mas o TypeScript
possui um recurso interessante quando precisamos definir se uma propriedade é somente leitura ou
somente escrita.

Primeiro, vamos remover os dois métodos que criamos, inclusive vamos prefixar as duas
propriedades da nossa classe com underline. Com essa alteração, precisamos ajustar o construtor da
nossa classe:

// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
export class MensagemCadastro {

private _mensagem: string;


private _inclusao: boolean;

constructor(mensagem: string, inclusao: boolean) {


this._mensagem = mensagem;
this._inclusao = inclusao;
}
}

Agora, vamos criar dois métodos com o mesmo nome das nossas propriedades, mas sem o underline
e usar uma palavrinha especial logo após o modificador de acesso public , a palavra get:
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
export class MensagemCadastro {

private _mensagem: string;


private _inclusao: boolean;

constructor(mensagem: string, inclusao: boolean) {


this._mensagem = mensagem;
this._inclusao = inclusao;
}

public get mensagem(): string {

12.7 TYPESCRIPT E SUAS FACILIDADES 115


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
return this._mensagem;
}

public get inclusao(): boolean {


return this._inclusao;
}
}

Os métodos que acabamos de criar são chamados de getters , isto é, uma função que nos dará
acesso de leitura para as propriedades _mensagem e _inclusao . Mas não trocamos seis por meia
dúzia? Não, porque essa estrutura nos permite acessamos as propriedades como se fossem atributos, no
entanto só permitirão a leitura e jamais a escrita. Vamos alterar novamente CadastroComponent :

// caelumpic/src/app/cadastro/cadastro.component.ts
// código anterior omitido
cadastrar() {

this.service.cadastra(this.foto)
.subscribe(
res => {
this.mensagem = res.mensagem;
this.foto = new FotoComponent();
if(!res.inclusao) this.router.navigate(['']);
},
erro => {
console.log(erro);
this.mensagem = 'Não foi possível savar a foto';
});
}

Por fim, temos a checagem de tipos do TypeScript, autocomplete e ainda por cima estamos
garantindo que a única maneira de um instância de MensagemCadastro receber a mensagem e a
indicação da ação é através do seu construtor. Além disso, instâncias de MensagemCadastro não
permitem que suas propriedades sejam alteradas, mas apenas lidas.

12.8 PROPRIEDADES E O AÇÚCAR SINTÁTICO DO TYPESCRIPT


Temos nossa classe MensagemCadastro criada, mas podemos lançar mão de um recurso exclusivo
do TypeScript para tornar ainda menos verbosa a declaração da nossa classe. TypeScript permite
declarar as propriedades de uma classe no próprio construtor:
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
export class MensagemCadastro {

// por debaixo dos panos cria das propriedades `_memsagem` e `_inclusao` como privados
constructor(private _mensagem: string, private _inclusao: boolean) {
this._mensagem = _mensagem;
this._inclusao = _inclusao;
}

public get mensagem(): string {


return this._mensagem;
}

116 12.8 PROPRIEDADES E O AÇÚCAR SINTÁTICO DO TYPESCRIPT


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
public get inclusao(): boolean {
return this._inclusao;
}
}

Veja que poupamos algumas linhas de código. No entanto, se tivermos alguma informação que não é
recebida pelo construtor ainda será necessário defini-la como propriedade da classe como fizemos antes.

12.9 EXERCÍCIO - MODIFICADORES DE ACESSE EM UMA CLASSE


1.Crie a classe MensagemCadastro dentro de FotoService:

// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
export class MensagemCadastro {

constructor(private _mensagem: string, private _inclusao: boolean) {


this._mensagem = _mensagem;
this._inclusao = _inclusao;
}

public get mensagem(): string {


return this._mensagem;
}

public get inclusao(): boolean {


return this._inclusao;
}
}

2.Altere o tipo do retorno do método cadastra ainda em FotoService, agora usando a classe que
criamos MensagemCadastro. E no retorno, não esquece de criar uma instância de MensagemCadastro:
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
cadastra(foto: FotoComponent): Observable<MensagemCadastro> {

if(foto._id) {
return this.http.put(this.url + '/' + foto._id, JSON.stringify(foto),
{ headers: this.headers })
.map(
() => new MensagemCadastro('Foto alterada com sucesso', false)
);

} else {
return this.http.post(this.url, JSON.stringify(foto),
{ headers: this.headers })
.map(
() => new MensagemCadastro('Foto incluída com sucesso', true)
);
}
}

12.9 EXERCÍCIO - MODIFICADORES DE ACESSE EM UMA CLASSE 117


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 13

APÊNDICE: DECISÃO NAS MÃOS DO


USUÁRIO

Completamos nossa aplicação concluindo nosso cadastro de fotos, contudo podemos deixar ainda
melhor a experiência dos nossos usuários.

Em ListagemComponent, a remoção de uma foto ocorre instantaneamente. Como esta operação é


irreversível, o ideal é solicitar a confirmação da ação com o usuário, garantindo assim uma segunda
chance para que ele reflita sobre a operação que irá realizar.

Poderíamos conseguir essa funcionalidade adicionando no método remove de ListagemComponent


uma confirmação rudimentar, porém não menos eficaz, usando o confirm do Javascript:
// caelumpic/src/app/listagem/listagem.component.ts
// código anterior omitido
remove(foto: FotoComponent): void {
if(confirm('Tem certeza?')) {
this.service.remove(foto)
.subscribe(
fotos => {
let novasFotos = this.fotos.slice(0);
let indice = novasFotos.indexOf(foto);
novasFotos.splice(indice, 1);
this.fotos = novasFotos;
this.mensagem = 'Foto removida com sucesso';
},
erro => {
console.log(erro);
this.mensagem = 'Não foi possível remover a foto';
});
}
}
// código posterior omitido

13.1 ISOLANDO A LÓGICA DE CONFIRMAÇÃO EM UM COMPONENTE


Problema resolvido, mas será que é a melhor solução? Se tivermos ações de outros componentes que
necessitem confirmação do usuário? Sairemos espalhando o código de confirmação em todos eles?

Vamos remover a lógica de confirmação que acabamos de adicionar no método remove de


ListagemComponent . Agora, Vamos isolar a lógica de confirmação em um componente reutilizável por
outros componentes que necessitem dessa verificação antes de executar determinada ação. Ele se

118 13 APÊNDICE: DECISÃO NAS MÃOS DO USUÁRIO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
chamará BotaoComponent :

// caelumpic/src/app/botao/botao.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {

@Input() nome: string = 'Ok';


@Input() estilo: string = 'btn-default';
@Input() tipo: string = 'button';
@Input() desabilitado: boolean = false;
}

Nosso componente aceita receber quatro parâmetros. O primeiro é o nome do botão. Se nenhum
nome for passado seu valor padrão será "Ok". O segundo parâmetro é o estilo do botão que possui valor
padrão btn-default . É este parâmetro que nos permite exibir diferentes botões do Bootstrap,
contanto que passemos a classe correta do botão como parâmetro. Há também o parâmetro tipo , para
indicar o type , que possui valor padrão button . Por fim, temos o parâmetro desabilitado para
permitir a habilitação ou não do botão.

13.2 O TEMPLATE DO NOSSO COMPONENTE


Agora precisamos definir o template de BotaoComponent :

<!-- caelumpic/src/app/botao/botao.component.html -->


<button class="btn {{estilo}}" [type]="tipo" [disabled]="desabilitado">{{nome}}</button>

Vamos utilizá-lo no template de ListagemComponent :


<!-- caelumpic/src/app/listagem/listagem.component.html -->
<painel *ngFor="let foto of fotos | filtroPorTitulo:textoProcurado.value" titulo="{{foto.titulo | upp
ercase}}" class="col-md-2">
<a [routerLink]="['/cadastro', foto._id]">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</a>
<br>
<botao
nome="Remover"
estilo="btn-danger btn-block"
(click)="remove(foto)">
</botao>
</painel>

Apenas os parâmetros nome e estilo foram passados, no entanto temos um problema. Quando
clicamos em nosso botão, estamos executando o método remover de ListagemComponent e nenhuma
verificação é realizada. Para atingirmos nosso objetivo, o método remover só pode ser chamado após a
confirmação do usuário. Você deve lembrar que criamos este novo componente para isolar a lógica de
confirmação.

13.2 O TEMPLATE DO NOSSO COMPONENTE 119


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
Primeiro, vamos remover a associação de evento click que acabamos de fazer:

<!--caelumpic/src/app/listagem/listagem.component.html-->
<botao nome="Remover" estilo="btn-danger btn-block"></botao>

Vamos alterar o template do nosso botão adicionando uma ação para o evento click :
<!-- caelumpic/src/app/botao/botao.component.html -->
<button (click)="executaAcao()" class="btn {{estilo}}" [type]="tipo" [disabled]="desabilitado">{{nome
}}</button>

Precisamos adicionar esse método em BotaoComponent :

// caelumpic/src/app/botao/botao.component.ts
import { Component, Input } from '@angular/core';

@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {

@Input() nome: string = 'Ok';


@Input() estilo: string = 'btn-default';
@Input() tipo: string = 'button';
@Input() desabilitado: boolean = false;

executaAcao() {

if(confirm('Tem certeza?')) {
// como executar o método `Remove` de principal?
}
}
}

Ótimo, quando nosso botão é clicado, ele executa a nossa confirmação, no entanto em nenhum
momento indicamos que ele deve executar o método remove(foto) de ListagemComponent . Como
seguir isso?

13.3 EVENTOS CUSTOMIZADOS COM EVENTEMITTER


A solução desse problema mora na criação de eventos customizados. Vamos fazer com que nosso
botão dispare o evento acao , mas apenas quando houver confirmação. Como o evento sai do nosso
componente para o mundo externo, vamos adicionar uma propriedade chamada acao na definição da
nossa classe decorada com Output :
// caelumpic/src/app/botao/botao.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {

@Input() nome: string = 'Ok';

120 13.3 EVENTOS CUSTOMIZADOS COM EVENTEMITTER


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
@Input() estilo: string = 'btn-default';
@Input() tipo: string = 'button';
@Input() desabilitado: boolean = false;
@Output() acao = new EventEmitter();

executaAcao() {
if(confirm('Tem certeza?')) {
// como executar o método `Remove` de ListagemComponent?
}
}
}

Veja que nosso evento acao recebe uma instância da classe EventEmitter . É através desta
instância que podemos indicar o disparo do evento.

Vamos disparar o evento através de acao.emit apenas quando o usuário confirmar a ação:
// caelumpic/src/app/botao/botao.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {

@Input() nome: string = 'Ok';


@Input() estilo: string = 'btn-default';
@Input() tipo: string = 'button';
@Input() desabilitado: boolean = false;
@Output() acao = new EventEmitter();

executaAcao() {
if(confirm('Tem certeza?')) {
this.acao.emit();
}
}
}

Como nosso botão saberá que deve executar remove de ListagemComponent ?

Para isso, precisamos associar o método remove com o evento acao do nosso botão no template
de ListagemComponent , como no exemplo:
<botao nome="Remover" estilo="btn-danger btn-block" (acao)="remove(foto)"></botao>

Perfeito! Quando clicamos em nosso botão, seu evento click será disparado e executará a lógica de
confirmação. Se confirmarmos, através do EventEmitter dispararemos o evento acao . Isso fará com
que a expressão remove(foto) atribuída ao evento acao seja executada.

13.4 TORNANDO NOSSO COMPONENTE AINDA MELHOR


Podemos tornar nosso botão ainda melhor ativando ou não a lógica de confirmação:

// caelumpic/src/app/botao/botao.component.ts
//codigo anterior omitido

13.4 TORNANDO NOSSO COMPONENTE AINDA MELHOR 121


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
export class BotaoComponent {

@Input() nome: string = 'Ok';


@Input() estilo: string = 'btn-default';
@Input() tipo: string = 'button';
@Input() desabilitado: boolean = false;
@Output() acao = new EventEmitter();
@Input() confirmacao: boolean = false;

executaAcao() {
if(this.confirmacao) {
if(confirm('Tem certeza?')) {
this.acao.emit();
}
return;
}
this.acao.emit();
}
}

Por fim, que tal usarmos nosso botão em CadastroComponent ? Vamos substituir nosso botão
Salvar pelo nosso novo botão:

<!-- caelumpic/src/app/cadastro/cadastro.component.html -->


<!-- código anterior omitido -->
<botao
nome="Salvar"
tipo="submit"
estilo="btn-primary"
[desabilitado]="!meuForm.valid">
</botao>

Agora temos um botão genérico que podemos usar em vários lugares da nossa aplicação.

13.5 EXERCÍCIO - CRIANDO UM BOTÃO PARA DECISÕES


1.Crie o BotaoComponent:

// caelumpic/src/app/botao/botao.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {

@Input() nome: string = 'Ok';


@Input() estilo: string = 'btn-default';
@Input() tipo: string = 'button';
@Input() desabilitado: boolean = false;
@Output() acao = new EventEmitter();

executaAcao() {

if(confirm('Tem certeza?')) {
this.acao.emit(null);
}
}

122 13.5 EXERCÍCIO - CRIANDO UM BOTÃO PARA DECISÕES


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
}

2.Vamos criar agora o módulo BotaoModule:


// caelumpic/src/app/botao/botao.module.ts
import { NgModule } from '@angular/core';
import { BotaoComponent } from './botao.component';

@NgModule({
declarations: [ BotaoComponent ],
exports: [ BotaoComponent ]
})
export class BotaoModule { }

3.Não podemos nos esquecer de importá-lo em AppModule:

import 'rxjs/add/operator/map';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';
import { HttpModule } from '@angular/http';
import { RouterModule, Routes } from '@angular/router';
import { FotoModule } from './foto/foto.module';
import { PainelModule } from './painel/painel.module';
import { routing } from './app.routes';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

// importando o novo módulo! Não esqueça de adicioná-lo também no array da propriedade imports
import { BotaoModule } from './botao/botao.module';

@NgModule({
imports: [
BrowserModule,
HttpModule,
PainelModule,
FotoModule,
routing,
FormsModule,
ReactiveFormsModule,
BotaoModule
],
declarations: [ AppComponent, ListagemComponent, CadastroComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

4.Use BotaoComponent em ListagemComponent:

<!--caelumpic/src/app/listagem/listagem.component.html-->
<!-- código anterior omitido -->
<painel *ngFor="let foto of fotos | filtroPorTitulo:textoProcurado.value" titulo="{{foto.titulo | upp
ercase}}" class="col-md-2">
<a [routerLink]="['/cadastro', foto._id]">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</a>
<br>
<botao nome="Remover" estilo="btn-danger btn-block" (acao)="remove(foto)"></botao>
</painel>
<!-- código posterior omitido -->

13.5 EXERCÍCIO - CRIANDO UM BOTÃO PARA DECISÕES 123


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
5.Agora em CadastroComponent substitua o botão Salvar pelo BotaoComponent:

<!-- caelumpic/src/app/cadastro/cadastro.component.html -->


<!-- código anterior omitido -->
<botao
nome="Salvar"
tipo="submit"
estilo="btn-primary"
[desabilitado]="!meuForm.valid">
</botao>

6.(Opcional) Podemos tornar nosso botão ainda melhor ativando ou não a lógica de confirmação:
// caelumpic/src/app/botao/botao.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {

@Input() nome: string = 'Ok';


@Input() estilo: string = 'btn-default';
@Input() tipo: string = 'button';
@Input() desabilitado: boolean = false;
@Output() acao = new EventEmitter();
@Input() confirmacao: boolean = false;

executaAcao() {
if(this.confirmacao) {
if(confirm('Tem certeza?')) {
this.acao.emit(null);
}
return;
}
this.acao.emit(null);
}
}

13.6 O MENU DA APLICAÇÃO


Independente do escopo de uma aplicação elas devem ter um menu para auxiliar o usuário em sua
navegação. Com nossa aplicação não será diferente. No entanto, onde adicionaremos esse menu? Que tal
no template de App , antes da diretiva <router-outlet> ? Dessa forma, o menu será exibido para
todos os componentes carregados pelo sistema de rotas do Angular.

13.7 EXERCÍCIO OPCIONAL - ADICIONANDO A NAVEGAÇÃO


1.Adicione no template caelumpic/src/app/app.component.html :

<!-- caelumpic/src/app/app.component.html -->


<nav class="navbar navbar-default">
<ul class="nav navbar-nav">
<li><a [routerLink]="['/cadastro']">Nova</a></li>
<li><a [routerLink]="['']">Home</a></li>

124 13.6 O MENU DA APLICAÇÃO


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
</ul>
</nav>
<router-outlet></router-outlet>

Agora nossa aplicação tem um menu!

13.7 EXERCÍCIO OPCIONAL - ADICIONANDO A NAVEGAÇÃO 125


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
CAPÍTULO 14

APÊNDICE: APROVEITANDO SEU


CONHECIMENTO SOBRE JQUERY

14.1 JQUERY COM ANGULAR2 É POSSÍVEL?


Nossa aplicação está cada vez melhor, contudo a remoção de fotos ocorre de maneira abrupta após
confirmada. Para deixarmos nossos usuários ainda mais confortáveis com nossa aplicação podemos
realizarmos um efeito de fade out, aquele no qual a imagem vai se tornando transparente até
desaparecer. Podemos até pensar em uma solução 100% Angular combinando CSS dinamicamente,
contudo adotaremos uma abordagem diferente.

Não é raro o desenvolvedor front-end que passa a utilizar o framework da Google ter alguma noção
de jQuery, a biblioteca de manipulação de DOM mais famosa do mercado. Realizar um fade out não é
complicado com jQuery, basta selecionarmos o elemento do DOM que desejamos aplicar o efeito para
em seguida chamarmos a função fadeOut como no exemplo hipotético abaixo:

$('div').fadeOut(function() {
console.log('Realizou o fade out');
});

Apesar da equipe do Angular desencorajar a manipulação direta do DOM em nossa aplicação


Angular, pode ser que em determinadas situações ela seja a melhor opção, mesmo que em casos raros.
Saber acessar elementos do DOM e integrar nosso código do Angular com jQuery é algo que precisamos
ter na manga para qualquer eventualidade, principalmente em sistemas nos quais boa parte das
funcionalidades fazem uso do jQuery.

14.2 EXERCÍCIO - BAIXANDO O JQUERY PELO NPM


1.Para baixar a biblioteca do jQuery e já deixarmos com uma dependencia do projeto, utilizaremos o
npm ,
assim como fizemos anteriormente com o Bootstrap. No terminal, e dentro da pasta raiz
caelumpic execute o comando:
npm install --save jquery

2.Como qualquer dependência baixada pelo npm, o jQuery ficará dentro da pasta
caelumpic/node_modules . Agora para usar sua biblioteca, temos que importá-lo em
caelumpic/.angular-cli.json na configuração de scripts, passando o caminho do arquivo:

126 14 APÊNDICE: APROVEITANDO SEU CONHECIMENTO SOBRE JQUERY


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
//caelumpic/.angular-cli.json
//código anterior omitido
"scripts": [
"../node_modules/jquery/dist/jquery.min.js"
],
//código posterior omitido

14.3 REFERÊNCIA DE ELEMENTO ATRAVÉS DE INJEÇÃO


Como utilizaremos o jQuery? Como nossa foto é exibida dentro PainelComponent , podemos
adicionar um método em sua classe que realiza o fade out. Com isso, basta termos uma referência do
painel em nosso código para em seguida chamarmos a função. Porém, para que isso seja possível,
precisamos ter uma referência do DOM do componente para que possamos manipulá-lo com jQuery.

Podemos solicitar ao sistema de injeção de dependências do Angular o elemento do DOM do nosso


componente adicionando no construtor de PainelComponent um elemento do tipo ElementRef , só
não podemos esquecer de importá-lo do módulo @angular/core , caso contrário nosso código não
compilará.

// caelumpic/src/app/painel/painel.components.ts
import { Component, Input, OnInit, ElementRef } from '@angular/core';

@Component({
selector: 'painel',
templateUrl: './painel.component.html',
styleUrls: ['./painel.component.css'],
})
export class PainelComponent implements OnInit {

@Input() titulo: string;


private elemento: ElementRef;

constructor(elemento: ElementRef) {
this.elemento = elemento;
}
//código posterior omitido

Um elemento do tipo ElementRef encapsula o elemento do DOM do nosso componente na


propriedade nativeElement . A implementação de um método fadeOut para usar o jQuery deve ficar
assim:
// caelumpic/app/src/painel/painel.component.ts
//código anterior omitido
fadeOut(callback) {
$(this.elemento.nativeElement).fadeOut(callback);
}

14.4 EXERCÍCIO - IMPLEMENTANDO O FADEOUT DO PAINEL


1.Importe ElementRef e implemente o método fadeOut:

// caelumpic/app/src/painel/painel.component.ts
import { Component, Input, OnInit, ElementRef } from '@angular/core';

14.3 REFERÊNCIA DE ELEMENTO ATRAVÉS DE INJEÇÃO 127


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
@Component({
selector: 'painel',
templateUrl: './painel.component.html',
styleUrls: ['./painel.component.css'],
})
export class PainelComponent implements OnInit {

@Input() titulo: string;


private elemento: ElementRef;

constructor(elemento: ElementRef) {
this.elemento = elemento;
}

ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}

fadeOut(callback) {
$(this.elemento.nativeElement).fadeOut(callback);
}
}

14.5 QUE ERRO DE COMPILAÇÃO É ESSE?


Cannot find name '$'.

Temos um problema, o compilador TypeScript não consegue encontrar a declaração da variável $ .


Por padrão, o script que carrega o jQuery disponibiliza globalmente a variável $ para que possamos
utilizá-lo, contudo o TypeScript considera a variável como não declarada e se recusará em compilar
nosso código. Além disso, se escrevermos $ e o operador . o Visual Studio Code (ou seu editor
favorito com suporte ao TypeScript) não reconhece nenhuma de suas funções e nem poderia, já que o
jQuery foi criado sem TypeScript e não usa qualquer tipagem.

Para podemos trabalhar com bibliotecas que não foram escritas em TypeScript precisamos declarar a
API que a biblioteca expõe em TypeScript. É apenas uma declaração sem qualquer implementação, até
porque a implementação já existe na biblioteca. O TypeScript chama de "ambient" toda declaração sem
implementação, inclusive essas declarações costumam ser definidas em arquivo .d.ts .

A boa notícia é que não precisamos nos preocupar com a declaração de arquivos "ambient".
Podemos baixar de um repositório na Web os arquivos .d.ts do jQuery e de outras bibliotecas usadas
pela comunidade, através do próprio npm.

14.6 DEFINIÇÕES DO JQUERY PARA O TYPESCRIPT


Existe uma ferramenta especifia para gerenciar os arquivos .d.ts chamada Typings, no qual foi a
primeira opção antes do suporte do npm para isto. Porém npm já resolve muito bem a instalação destas
bibliotecas typings. Para instalar os typings do jquery, por exemplo, basta executar o comando:

128 14.5 QUE ERRO DE COMPILAÇÃO É ESSE?


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
npm install @types/jquery --save

Conseguimos ver os typings que são dependencia do projeto no package.json pela configuração
@types , por exemplo a do jquery fica algo como: "@types/jquery": "^2.0.46" , e é possível
verificar os arquivos d.ts instalados em nosso projeto na pasta node_modules/@types.

Agora temos o tipo jquery disponível para uso na app, porém nosso problema ainda não foi
resolvido. Faltou o útlimo detalhe: importar $ em painel.component.ts:
// caelumpic/app/src/painel/painel.component.ts
import { Component, Input, OnInit, ElementRef } from '@angular/core';
import * as $ from 'jquery'
//código posterior omitido

Agora precisamos alterar o método remove do componente ListagemComponent . Ele agora deve
receber também como parâmetro um PainelComponent da foto que estamos removendo em mãos,
podemos chamar seu método fadeOut e assim que o efeito terminar, removemos a foto da lista com a
posição que recebemos.

14.7 DECLARANDO MAIS UMA VEZ VARIÁVEIS NO TEMPLATE


Por fim, só precisamos alterar o template de ListagemComponent para que passe, além da foto, o
painel. Vamos lançar mão mais uma vez de variável local, de template. Criaremos uma para termos uma
referência para o painel:

<!-- caelumpic/src/app/listagem/listagem.component.html -->


<!-- código anterior omitido -->
<div class="row">
<painel #painel *ngFor="let foto of fotos | filtroPorTitulo: textoProcurado.value" titulo="{{foto
.titulo | uppercase}}" class="col-md-2">
<a [routerLink]="['/cadastro', foto._id]">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</a>
<br>
<botao confirmacao="true" nome="Remover" estilo="btn-danger btn-block" (acao)="remove(foto, p
ainel)"></botao>
</painel>
</div><!-- fim row -->

14.8 EXERCÍCIO - USANDO O JQUERY


1.Instale o typings do jQuery:

npm install @types/jquery --save

2.Importe $ de jQuery em painel.component.ts:


// caelumpic/app/src/painel/painel.component.ts
import { Component, Input, OnInit, ElementRef } from '@angular/core';
import $ from 'jquery';

@Component({

14.7 DECLARANDO MAIS UMA VEZ VARIÁVEIS NO TEMPLATE 129


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
selector: 'painel',
templateUrl: './painel.component.html',
styleUrls: ['./painel.component.css'],
})
export class PainelComponent implements OnInit {

@Input() titulo: string;


private elemento: ElementRef;

constructor(elemento: ElementRef) {
this.elemento = elemento;
}

ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}

fadeOut(cb) {
$(this.elemento.nativeElement).fadeOut(cb);
}
}

3.Referencie PainelComponent para ser removido também pelo método remove em


ListagemComponent:
// caelumpic/src/app/listagem/listagem.component.ts
//código anterior omitido
remove(foto: FotoComponent, painel: PainelComponent) {

this.service.remove(foto)
.subscribe(
() => {
painel.fadeOut(() => {
let novasFotos = this.fotos.slice(0);
let indice = novasFotos.indexOf(foto);
novasFotos.splice(indice, 1);
this.fotos = novasFotos;
this.mensagem = 'Foto removida com sucesso';
});
},
erro => {
console.log(erro);
this.mensagem = 'Não foi possível remover a foto';
}
);
}

4.Adicione a variável de template #painel no componente <painel> , e ação remove passe o painel
como parâmetro:
<!-- caelumpic/src/app/listagem/listagem.component.html -->
<!-- código anterior omitido -->
<div class="row">
<painel #painel *ngFor="let foto of fotos | filtroPorTitulo: textoProcurado.value" titulo="{{foto
.titulo | uppercase}}" class="col-md-2">
<a [routerLink]="['/cadastro', foto._id]">
<foto titulo="{{foto.titulo}}" url="{{foto.url}}"></foto>
</a>
<br>

130 14.8 EXERCÍCIO - USANDO O JQUERY


Apostila gerada especialmente para Romero Veloso Costa Filho - romerovcf@gmail.com
<botao confirmacao="true" nome="Remover" estilo="btn-danger btn-block" (acao)="remove(foto, p
ainel)"></botao>
</painel>
</div><!-- fim row -->

Agora, quando removemos um foto, seu painel inteiro vai ficando translúcido até desaparecer.

14.8 EXERCÍCIO - USANDO O JQUERY 131

Você também pode gostar