FrontEnd Driven Development com MEAN e Atomic Design

Parece uma loucura esse título não?

Mas atualmente nós vivemos revoluções na Internet nunca antes imaginadas, todo o dia novas tecnologias surgem, sistemas são trocados, linguagens são criadas.

Então estava lendo esse artigo http://www.codelord.net/2014/02/20/frontend-driven-development/
e lembrei de uma ideia que tenho, pois como trabalho um bom tempo com MEAN dá para perceber vários padrões entre o front/back/banco.

Nosso padrão de inicio de algum sistema web sempre foi pensar no backend inicialmente, aliás antes mesmo inicia-se com a modelagem do banco de dados.

Porém hoje em dia com a evolução desenfreada do frontend nós ganhamos muito poder para criar sistemas sem a necessidade de um backend atrelado a nossa aplicação, podendo criar um sistema puramente baseado em APIs de terceiros. E agora com conceitos e metodologias como: offline first, mobile first, atomic design

Exemplo de um form em AngularJs:

<form actiton="/beers">
  <label>Name:
      <input type="text" name="name" data-ng-model="beer.name"/>
  </label>
  <label>Category:
      <input type="text" name="category" data-ng-model="beer.category"/>
  </label>
  <label>Alcohol:
      <input type="number" step="0.1" min="0" name="alcohol" data-ng-model="beer.alcohol"/>
  </label>
  <label>Description:
      <input type="text" name="description" data-ng-model="beer.description"/>
  </label>
  <button data-ng-click="salvar(beer)">Salvar</button>
  <button data-ng-click="remover(beer)">Remover</button>
</form>

Agora vamos ver quais padrões podemos tirar desse form:

  • base url api: /beers
  • model: beer/Beer
  • schema:
    • name: String
    • category: String
    • alcohol: Number, float, min=0
    • description: String
  • ações:
    • salvar / update
    • remover / delete
  • rotas:
    • PUT /beers/:id
    • DELETE /beers/:id

Ok, então basicamente temos um formulário da entidade Beer, o qual possui seu schema definido via html, podendo ser melhorado assim:

<form actiton="/beers">
  <label>Name:
      <input type="text" name="name" data-ng-model="beer.name" data-validate="notNull" required/>
  </label>
  <label>Category:
      <input type="text" name="category" data-ng-model="beer.category" data-ng-validate="lowerCase" required/>
  </label>
  <label>Alcohol:
      <input type="number" step="0.1" min="0" name="alcohol" data-ng-model="beer.alcohol" required/>
  </label>
  <label>Description:
      <input type="text" name="description" data-ng-model="beer.description" required/>
  </label>
  <button data-ng-click="salvar(beer)">Salvar</button>
  <button data-ng-click="remover(beer)">Remover</button>
</form>

Ou em Jade:

form(actiton='/beers')
  label
    | Name:
    input(
      type='text', 
      name='name', 
      data-ng-model='beer.name', 
      data-validate='notNull', 
      required='required')
  label
    | Category:
    input(
      type='text', 
      name='category', 
      data-ng-model='beer.category', 
      data-ng-validate='lowerCase', 
      required='required')
  label
    | Alcohol:
    input(
      type='number', 
      step='0.1', 
      min='0', 
      name='alcohol', 
      data-ng-model='beer.alcohol', 
      required='required')
  label
    | Description:
    input(
      type='text', 
      name='description', 
      data-ng-model='beer.description', 
      required='required')
  button(data-ng-click='salvar(beer)') Salvar
  button(data-ng-click='remover(beer)') Remover

Logo nosso Schema no Mongoose ficará:

  • name:
    • type: String
    • validate: notNull
  • category:
    • type: String
    • lowerCase: true
  • alcohol:
    • type: Number
    • min: 0
  • description:
    • type: String

Agora vamos ver como ficaria nosso código no Mongoose:

 function notNull(value){
    return !!value;
  }

  var validator = { validator: notNull, msg: '{PATH} NULL' };

 var BeerSchema = new Schema({
   name: { 
     type: String,
     validate: validator,
     required: true,
     default: '' 
   },
   description: { 
     type: String,
     required: true, 
     default: '' 
   },
   alcohol: { 
     type: Number, 
     min: 0,
     required: true
   },
   category: { 
     type: String, 
     required: true,
     default: ''
   },
   created: { 
     type: Date, 
     default: Date.now 
   },
 });

Padrões

type

Basicamente o type do Schema é o mesmo type do input, porém com ressalvas como: email e password, os quais sabemos que são String.

validate

Quase sempre teremos algum tipo de validação dos dados tanto no front-end como no back-end, nada mais lógico do que re-utilizá-la (é assim que escreve agora?). Para isso pensei no browserify para validações mais complexas, mas isso é assunto para um próximo post.

default

Vou explicar o porque desse default: ''. Como o MongoDb pré-aloca memória para deixar seus dados sequencias para melhor performance, quando você inicia um valor com vazio, caso ele não seja passado, esse schema será presistido com todos os campos criados, caso não se faça isso, quando for alterado o registro o MongoDb terá que inserir um campo novo e modificar a sequencia que estava persistido o BSON (formato binário persistido do JSON).

required

É apenas um exemplo, pois como usamos o required: true o Schema só irá validar caso algum valor tenha sido informado.

created

Para possuirmos mais dados sobre nossas entidades nada melhor saber quando ele foi criado, logo todos os Schemas irão possuir esse campo como padrão.

Rotas

Tudo bem já fizemos a ligação entre nossos models do front-end com o do back-end, agora precisamos criar as rotas para que eles se comuniquem e essa é a parte mais fácil.

Como estaremos trabalhando com uma API Restful no backend precisamos criar pelo menos o CRUD(Create, Retrieve, Update, Delete) para a nossa entidade.

Logo cada ação do nosso CRUD possuirá uma rota específica e isso é bem padronizado ficando assim:

  • Create: POST /beers
  • Retrieve:
    • GET /beers
    • GET /beers/:id
  • Update: PUT /beers/:id
  • Delete: DELETE /beers/:id

Para criar nossa API com Node.js iremos usar o Express

// app.js
  var _beer = require('./controllers/beer');
// ...

// formato Express 3.x mas também
// funciona no Express 4.x
app.get('/api/beers/:id', function(req, res){
    _beer.retrieve(req, res);
});

app.post('/api/beers', function(req, res){
    _beer.create(req, res);
});

app.put('/api/beers/:id', function(req, res){
    _beer.update(req, res);
});

app.delete('/api/beers/:id', function(req, res){
    _beer.remove(req, res);
});

Nesse caso você pode perceber que usei o prefixo /api na rota e lá no nosso form era /beers, isso se deve pois nossa API irá funcionar tanto para o FORM como uma API JSON.

Você deve se perguntar:

Tá mas e que que é esse _beer?

Esse é nosso Controller:

// controllers/beer.js
var Beer = require("../models/beer").model;

module.exports = {
  create: function(request, response){
    var dados = request.body;

    var model = new Beer(dados);

    model.save(function (err, beers) {
      if (err){
        console.log('Erro: ', err);
        response.send("ERRO");
      }
      else{
        console.log('Cerveja inserida: ',  beers);
        response.json(beers);
      }
      response.end();
    });
  },
  retrieve: function(request, response){
    var id = request.params.id;
    if(id){
      var query = {_id: id};
      Beer.findOne(query, function (err, beers) {
        if (err){
            console.log('Erro: ', err);
            response.send("ERRO");
          }
          else{
            console.log('Cerveja achada: ',  beers);
            response.json(beers);
          }
      }); 
    }
    else{
      Beer.find(function (err, beers) {
        if (err){
            console.log('Erro: ', err);
            response.send("ERRO");
          }
          else{
            console.log('Cervejas listadas: ',  beers);
            response.json(beers);
          }
      }); 
    }
  },
  update: function(request, response){
    var id = request.params.id;
    var query = {_id: id};
    var mod = request.body;

    delete mod._id;

    Beer.update(query, mod, function (err, beers) {
    if (err){
        console.log('Erro: ', err);
        response.send("ERRO");
      }
      else{
        console.log('Cerveja alterada: ',  beers);
        response.json(beers);
      }
    });

  },
  remove: function(request, response){
    var query = {_id: request.params.id};
    // var query = {"_id": "5345d8b3f390097163e95fe4"};

    Beer.remove(query, function(err, beers) {
      if (err){
        console.log('Erro: ', err);
        response.send("ERRO");
      }
      else{
        console.log('Cerveja deletada: ',  beers);
        response.json(beers);
      }
    });

  }

E para nosso Controller funcionar estamos chamando nosso Model Beer:

// models/beer.js
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/workshop-bemean');

var db = mongoose.connection;
db.on('error', function(err){
    console.log('Erro de conexao.', err)
});
db.once('open', function () {
  console.log('Conexão aberta.')
});

var Schema = mongoose.Schema;

function notNull(value){
  return !!value;
}

var validator = { validator: notNull, msg: '{PATH} NULL' };

var BeerSchema = new Schema({
   name: { 
     type: String,
     validate: validator,
     required: true,
     default: '' 
   },
   description: { 
     type: String,
     required: true, 
     default: '' 
   },
   alcohol: { 
     type: Number, 
     min: 0,
     required: true
   },
   category: { 
     type: String, 
     required: true,
     default: ''
   },
   created: { 
     type: Date, 
     default: Date.now 
   },
 });

exports.model = mongoose.model('Beer', BeerSchema);

Com isso nós concluímos nosso scaffold do backend, porém agora precisamos fazer nosso formulário do front-end funcionar com o AngularJs e para começar precisamos criar uma rota.

Antes de tudo irei listar aqui para você como é o workflow quando vai-se criar uma funcionalidade nova no MEAN, exemplo de UPDATE:

  1. criar uma Rota no AngularJs
    • /beers/edit/:id
  2. criar a View que será chamada no templateUrl
    • /expose/beer/edit
  3. criar o Controller que será chamado em controller
    • BeerEditController
    • $scope.salvar = function(beer)
    • $http( method: 'PUT', url: '/api/beers/'+id, data: beer)
  4. criar a Rota no Express para a ação
    • app.put('/api/beers/:id', _beer.update)
  5. criar a função update no Controller no Node.js
  6. criar, se necessário, a validação no model.

Nossa rota no AngularJs ficará assim:

$routeProvider.
  when('/beers/:id', {
    templateUrl: 'expose/beers/edit',
    controller: 'BeerEditController'
  })

Na minha view Edit eu tenho:

form(actiton='/beers')
  label
    | Name:
    input(
      type='text', 
      name='name', 
      data-ng-model='beer.name', 
      data-validate='notNull', 
      required='required')
  label
    | Category:
    input(
      type='text', 
      name='category', 
      data-ng-model='beer.category', 
      data-ng-validate='lowerCase', 
      required='required')
  label
    | Alcohol:
    input(
      type='number', 
      step='0.1', 
      min='0', 
      name='alcohol', 
      data-ng-model='beer.alcohol', 
      required='required')
  label
    | Description:
    input(
      type='text', 
      name='description', 
      data-ng-model='beer.description', 
      required='required')
  button(data-ng-click='salvar(beer)') Salvar
  button(data-ng-click='remover(beer)') Remover

No BeerEditController temos:

controller('BeerEditController', ['$scope', '$http', '$routeParams',
    function ($scope, $http, $routeParams) {

    $scope.msg = 'Edição de cerveja';

// Pega valores da cerveja
    var id = $routeParams.id;
    var url ='/api/beers/'+id;

    $http({
      method: 'GET',
      url: url
    }).
    success(function (data, status, headers, config) {
      $scope.beer = data;
      $scope.msg = 'Cerveja: ' + data.name;
    }).
    error(function (data, status, headers, config) {
      $scope.msg = 'Error!';
    });

    $scope.salvar = function(data){

      $http({
        method: 'PUT',
        url: url,
        data: data
      }).
      success(function (data, status, headers, config) {
        // $scope.beers = data;
        console.log(data);
        $scope.msg = 'Cerveja alterada com SUCESSO!';
      }).
      error(function (data, status, headers, config) {
        $scope.msg = 'Error!';
      });
    }

    $scope.remover = function(data){

      $http({
        method: 'DELETE',
        url: url
      }).
      success(function (data, status, headers, config) {
        // $scope.beers = data;
        console.log(data);
        $scope.msg = 'Cerveja deletada com SUCESSO!';
      }).
      error(function (data, status, headers, config) {
        $scope.msg = 'Error!';
      });
    }

  }])

ps: exemplo simples com $http, sem resource.

Com isso nós conseguimos amarrar nosso front-end e back-end a partir do nosso form.

Devaneio

Agora imagine que lá no começo quando criamos nosso form no AngularJs poderíamos ter usado o Pattern Lab do Atomic Design, facilitando a criação de formulários re-usáveis com Scaffold partindo do front-end para o back-end e não ao contrário como é feito atualmente.

Em um próximo artigo abordarei o Atomic Design com comportamento.

Conclusão

A minha ideia de Frontend Driven Development é que o sistema comece criando seus pequenos átomos re-usáveis com comportamento, sendo agregados(moléculas/organismos) para geração dos forms/views, após essa etapa visual o scaffold de MEAN é criado em cima dos padrões definidos entre as partes gerando um sistema com o CRUD básico de todos os forms funcionando após a finalização de uma task/script.

Acredito que isso facilitará bastante a vida do desenvolvedor que está iniciando até o mais experiente, pois irá poupar bastante tempo e gerará um código consiso e padronizado.

ps: código retirado do último Workshop Be MEAN

Comentários

comments powered by Disqus