sexta-feira, 25 de julho de 2014

Curso de MEAN - Sessão 4: Criação de páginas dinâmicas com Angular.js

Sessão 4: Criação de páginas dinâmicas com Angular.js

Vamos estudar como criar páginas dinâmicas com o Angular.js, da Google! Veja como se livra de vez daqueles códigos Javascript complicados!


O trabalho Workshop de Desenvolvimento com Stack MEAN de Cleuton Sampaio de Melo Jr está licenciado com uma Licença Creative Commons - Atribuição-CompartilhaIgual 4.0 Internacional.

Isso inclui: Textos, páginas, gráficos e código-fonte.

Fala sério! O Angular é muito legal! Ele poupa muito trabalho na criação de páginas HTML dinâmicas, evitando que percamos a cabeça tentando entender o DOM e as diferenças entre os navegadores. É claro que podemos usar o jQuery, porém, ele dificulta muito a manutenção do site, e cria um problema para os Web Designers.

O Angular.js é um framework para criação de páginas dinâmicas, baseado em templates e em “two-way data binding”. Ele atua como um MVW (Model – View – Whatever: https://plus.google.com/+AngularJS/posts/aZNVhj355G2).

Test drive


Ok, você deve estar se perguntando: “Por que eu deveria usar o Angular? Eu já tenho o DOM ou o jQuery...”. Então, eu vou te dar algumas razões para isso, mas, em vez de escrever um monte de linhas, acho que ficará mais didático se eu der um exemplo.

Bem, vamos supor que queiramos criar uma página como essa:  

Conforme você digita os caracteres na caixa de texto, eles aparecem na “div” abaixo. Por exemplo, ao digitar “C”, aparece: “Olá C!”, e daí em diante.

Como você faria isso? Bem, poderia usar diretamente DHTML, ou seja, modificar o DOM. Eis um primeiro “approach” (arquivo “noangular.html”):  

<!doctype html>
<html>
  <head>
    <script type="text/javascript">
    function hook() {
       var saida  = document.getElementById('saida');
       seunome.onkeypress = function (e) {
         if (e.which >= 32) {
          saida.textContent += String.fromCharCode(e.which);
        }
        else {
          if (e.which == 8) {
            var origem = '';
            if (this.value.length > 0) {
              origem = this.value.substring(0,this.value.length-1);
            }
            saida.textContent = origem;           
          }
        }
       };
    }
    </script>
  </head>
  <body onload="hook()">
    <div>
      <label>Nome:</label>
      <input type="text" placeholder="Digite seu nome" id="seunome" />
      <hr>
      <h1>Olá <span id="saida"></span>!</h1>
    </div>
  </body>
</html>

É simples, não? Nós associamos o <span> “saida” com o que está sendo digitado no <input> “seunome”. Se o usuário digitar uma tecla 8 (backspace), nós simplesmente voltamos um caracter atrás.

E funciona bem no Firefox, não? Ok. Agora, se você tentar fazer isso no Google Chrome, vai dar xabú... O Backspace não vai funcionar.


Tudo bem, é para isso que existem os frameworks como o jQuery... Então, vamos usá-lo (arquivo “comjquery.html”):

<!doctype html>
<html>
  <head>
    <script type="text/javascript" src="jquery-2.1.1.min.js"></script>
    <script type="text/javascript">
    $(document).ready(function() {
      $("#seunome").keypress(function(e){
         $("#saida").text($("#saida").text() 
           + String.fromCharCode(e.which));
      });
      $("#seunome").keyup(function(e) {
        if (e.keyCode == 8) {
            var origem = '';
            if (this.value.length > 0) {
              origem = this.value.substring(0,this.value.length-1);
            }
            saida.textContent = origem;  
        }
      });
    });
    </script>
  </head>
  <body>
    <div>
      <label>Nome:</label>
      <input type="text" placeholder="Digite seu nome" id="seunome" />
      <hr>
      <h1>Olá <span id="saida"></span>!</h1>
    </div>
  </body>
</html>

Ok, o código parece complicado, mas agora funciona com Firefox e Google Chrome!

Então, por que usar o Angular?

Na boa… Já leu o último exemplo com atenção? Não parece meio confuso? O jQuery é tudo… só não é simples! E cria um verdadeiro caos para os Web Designers. É claro que podemos separar o código jQuery, colocando-o em um arquivo à parte, mas estaríamos multiplicando o problema, pois agora, temos dois arquivos para lidar com a questão.

Agora, vamos ver como isso seria feito no Angular.js (“first.html”):

<!doctype html>
<html ng-app>
  <head>
    <script src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.14/angular.min.js">
    </script>
  </head>
  <body>
    <div>
      <label>Nome:</label>
      <input type="text" ng-model="seuNome" placeholder="Digite seu nome">
      <hr>
      <h1>Olá {{seuNome}}!</h1>
    </div>
  </body>
</html>

São só 3 modificações na página:
  1. Declaramos o bloco “ng-app”, informando ao Angular que tudo o que estiver dentro do tag <html> é para ser analisado por ele;
  2. Declaramos um “modelo” chamado “seuNome”, que vai conter o que for digitado na caixa de texto;
  3. No lugar do <span>, temos a exibição da variável criada.

Não tem Javascript, e não tem aqueles comandos confusos do jQuery. Simples e prático.

Ué? Só isso? E funciona?

Não só funciona, como funciona em qualquer navegador. E tem mais: Até a tecla DEL funciona, coisa que não acontecia com os outros exemplos!

E aí? Agora se convenceu? As vantagens de usar Angular.js são:
  • Escreve-se menos;
  • Menor risco de problemas;
  • Compatibilidade cross-browser;
  • A página fica mais simples.

Entendendo o Angular

Antes de mais nada, vamos deixar claro que veremos apenas uma pequena parte do Angular.js, necessária para criar uma aplicação MEAN. Há muito mais para ser visto, porém, de modo a não estender muito o curso, fizemos um “corte” para simplificar o aprendizado.

Vamos rever nosso exemplo:

<!doctype html>
<html ng-app>
  <head>
    <script src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.14/angular.min.js">
    </script>
  </head>
  <body>
    <div>
      <label>Nome:</label>
      <input type="text" ng-model="seuNome" placeholder="Digite seu nome">
      <hr>
      <h1>Olá {{seuNome}}!</h1>
    </div>
  </body>
</html>

Bootstrap

Bootstrap é o processo de inicialização de uma página Angular. Ele começa com o script do Angular.js sendo carregado. A maneira que demonstramos acima é bem funcional, mas pode ser feita de maneira mais otimizada (“bootstrap.html”):


<!doctype html>
<html ng-app>
  <body>
    <div>
      <label>Nome:</label>
      <input type="text" ng-model="seuNome" placeholder="Digite seu nome">
      <hr>
      <h1>Olá {{seuNome}}!</h1>
    </div>
  <script src="angular.min.js"></script>    
  </body>
</html>

Deixando o script do Angular em seu próprio site, a carga fica mais rápida e você evita falhas de segurança. Em segundo lugar, carregando-o ao final do “<body>” a carga total da página fica aparentemente mais rápida.

O atributo “ng-app”, colocado no tag “<html>”, indica que nossa página é uma aplicação Angular, e faz com que o script do Angular a inicialize.


Data binding

Uma das características mais importantes de um framework MVW, como o Angular, é o “two-way data binding”, que é a associação bidirecional entre o Modelo e a Visão, conforme nossa pequena página demonstrou.


<!doctype html>
<html ng-app>
  <body>
    <div>
      <label>Nome:</label>
      <input type="text" ng-model="seuNome" placeholder="Digite seu nome">
      <hr>
      <h1>Olá {{seuNome}}!</h1>
    </div>
  <script src="angular.min.js"></script>    
  </body>
</html>

Esta página é um Template do Angular, pois possui atributos e diretivas específicas dele.

O atributo “ng-model”, colocado no tag “<input>”, é uma Diretiva, indicando que essa caixa de texto deve ser associada a uma variável chamada “seuNome”. Essa variável é um objeto de Modelo, e pode ser utilizada para atualizar a Visão, que são os outros elementos do Template.

O conteúdo do tag “<h1>” é alterado pelo Angular, pois usamos as chaves-duplas (“{{ }}”) para indicar que aquele texto está associado ao objeto de Modelo “seuNome”, sendo substituído pelo seu valor atual.

Validação e formatação

Podemos validar o conteúdo de campos, mostrando mensagens automaticamente, sem necessidade de escrever código javascript. Considere essa página:



Se você digitar um valor em Reais, ele mostra o valor em Dólares na parte de baixo, da mesma forma, se você digitar um valor em Dólares, ele mostra o valor em Reais. E, se deixar algum campo em branco, ele mostra uma mensagem de erro, como aconteceu com o campo “Valor em Dólares”.

Eis o código dessa página (“cotacoes.html”):

<!doctype html>
<html ng-app ng-init="reais=0.00;dolares=0.00;cotacao=2.00">
  <body>
    <div>
      <form name="conversao">
        <label>Valor em Reais:</label>
        <input name="txtreais" type="text" ng-model="reais" required>
        <span class="error" 
            ng-show="conversao['txtreais'].$error.required">
            Digite um valor em Reais!</span><br/>
        <label>Valor em Dólares:</label>
        <input name="txtdolares" type="text" ng-model="dolares" required> 
        <span class="error" 
            ng-show="conversao['txtdolares'].$error.required">
            Digite um valor em Dólares!</span><br/>                 
        <label>Cotação:</label>
        <input name="txtcotacao" type="text" ng-model="cotacao" required> 
        <span class="error" 
            ng-show="conversao['txtcotacao'].$error.required">
            Digite um valor em Reais para a cotação!</span><br/>   
        <br/>Valor em Reais: {{ dolares * cotacao | currency }}
        <br/>Valor em Dólares: {{ reais / cotacao | currency }}
      </form>
    </div>
  <script src="angular.min.js"></script>    
  </body>
</html>

A primeira diretiva nova é o “ng-init”, que permite iniciar os Modelos usados. Depois, criamos três textboxes, cada uma usando um Modelo (previamente inicializado). Note que usamos o atributo “required”, do HTML 5, indicando que, caso o campo seja vazio, o “flag” de erro “required” será ligado.

Depois, criei alguns “spans” com mensagens de erro. Como sua classe é “erro”, eles só aparecem caso a condição da diretiva “ng-show” seja verdadeira. Então, se houver o erro “required” em um campo, o “span” será exibido. Para usar o “required” e mostrar o erro, temos que incluir os elementos em um “form” e usar o atributo “name”.

Note também que acrescentei uma opção de formatação do valor (“| currency”). E também podemos formatar datas (“data.html”):


<!doctype html>
<html ng-app ng-init="data='2014-10-10'">
  <body>
    <div>
      <label>Data: {{data | date : 'fullDate'}}</label>
    </div>
  <script src="angular.min.js"></script>
  <script src="angular-locale_pt-br.js"></script>
  </body>
</html>

E, se você quiser localizar para Português, é só incluir o script de localização “angular-locale_pt-br.js”, que pode ser baixado de: “https://code.angularjs.org/1.2.9/i18n/”:

<script src="angular.min.js"></script>
<script src="angular-locale_pt-br.js"></script>

Eis o resultado:

Para ver mais informações sobre formatação e localização:


Controllers

O Angular é um framework MVW, logo, pode ser usado com um “Controller”, que controla como o Modelo será atualizado. Esse “Controller” é criado em Javascript e associado a um tag com a diretiva: “ng-controller”.

A comunicação entre a Visão e o Controller se dá através do escopo ($scope), que é um objeto representando as variáveis e funções que estão no escopo daquele controlador.

Vamos ver um exemplo simples (“controller.html”):

<!doctype html>
<html ng-app="exemplo">
  <body>
    <div ng-controller="controlador">
      <h1>Olá {{seuNome}}!</h1>
    </div>
    <script src="angular.min.js"></script>
    <script type="text/javascript">
      angular.module('exemplo', [])
        .controller('controlador', ['$scope', function($scope) {
        $scope.seuNome = 'Cleuton Sampaio';
      }]);  
    </script>  
  </body>
</html>

Para começar, inicializamos a nossa aplicação no Angular passando o nome de um Módulo (ng-app=”exemplo”). Um módulo é onde os objetos de nossa aplicação, como: Controllers, são configurados. Podemos declarar um módulo com a diretiva “ng-app” e ele será utilizado para inicializar nossa aplicação (“bootstrap”):

<html ng-app="exemplo">

Depois, definimos o nosso módulo (em um tag “script”, que pode ser inline ou em um arquivo externo):

angular.module('exemplo', [])


E adicionamos um Controller chamado “controlador”:

angular.module('exemplo', [])
.controller('controlador', ['$scope', function($scope) {
$scope.seuNome = 'Cleuton Sampaio';
}]);

Poderíamos ter feito isso de outra forma (“controller2.html”):

<!doctype html>
<html ng-app="exemplo">
  <body>
    <div ng-controller="controlador">
      <h1>Olá {{seuNome}}!</h1>
    </div>
    <script src="angular.min.js"></script>
    <script type="text/javascript">
      var modulo = angular.module('exemplo', []);
      modulo.controller('controlador', ['$scope', function($scope) {
        $scope.seuNome = 'Cleuton Sampaio';
      }]);  
    </script>  
  </body>
</html>

Definimos um módulo com o método:
angular.module('<nome>', [<módulos dos quais depende>])

Depois de definirmos o módulo, podemos adicionar controladores a ele com o método:
controller('<nome>', '<construtor>')

A sintaxe do controlador é um pouco confusa, mas vamos explicar:

modulo.controller('controlador', ['$scope', function($scope) {
$scope.seuNome = 'Cleuton Sampaio';

}]);

O segundo parâmetro é um vetor, e indica os módulos dos quais este controlador é dependente, no caso o “$scope”, e a função de construção do controller. Neste caso, nosso controller apenas coloca uma variável “seuNome” no escopo, inicializando-a.

Finalmente, nós associamos o Controller a um grupo de tags usando a diretiva “ng-controller”:

<div ng-controller="controlador">

Agora, dentro dessa “div” esse controlador manipula o DOM e inicializa o escopo.

Comportamento

Assim como usamos os Controllers para modificar o escopo válido para um conjunto de tags, também podemos adicionar comportamento. Vamos ver um exemplo (“controller-comportamento.html”):

<!doctype html>
<html ng-app="exemplo">
  <body>
    <div ng-controller="controlador">
      <h1>Olá {{seuNome}}!</h1>
      <p>{{ bomdia(seuNome) }}</p>
    </div>
    <script src="angular.min.js"></script>
    <script type="text/javascript">
      var modulo = angular.module('exemplo', []);
      modulo.controller('controlador', ['$scope', function($scope) {
        $scope.seuNome = 'Cleuton Sampaio';
        $scope.bomdia  = function(nome) {
            return "Bom dia, " + nome;
        };
      }]);  
    </script>  
  </body>
</html>

Repetição


É muito comum criarmos páginas que repetem elementos, como listas, por exemplo. A diretiva “ng-repeat” pode ser utilizada para isto. Vamos ver um exemplo simples:

<!doctype html>
<html ng-app="exemplo">
  <body>
    <div ng-controller="controlador">
     <ul>
      <li ng-repeat="pessoa in lista">
        {{ pessoa.nome }},
        {{ pessoa.telefone }}
      </li>
     </ul>
    </div>
    <script src="angular.min.js"></script>
    <script type="text/javascript">
      angular.module('exemplo', [])
        .controller('controlador', ['$scope', function($scope) {
        $scope.lista = [{nome : 'Cleuton Sampaio', telefone : 555},
                        {nome : 'Fulano de Tal',   telefone : 444},
                        {nome : 'Beltrano',        telefone : 333}];
      }]);  
    </script>  
  </body>
</html>


Nosso controlador colocou um vetor de objetos no escopo, com o nome “lista”. Logo, podemos criar um “loop” com o “ng-repeat”, pegando cada elemento do vetor e usando para gerar os tags “<li>” automaticamente.

Consumindo RESTful Webservices



Nas aplicações MEAN, a camada de apresentação é totalmente feita em HTML, usando o Angular para requisitar informações e modificar o DOM da página. Geralmente, consumimos RESTful Webservices para gerar o conteúdo a ser exibido. Eis a resposta esperada:

  E temos que mesclar os templates Jade com o Angular.js, para formar a página principal.

Para começar, vamos criar um RESTful Webservice que retorne a lista de estilos que existem no nosso banco de dados (lembra da lição sobre MongoDB?)

Só para relembrar, eis o esquema “mongoose” do nosso Banco:
schemaEstilo = new mongoose.Schema(
{
nome : String
}
);


Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');

Vamos fazer uma aplicação MEAN completa, que retorna uma página HTML contendo os estilos. Vamos fazer passo a passo (“exemplo-mean”). Vamos fazer passo a passo.

1) Crie uma pasta independente;

2) Nessa pasta, rode o comando: “express exemplo”;

3) Entre na pasta “exemplo”;

4) Edite o arquivo “package.json” e acrescente o “mongoose” como dependência:

{
"name": "exemplo",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"express": "~4.2.0",
"static-favicon": "~1.0.0",
"morgan": "~1.0.0",
"cookie-parser": "~1.0.1",
"body-parser": "~1.0.0",
"debug": "~0.7.4",
"jade": "~1.3.0",
"mongoose" : "*"
}
}

5) Rode “npm install”;

6) Altere o arquivo “app.js” para incluir o esquema do Banco:

// Database:
mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/banco', function(erro) {
    if(erro) {
	    throw new Error('erro ao conectar com o banco: ' + erro);
    }
    else {
	    console.log('Conexão com o banco OK');
    }
});
schemaEstilo = new mongoose.Schema(
	{ 
		nome : String
	}
);


Estilo = mongoose.model('Estilo',schemaEstilo, 'estilo');

7) Edite o arquivo de rotas (“routes/index.js”):

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res) {
  res.render('index', { title: 'Estilos de artistas' });
});

/* Lista de artistas */
router.get('/estilos', function(req, res) {
  Estilo.find({})
  .exec(function(erro,estilos) {
    if(!erro) {
      res.header('Content-type', 'application/json; charset=utf-8');
      res.json(200,estilos);
    }
    else {
      throw new Error('Erro ao acessar banco: ' + erro);
    }
  });
});

module.exports = router;

8) Baixe o “angular.min.js” e coloque em “public/javascripts”. Você pode obtê-lo de: “https://code.angularjs.org/1.2.9/angular.min.js”. Veja a última versão antes de baixar;

9) Crie um javascript contendo um Controller Angular.js, que irá acessar o Servidor e baixar uma lista de estilos. O arquivo deverá se chamar “estilocontroller.js”, e ficará na pasta “public/javascripts”. Eis o código-fonte:

angular.module('exemplo',[])
  .controller('estilocontroller', ['$scope','$http',
    function($scope, $http) {

      $http.get('/estilos').success(function(lista) {
        $scope.estilos = lista;
      });
    }
]);

Bem, é um controller como o que já vimos, a diferença é que ele usa mais um Serviço Angular além do $scope, que é o $http.

O módulo $http serve para enviarmos requests Ajax a um Servidor. Sua API possui vários métodos, como: “get”, “post”, “head”, “put”, “delete”, permitindo que nos comuniquemos com uma API REST usando os verbos HTTP. Para ver a documentação completa: https://docs.angularjs.org/api/ng/service/$http

Os métodos do $http retornam “Promises” com os métodos “success()” e “error()”. Se quisermos fazer um request POST, poderíamos usar assim:

$http.post('/url', dados).success(successCallback);

Esse “estilocontroller” apenas envia um request para a URI “/estilos” e recebe uma resposta JSON. Conforme nossas modificações no arquivo “routes/index.js”, feitas no passo 7, essa resposta é um vetor JSON contendo os objetos “Estilo” encontrados no banco MongoDB.


10) Modifique o template Jade base “views/layout.jade” para incluir as diretivas Angular e os scripts:

doctype html
html(ng-app="exemplo")
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript' src='/javascripts/angular.min.js')
    script(type='text/javascript' src='/javascripts/estilocontroller.js')
  body
    block content

Com isso, o Jade vai gerar uma página Angular, que vai usar o módulo “exemplo” para “bootstrap”. Esse módulo foi criado no arquivo “estilocontroller.js”:

angular.module('exemplo',[])


11) Modifique o template Jade “views/index.jade” para incluir a chamada ao Controller e a renderização dos estilos:

extends layout

block content
  h1= title
  p #{title}
  div(ng-controller="estilocontroller")
    li(ng-repeat="estilo in estilos") 
      {{ estilo.nome }}

Nós queremos que o título seja fornecido diretamente pela Rota, e queremos uma “<div>” que contenha um escopo preenchido pelo nosso Controller. Ele foi criado dentro do arquivo “public/javascripts/estilocontroller.js”, e foi anexado ao módulo “exemplo”:

angular.module('exemplo',[])
.controller('estilocontroller',

Temos um Controller que vai colocar um Objeto JSON chamado “estilos” dentro do escopo. Como é um vetor, podemos usar o “ng-repeat” para fazer o Angular gerar tags “<li>” para cada objeto desse vetor, conforme já vimos em um exemplo anterior.

Atualizar dados

É muito simples. Você já sabe como atualizar um esquema Mongoose, e também já sabe como enviar parâmetros via GET ou POST, então, é bem simples.

Exercício

Agora, que já fizemos um belo exercício juntos, é hora de você tentar ir além e fazer um exercício mais elaborado.

Quero uma página assim (sim, você já fez isso antes...):


A página inicial traz a lista de artistas e, quando clicamos sobre o nome de um artista, seus dados e suas músicas aparecem na parte de baixo.

Você já fez esse exercício, usando jQuery. Agora, vai usar Angular. Lembre-se de usar:

  • Três rotas: “/” para a página inicial, “/artistas”, para trazer a lista de artistas (como fizemos com a lista de estilos), e “/artista/:nome”, para trazer os dados de um artista;
  • “ng-show” para só mostrar os dados do Artista, quando tiver um artista selecionado (use a variável de escopo criada pela rota “/artista/:nome”);

Se não conseguir fazer, veja a resposta em “exercicio-mean”.