Criando do zero uma aplicação com Rails5 + AngularJS + Bootstrap

Criando do zero uma aplicação com Rails5 + AngularJS + Bootstrap

Decidi criar essa postagem por ter tido muita dificuldade em algo que teoricamente deveria ser simples: criar uma aplicação usando Rails apenas como provedor de API, AngularJS para renderizar as páginas e o Bootstrap como framework CSS. Várias vezes me deparava com tutoriais desatualizados, ou que não explicavam desde o básico para quem não está habituado a desenvolver para web.
Então decidi escrever essa postagem, e aqui buscarei explicar passo a passo e porque de cada coisa.

Tal como um bom evangelizador da JetBrains, recomendo fortemente que use as IDEs WebStorm e RubyMine, pois elas possuem vários recursos que facilitam bastante o nosso trabalho, da qual descreverei no decorrer deste manual.

Eu reconheço que esse manual ficou meio longo e muito detalhado, então, para deixá-lo mais divertido, escute essa play list.

Por qual razão usar esses frameworks?

Projeto no lado do servidor

Após instalar o Rails, precisamos iniciar um projeto. Para isso, abra o RubyMine e vá em "New Project". No painel lateral esquerdo já tem uma opção "Rails API". Escolha essa.
Em seguida, em "Preconfigure for selected database", escolha "sqlite3". Ele será o sucifiente para o nosso caso.
Então, agora crie o projeto.

O Rails gerou toda a base do projeto. Agora vamos criar um model. Para isso, vamos usar o generator scaffold.
Generators é uma das melhores features do Rails. Com ele, você chama um script que, com base nos parâmetros passados, gerará códigos para você.
O scaffold gerará para nós tanto o migration (script de atualizações do banco de dados) como as entradas RESTful para acessar o nosso model.
A propósito, se não tivéssemos criado uma aplicação api-only, o scaffold geraria vários outros arquivos desnecessários para uma API. Esse é um exemplo das mudanças causadas ao criar uma aplicação api-only.

Para usar o generator scanffold, use ctrl + alt + g, escreva scaffold e dê enter. O próprio RubyMine explica os argumentos para usar esse generator: ModelName [field:type]
Primeira dica: escreva o nome do model no singular, pois o Rails tomará o cuidado em nomear com plural ou singular do jeito certo. Segunda dica: se não especificar o type, o Rails irá supor que aquele field é uma string.

Para nosso exemplo, use como argumento person name age:integer e dê ok. Optei por usar no exemplo o nome person para resaltar a diferença no plural e singular.

A vantagem de usar o generator diretamente no RubyMine é que ele já abrirá os arquivos importantes gerados. No caso, são três:

Caso esteja confuso sobre para que serve o person.rb e o que é um model, recomendo que estude sobre o ActiveRecord. Esse é o nome do padrão que o Rails usa para lidar com o banco de dados.

Nós precisamos criar o nosso banco de dados. Para isso, use ctrl + alt + r, escreva db:create e dê enter, e clique em OK. Assim o arquivo db/development.sqlite3 foi gerado. Para visualizá-lo, podemos abrir o painel "Database" localizado ao lado direito, e ir em "+" -> Data Source -> Sqlite. Na parte "File", clique em "..." e selecioe o arquivo gerado, e então clique em "ok". Com isso, poderemos visualizar o nosso banco de dados.
Verá que ele ainda está vazio, pois ainda precisamos executar os scripts de migração. Para isso, use ctrl + alt + r e escreva db:migrate e clique em OK. Assim executamos os scripts da pasta db/migrate, que no caso é apenas o de "create_person". Caso não entendeu o que foi feito, leia isso.
No painel do banco de dados, se der refresh verá que agora tem a tabela "person".

Para iniciar o servidor, tecle F9, escolha "Development: Project" e dê enter. Agora ele está rodando em localhost:3000

Antes de partimos para o lado do cliente, podemos testar usando o terminal. Para isso, usaremos o comando curl. Caso não o conheça, curl é um serviço por linha de comando e biblioteca para transferência de dados por URL.
No painel inferior do RubyMine há um botão chamado "Terminal", e trata-se de um emulador do terminal. Abra-o e execute curl -H "Content-Type:application/json; charset=utf-8" -d '{"name": "macabeus", "age": 20}' http://localhost:3000/people
Explicando os parâmetros: é necessário que especifiquemos que estamos enviando um json, e para isso escrevemos aquele header usando o parâmetro -H. O -d é para enviar os dados especificados, e serão transferidos por POST.
Se atualizar a tabela people, verá que agora há um registro nela!
Para ficar mais legal, crie mais alguns registros seguindo o exemplo.

Para listar todos os nossos registros, podemos usar curl http://localhost:3000/people
Por padrão, o curl usa o método GET. A requisição do parágrafo anterior estava enviando por POST por causa do parâmetro -d.
Para retornar apenas o registro de ID 1 basta usar curl http://localhost:3000/people/1

Para apagar o registro de ID 1, podemos usar curl -X DELETE http://localhost:3000/people/1
O -X serve para escolhermos o request.

Aparentemente, parece que está tudo pronto para irmos para o client side, porém, falta ainda mais um detalhe. O nosso cliente vai rodar em um domínio, enquanto o servidor rodará em outro. Por conta disso, nós precisamos configurar o CORS (Cross-Origin Resource Sharing). Ele é de um mecanismo que permite que recursos de um endereço seja solicitado a partir de outro domínio. Por padrão, uma consulta feita no navegador seria negada pelo Rails.
A razão de ter dado certo com o curl e falhar no navegador é que o navegador, por padrão, envia no header o Origin. Se enviar esse header no curl obterá o mesmo erro. Eu também não sabia disso e responderam essa minha dúvida no StackoverFlow.

Para evitar problemas de CORS, descomente a linha gem 'rack-cors' no arquivo Gemfile, no terminal insira bundle install e agora vá para o arquivo config/initializers/cors.rb.
Ele por sí só já é auto-explicativo. Descomente todas as linhas de código e, para simplificar, libere todas as origens, alterando o parâmetro de origins para *.
Agora que configuramos o CORS, mate o servidor (ctrl + F2) e inicie-o novamente (F9).

Projeto no lado do cliente

Agora que já entedemos e configurados o lado do servidor, temos que entender o do cliente. Abra o WebStorm e inicie um novo projeto. Até poderíamos usar o templete "AngularJS", mas, para simplificação, é melhor começar um projeto do zero. Selecione "Empty Project" e clique em "Create".

Tecle alt + 1 para abrir o painel de diretório, clique com o botão direito do mouse sobre a pasta do projeto e crie um arquivo HTML chamado index.html e depois um arquivo JS chamado app.js.

No index.html, coloque

<html ng-app="example">

<body ng-controller="myApp as app">
<h1>{{ app.exampleString }}</h1>

<table>
    <thead>
    <tr>
        <th>#</th>
        <th>Name</th>
        <th>Age</th>
        <th>Action</th>
    </tr>
    </thead>
    <tbody>
    <tr ng-repeat="person in app.peopleList">
        <th scope="row">{{ person.id }}</th>
        <td>{{ person.name }}</td>
        <td>{{ person.age }}</td>
        <td>
            <button ng-click="app.personRemove(group.id)">Remover</button>
        </td>
    </tr>
    <tr>
        <th></th>
        <td><input type="text" ng-model="app.personNewName"></td>
        <td><input type="number" ng-model="app.personNewAge"></td>
        <td><button ng-click="app.personAdd()">Adicionar</button></td>
    </tr>
    </tbody>
</table>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"></script>
<script src="app.js"></script>
</body>

</html>

E no app.js, coloque

angular.module('example', [])
    .controller('myApp', ['$http', function($http) {
        var self = this;

        self.exampleString = 'Example by Macabeus';

        self.peopleList = [];

        function peopleListRefresh() {
            $http.get('http://localhost:3000/people').then(function(response) {
                self.peopleList = response.data;
            }, function(response) {
                alert('Erro enquanto atualiza a listagem');
                console.log(response);
            });
        }
        peopleListRefresh();

        self.personAdd = function() {
            $http.post('http://localhost:3000/people', {'name': self.personNewName, 'age': self.personNewAge}).then(function(response) {
                peopleListRefresh();
                self.personNewName = '';
                self.personNewAge = '';
            }, function(response) {
                alert('Erro enquanto adicionava');
                console.log(response);
            });
        };

        self.personRemove = function(todoId) {
            $http.delete('http://localhost:3000/people/' + todoId).then(function(response) {
                peopleListRefresh();
            }, function(response) {
                alert('Erro enquanto removia');
                console.log(response);
            });
        };

    }]);

Não explicarei minuciosamente esse código, pois foge do escopo desse manual, mas creio que, caso já tenha uma noção de JavaScript, consiga ter uma ideia só lendo.

O WebStorm vai reclamar que não reconhece de onde veio variáveis como "angular", presente na primeira linha do JS. Para resolver isso, estando com o arquivo app.js aberto, clique no homenzinho que fica no canto inferior direito e, em seguida, em "Libraries in scope". Agora, adicione as bibliotecas "angular", "bootstrap" (será usada em breve) e "jquery".

Para realmente testar e sentir na veia os poderes do WebStorm, vá para a aba do HTML, aperte F9, deixe marcado o index.html e dê enter.
Desse modo, o WebStorm começará a debugar seu site usando o navegador. Para poder haver uma comunicação entre o seu navegador e o WebStorm, é necessário instalar uma extensão. Ele vai te avisar isso num balão com o link de download da extensão. Instale-a e veja a mágica acontecer.
Comece a brincar jogando breakpoint no JavaScript e fique passenado em todas as ferramentas de debug que o WebStorm fornece. São bem interessantes.
Uma outra vantagem de usar o WebStorm é que ele gerará um servidor local para rodar seus códigos. Ficar usando o arquivo diretamente costuma da problemas de CORS.
A extensão do navegador que o WebStorm usa tem um bug: após abrir o navegador, primeiro você deve abrir o WebStorm e só depois o RubyMine. Caso contrário, não funcionará corretamente.

Ótimo! Agora temos o nosso cliente e servidor funcionando! Mas ainda precisamos deixar o site bonito. Para isso, usaremos o Bootstrap. No HTML, adicione a seguinte linha após <html ng-app="treegroups">:

<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">

Ele puxará o CSS do Bootstrap a partir do site MaxCDN. O MaxCDN é um provedor de CDN. E o que diabos são CDN? São vários servidores espelhos espalhados pelo mundo para prover algum recurso de forma veloz.
Agora, acima do <script src="app.js"></script>, adicione:

<script src="ui-bootstrap-2.0.0.js"></script>

Esse é JS do UI Bootstrap, que jaja vamos baixar. Ele serve para fazer a ponte entre os componentes do Bootstrap e o AngularJS, para deixar os componentes mais naturais de serem usandos no AngularJS. Não há um CDN para os componentes do Boostrap para o AngularJS, então baixe nesse site o JS e coloque na sua pasta.

Então faltará dizer ao AngularJS para importar o UI Bootstrap. Para isso, altere a primeira linha do app.js para:

angular.module('treegroups', ['ui.bootstrap'])

Se atualizar a página não verá os poderes do Bootstrap, pois nenhum de nossos elementos estão com as classes dele. Então, substitua todo o código do index.html por esse:

<html ng-app="example">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">

<body ng-controller="myApp as app">
<h1>{{ app.exampleString }}</h1>

<div class="row">
    <div class="col-sm-12">
        <table class="table table-striped">
            <thead>
            <tr>
                <th>#</th>
                <th>Name</th>
                <th>Age</th>
                <th>Action</th>
            </tr>
            </thead>
            <tbody>
            <tr ng-repeat="person in app.peopleList">
                <th scope="row">{{ person.id }}</th>
                <td>{{ person.name }}</td>
                <td>{{ person.age }}</td>
                <td>
                    <button class="btn btn-default" ng-click="app.personRemove(group.id)">Remover</button>
                </td>
            </tr>
            <tr>
                <th></th>
                <td><input type="text" ng-model="app.personNewName"></td>
                <td><input type="number" ng-model="app.personNewAge"></td>
                <td><button class="btn btn-default" ng-click="app.personAdd()">Adicionar</button></td>
            </tr>
            </tbody>
        </table>
    </div>
</div>

<div class="row">
    <div class="col-sm-12">
        <uib-progressbar max="20" value="app.peopleList.length"><span style="color:white; white-space:nowrap;">{{app.peopleList.length}} / 20</span></uib-progressbar>
    </div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"></script>
<script src="ui-bootstrap-tpls-2.0.0.min.js"></script>
<script src="app.js"></script>
</body>

</html>

Adicionei um progressbar para demonstrar um dos componentes do Bootstrap com AngularJS. No já citado site do UI Bootstrap você poderá encontrar uma lista com todos os vários componentes e exemplos.

Outros materiais

Para entender mais sobre o modo api-only do Rails 5, recomendo essa postagem do Santiago Pastorino. Ele é um dos criadores dessa feature.
Para aprender de verdade como se usar o AngularJS, recomendo fortemente o livro Desenvolvendo com AngularJS. Ele foi escrito por um dos criadores desse fantástico framework.

Finalização

Espero que com esse manual tenha dado para esclarecer sobre como integrar o Rails5 + AngularJS + Bootstrap e dado um ponta pé inicial no projeto em que você tenha em mente.