Escolar Documentos
Profissional Documentos
Cultura Documentos
com
Caelum Sumário
Sumário
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
Versão: 20.9.14
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.
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.
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.
ou
node -v
Linux (Ubuntu)
No Ubuntu, através do terminal (permissão de administrador necessária) execute o comando abaixo:
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:
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.
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:
A partir deste momento temos uma API local, podendo ser acessada pelo endereço
http://localhost:3000/
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
Esse processo levará um tempo (cerca de 1 hora) porque várias dependências são baixadas.
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
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.
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.
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.
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 .
// caelumpic/app/src/app.component.ts
import {Component} from '@angular/core';
class AppComponent {}
// 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';
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.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']
})
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
1.Vamos instalar o Bootstrap usando o npm e salvá-lo como dependencia do projeto. No terminal,
dentro da pasta caelumpic, execute o comando:
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"
],
</div>
Com isto nossa aplicação está com uma aparência melhor utilizando os estilos do Bootstrap:
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.
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'
})
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 ]
})
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.
@NgModule({
imports: [ BrowserModule, FotoModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
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.
<div class="container">
<foto></foto>
</div>
<div class="container">
<foto></foto>
<foto></foto>
</div>
Seria perfeito se pudermos passar o endereço da foto e o seu título desta maneira:
<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.
@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {
titulo;
url;
}
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:
E agora? Temos duas lacunas que precisam ser preenchidas em nosso template.
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
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:
Veja que não houve manipulação de DOM para conseguirmos este resultado, um dos pontos fortes
de frameworks que trabalham com data binding.
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.
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;
}
Verifique se seu servidor local está iniciado, abra o navegador e digite o endereço:
http://localhost:3000/v1/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?
// caelumpic/src/app/app.component.ts
import {Component} from '@angular/core';
import { Http } from '@angular/http';
//código posterior omitido
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 {
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.
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',
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 {
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.
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
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';
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.
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:
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
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 {
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.
// 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 { }
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(http: Http) { }
}
// 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 {
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 {
constructor(http: Http) {
let stream = http.get('http://localhost:3000/v1/fotos')
}
}
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
constructor(http: Http) {
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.
=>
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 {
constructor(http: Http) {
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.
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 {
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:
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:
@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)
);
}
@NgModule({
imports: [ BrowserModule, FotoModule, HttpModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
// caelumpic/src/app/app.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';
constructor(http: Http) {
http.get('http://localhost:3000/v1/fotos')
.map(res => res.json())
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}
}
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 -->
<!-- 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.
@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent {
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 { }
// caelumpic/src/app/app.module.ts
import 'rxjs/add/operator/map'; // importou a extensão map!
@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> :
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 -->
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.
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.
// caelumpic/src/app/cadastro/cadastro.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent { }
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 {
constructor(http: Http) {
http.get('http://localhost:3000/v1/fotos')
.map(res => res.json())
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}
<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>
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.
// caelumpic/src/app/app.module.ts
import 'rxjs/add/operator/map';
@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!
Para podermos resolver esse problema, precisamos entender o motivo pelo qual o Angular é um
framework voltado para a criação de Single Page Applications.
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!
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';
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:
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';
@NgModule({
imports: [
BrowserModule,
FotoModule,
HttpModule,
PainelModule,
routing ],
declarations: [
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.
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.
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 -->
1. Quando nossa aplicação Angular iniciar, o primeiro componente a ser carregado será o
AppComponent.
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:
// caelumpic/src/app/app.routes.ts
import { RouterModule, Routes } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';
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>
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:
<div class="container">
<div class="row">
<div class="col-md-12">
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.
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:
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 :
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:
É 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.
<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>
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 -->
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.
// 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);
}
}
@NgModule({
imports: [ CommonModule ],
declarations: [ FotoComponent, FiltroPorTitulo ],
exports: [FotoComponent, FiltroPorTitulo]
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?
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).
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 .
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.
// 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 {
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
// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {
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.
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.
// caelumpic/src/app/foto/foto.pipes.ts
import {Pipe, PipeTransform} from '@angular/core';
import { FotoComponent } from './foto.component';
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á.
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {
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>
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 é
@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {
@Component({
selector: 'foto',
templateUrl: './foto.component.html'
})
export class FotoComponent {
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';
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?
// 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 {
cadastrar() {
console.log(this.foto);
}
}
// caelumpic/src/app/foto/foto.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'foto',
templateUrl: './foto.component.html'
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>
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
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
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
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.
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.
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 { }
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';
@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!
// 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 {
constructor(http: Http) {
cadastrar(event) {
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 {
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 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
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 {
constructor(http: Http) {
this.http = http;
}
cadastrar() {
console.log(this.foto);
Muita atenção, porque não passamos nosso objeto headers diretamente. Passamos um objeto com
a propriedade headers que o contém como valor.
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {
constructor(http: Http) {
this.http = http;
}
cadastrar() {
console.log(this.foto);
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.
// 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 {
constructor(http: Http) {
this.http = http;
}
cadastrar() {
console.log(this.foto);
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 {
constructor() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}
}
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 {
ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}
@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent {
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 {
ngOnInit() {
this.titulo = this.titulo.length > 7 ?
this.titulo.substr(0, 7) + '...' :
this.titulo;
}
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 {
@Component({
selector: 'painel',
templateUrl: './painel.component.html'
})
export class PainelComponent implements OnInit {
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;
}
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>
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?
<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">
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.
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:
<input
required
name="titulo"
#titulo = "ngModel"
[(ngModel)]="foto.titulo"
class="form-control">
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">
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.
@NgModule({
imports: [
BrowserModule,
HttpModule,
PainelModule,
FotoModule,
routing,
FormsModule,
ReactiveFormsModule
],
declarations: [ AppComponent, ListagemComponent, CadastroComponent ],
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:
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">
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 {
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:
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 {
this.http = http;
this.meuForm = fb.group({
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"
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 {
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>
// 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 { }
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {
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:
<div class="form-group">
<label>URL</label>
<input
name="url"
[(ngModel)]="foto.url"
formControlName="url"
class="form-control">
<div class="form-group">
<label>Descrição</label>
<textarea name="descricao" [(ngModel)]="foto.descricao" formControlName="descricao" class="form-c
ontrol">
</textarea>
</div>
// 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
<div *ngIf="!meuForm.controls.titulo.valid">
</div>
</div>
<!-- código posterior omitido -->
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {
http: Http;
meuForm: FormGroup;
this.http = http;
this.meuForm = fb.group({
titulo: ['', Validators.compose(
[Validators.required, Validators.minLength(4)]
)],
url: ['', Validators.required],
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>
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 :
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;
}
<!--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;
}
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.
A primeira forma dessa abordagem é que não precisamos lembrar de importar a folha de estilo do
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:
/* caelumpic/src/app/foto/foto.component.css */
.efeito:hover {
transition: all 0.5s;
transform: scale(1.15);
}
// 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 {
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 {
A palavra Emulated significa emulado . Quem emula, emula alguma coisa, certo? O alvo da
emulação é o Shadow DOM.
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.
@Component({
selector: 'foto',
templateUrl: './foto.component.html',
styleUrls: ['./foto.component.css'],
encapsulation: ViewEncapsulation.Native
})
export class FotoComponent {
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.
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';
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() {}
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.
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) {
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';
http: Http;
headers: Headers;
url: string = 'http://localhost:3000/v1/fotos';
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 {
No entanto, temos uma mensagem de erro, parece que o tipo que usamos é incompatível:
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
// 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 .
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());
}
@NgModule({
imports: [ CommonModule ],
declarations: [ FotoComponent, FiltroPorTitulo ],
exports: [FotoComponent, FiltroPorTitulo ],
providers: [ FotoService ]
})
export class FotoModule { }
// caelumpic/src/app/listagem/listagem.component.ts
import { Component } from '@angular/core';
import { FotoComponent } from '../foto/foto.component';
@Component({
selector: 'listagem',
templateUrl: './listagem.component.html'
})
export class ListagemComponent {
constructor(service: FotoService) {
service.lista()
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}
}
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {
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.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
@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
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 :)');
}
}
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.
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 ?:
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 {
constructor(service: FotoService) {
service.lista()
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}
Veja que agora estamos imprimindo o título da foto no console. Um teste mostra pefeitamente que
// caelumpic/src/app/listagem/listagem.component.ts
// código anterior omitido
export class ListagemComponent {
constructor(service: FotoService) {
service.lista()
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}
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
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:
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 .
@Injectable()
export class FotoService {
// código anterior omitido
remove(foto: FotoComponent): Observable<Response> {
return this.http.delete(this.url + '/' + foto._id);
}
}
//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 {
@Component({
selector: 'listagem',
templateUrl: './listagem.component.html'
})
export class ListagemComponent {
constructor(service: FotoService) {
this.service = service;
this.service.lista()
.subscribe(
fotos => this.fotos = fotos,
erro => console.log(erro)
);
}
this.service.remove(foto)
.subscribe(
fotos => console.log('foto removida com sucesso'),
erro => console.log(erro)
);
}
}
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));
}
@Component({
selector: 'listagem',
templateUrl: './listagem.component.html'
})
export class ListagemComponent {
constructor(service: FotoService) {
this.service = service;
this.service
.lista()
.subscribe(fotos => {
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)
);
}
}
constructor(service: FotoService) {
this.service = service;
this.service
.lista()
.subscribe(fotos => {
this.fotos = fotos;
remove(foto) {
this.service
.remove(foto)
.subscribe(
() => {
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.
Veja que alteramos o path na configuração de nossas rotas. Temos o valor cadastro/:id . O
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.
// 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 {
this.route = route;
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 {
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));
}
});
Nosso código não compila porque o método buscaPorId ainda não existe em FotoService .
Vamos criá-lo:
return this.http
.get(this.url + '/' + id)
.map(res => res.json());
}
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.
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 });
}
}
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {
this.route = route;
this.service = service;
return this.http
.get(this.url + '/' + id)
.map(res => res.json());
}
// 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 {
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));
}
});
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 });
}
}
Pronto! Todas as alterações que fizemos devem ser aplicadas em nosso servidor. Quando voltamos
para ListagemComponent vemos a foto com seus dados alterados.
@Component({
selector: 'cadastro',
templateUrl: './cadastro.component.html'
})
export class CadastroComponent {
this.router = router;
this.route = route;
this.service = service;
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 :
// 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);
});
}
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.
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.
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.
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';
});
}
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.
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}));
}
mensagem: string;
inclusao: boolean;
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),
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.
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 {
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:
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'.
Agora, precisamos alterar nosso componente CadastroComponent para que faça uso desses
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';
});
}
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 {
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 {
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.
// 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;
}
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.
// caelumpic/src/app/foto/foto.service.ts
// código anterior omitido
export class MensagemCadastro {
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)
);
}
}
Completamos nossa aplicação concluindo nosso cadastro de fotos, contudo podemos deixar ainda
melhor a experiência dos nossos usuários.
// caelumpic/src/app/botao/botao.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {
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.
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.
<!--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>
// caelumpic/src/app/botao/botao.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {
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?
@Component({
selector: 'botao',
templateUrl: './botao.component.html'
})
export class BotaoComponent {
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 {
executaAcao() {
if(confirm('Tem certeza?')) {
this.acao.emit();
}
}
}
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.
// caelumpic/src/app/botao/botao.component.ts
//codigo anterior omitido
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:
Agora temos um botão genérico que podemos usar em vários lugares da nossa aplicaçã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 {
executaAcao() {
if(confirm('Tem certeza?')) {
this.acao.emit(null);
}
}
@NgModule({
declarations: [ BotaoComponent ],
exports: [ BotaoComponent ]
})
export class BotaoModule { }
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 { }
<!--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 -->
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 {
executaAcao() {
if(this.confirmacao) {
if(confirm('Tem certeza?')) {
this.acao.emit(null);
}
return;
}
this.acao.emit(null);
}
}
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');
});
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:
// 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 {
constructor(elemento: ElementRef) {
this.elemento = elemento;
}
//código posterior omitido
// caelumpic/app/src/painel/painel.component.ts
import { Component, Input, OnInit, ElementRef } from '@angular/core';
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);
}
}
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.
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.
@Component({
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);
}
}
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>
Agora, quando removemos um foto, seu painel inteiro vai ficando translúcido até desaparecer.