spa - VWJavascript/Alurapic GitHub Wiki

Single Page Application (SPA) e Rotas

Uma Single Page Application (SPA) ou "Aplicação de Página Única" no português, é aquela que não recarrega durante o uso e geralmente já carrega todos os scripts e CSS's de que precisa no seu primeiro carregamento. O usuário pode realizar uma série de tarefas sem que a página recarregue. Se você é novo nessa abordagem, deve pensar assim: "Flávio, como assim não recarrega? Quer dizer que colocamos o conteúdo de todas as páginas em uma só? Não ficará confuso?". Se eu realmente captei sua dúvida, sim, realmente ficará confuso. E é por isso que em SPA não colocamos tudo em uma página apenas, criamos algo muito semelhante a páginas para realizar essa separação.

Em SPA, a página index.html é carregada com todos os scripts e CSS's, 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 eu usei o termo "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. Exemplos de SPA são o Gmail e o Inbox: quando você apaga um e-mail ou inicia a escrita de um novo, 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, conversa é barata, vamos ver como criar uma SPA com Angular!

Configurando rotas

Angular possui um módulo criado para ajudar nesse processo de mudança de página, o RouterModule. O primeiro passo será criar o arquivo alurapic/client/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:

// alurapic/client/app/app.routes.ts 

import { RouterModule } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';

Mas onde está a configuração de nossas rotas? 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:

// alurapic/client/app/app.routes.ts 

import { RouterModule, Routes } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';

const appRoutes: Routes  = [
  { path: '', component: ListagemComponent },
  { path: 'cadastro', component: CadastroComponent }
];

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

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:

// alurapic/client/app/app.routes.ts 

import { RouterModule, Routes } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';

const appRoutes: Routes  = [
  { path: '', component: ListagemComponent },
  { path: 'cadastro', component: CadastroComponent }
];

export const routing = RouterModule.forRoot(appRoutes);

Fizemos uma série de alterações, mas 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. Para isso, precisamos adicionar em alurapic/client/app/app.component.html a diretiva router-outlet:

<!-- alurapic/client/app/app.component.html -->

<router-outlet></router-outlet>

Muito bem, vamos recapitular. Quando nossa aplicação Angular bootar, o primeiro componente a ser carregado será o AppComponent. Este componente deve configurar as rotas da aplicação, isto é, os endereços que correspondem aos componentes ListagemComponent e CadastroComponent. Quando determinada URL for acessada pelo browser, seu respectivo componente deve ser carregado e exibido para o usuário. Ainda falta realizarmos essa configuração.

Experimente abrir os endereços:

http://localhost:3000/listagem
http://localhost:3000/cadastro

Ainda não foi dessa vez. Acessando o log do browser, no meu caso o Chrome, 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. Vamos adicioná-la em alurapic/client/index.html:

<!-- alurapic/client/index.html -->

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

Recarregando e testando novamente:

http://localhost:3000/listagem
http://localhost:3000/cadastro

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

http://localhost:3000/teste

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

// alurapic/client/app/app.routes.ts


import { RouterModule, Routes } from '@angular/router';
import { ListagemComponent } from './listagem/listagem.component';
import { CadastroComponent } from './cadastro/cadastro.component';

const appRoutes: Routes  = [
  { path: '', component: ListagemComponent },
  { path: 'cadastro', component: CadastroComponent },
  { path: '**', redirectTo: ''}
];

export const routing = RouterModule.forRoot(appRoutes);

Usamos a sintaxe de path especial ** e, no lugar de usarmos o atributo component para indicar qual componente deve ser renderizado, usaremos redirectTo. Veja que a propriedade recebe o valor ''. Esse valor é uma rota já existente, aquela que carrega o componente ListagemComponent.

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

// alurapic/client/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';
import 'rxjs/add/operator/map';
import { PainelModule } from './painel/painel.module';
import { CadastroComponent } from './cadastro/cadastro.component';
import { ListagemComponent } from './listagem/listagem.component';
import { routing } from './app.routes';

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

Cliquei no link e...

Muito bem, até agora estávamos acessando as rotas digitando-as diretamente no browser. Como fazemos isso através, por exemplo, de um link em nossa página? Vamos adicionar o botão Nova foto, na verdade, uma tag com classes do Bootstrap:

<!-- alurapic/client/app/listagem/listagem.component.html -->

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

<div class="container">

    <div class="row">
        <div class="col-md-12">
            <form>
                 <div class="input-group">
                    <span class="input-group-btn">
                        <a href="/cadastro" class="btn btn-primary">
                            Nova foto
                        </a>
                    </span>
                </div> 
            </form>
        </div> <!-- fim col-md-12 -->
    </div> <!-- fim row -->
    <br>
    <div class="row">
        <painel *ngFor="let foto of fotos" titulo="{{foto.titulo}}" class="col-md-2">
            <foto titulo="{{foto.titulo}}" url="{{foto.url}}">
            </foto>
        </painel>
    </div><!-- fim row -->
</div>

image

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.

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

<!-- alurapic/client/app/listagem/listagem.component.html -->

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

<div class="container">

    <div class="row">
        <div class="col-md-12">
            <form>
                 <div class="input-group">
                    <span class="input-group-btn">
                        <a [routerLink]="['/cadastro']" class="btn btn-primary">
                            Nova foto
                        </a>
                    </span>
                </div> 
            </form>
        </div> <!-- fim col-md-12 -->
    </div> <!-- fim row -->
    <br>
    <div class="row">
        <painel *ngFor="let foto of fotos" titulo="{{foto.titulo}}" class="col-md-2">
            <foto titulo="{{foto.titulo}}" url="{{foto.url}}">
            </foto>
        </painel>
    </div><!-- fim row -->
</div>
⚠️ **GitHub.com Fallback** ⚠️