15 ago 2016

Two-way data binding com Backbone.Js

Há tempos venho trabalhando com projetos que possuem uma arquitetura que combina ChaplinJs + Backbone.Js. Por terem essa arquitetura já desenvolvida, sempre segui os padrões já estabelecidos. Cansado de utilizar o JQuery Validator para validação de formulário nesses projetos, fui atrás de um substituto que não me desse dor de cabeça. E eis que encontrei o Backbone.Validator e, consequentemente, o Backbone.Stickit.

O Backbone.Validation cuida da validação dos dados na model, diferente do JQuery Validator que valida direto no HTML. O Validation é bem simples de usar; precisamos declarar o atributo “validator” na model passando as propriedades que serão validadas e as validações que serão aplicadas nos atributos. Para fazer uma validação “model + view”, basta fazer o bind da view com o Backbone.Validation. Uma grande vantagem de validar os dados na model é poder reaproveitar as regras de validação em qualquer lugar.
No meu exemplo tenho uma model que representa um Setor: ela possui quatro atributos, três básicos e um “complexo”, que é um nested object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  class SetorModel extends Backbone.DeepModel

    urlRoot: 'api/setor'

    validation:
      nome:
        required: true
        maxLength: 255
      descricao:
        required: true
        maxLength: 4000
      ordem:
        required: true
      'grupo.id':
        required: true

Existem muitas outras validações e métodos de suporte que podem ser aplicados na model. Para este post, irei mostrar só estes. O código da view está bem simples. Por enquanto só tem há método initialize que faz o bind da view com o Backbone.Validation:

1
2
3
4
5
6
7
8
9
10
  class SetorFormView extends Core.FormView

    # Override Methods
    # =============================================================

    initialize: (options={}) ->
      super
      _(@).extend options

      Backbone.Validation.bind @

Agora que existe a validação na model e o bind com a view está feita, como vamos preencher a model com os valores dos inputs ou preencher os inputs com os valores da model e exibir o resultado da validação para o usuário? É ai que entra o Backbone.Stickit, criado pelo jornal americano NYTimes, para fazer o bind dos atributos da model com os elementos da view, possibilitando o Two-way data binding entre os dois. Esse módulo é bem simples de utilizar, basta definir a propriedade “bindings” na view com os seletores dos inputs, o atributo da model e uma opção que define se é preciso fazer o Two-way data binding. Depois de definir os bindings, precisamos chamar um método global que o stickit disponibiliza para criar o binding. Esse método pode ser chamado em qualquer momento, mas costumo deixar no método “afterRender”, um método que é chamado manualmente após renderizar a view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  class SetorFormView extends Core.FormView

    # Override Methods
    # =============================================================
    bindings:
      '#nome':
        observe: 'nome'
        setOptions:
          validate: true
      '#ordem':
        observe: 'ordem'
        setOptions:
          validate: true
      '#descricao':
        observe: 'descricao'
        attributes: [
          name: 'disabled', observe: 'nome', onGet: (val) -> _.isEmpty val
        ]
        setOptions:
          validate: true
      '#grupoSetor':
        observe: 'grupo.id'
        selectOptions:
          collection: 'this.grupos'
          labelPath: 'grupo'
          valuePath: 'id'
          defaultOption:
            label: 'Selecione'
            value: null
        setOptions:
          validate: true

    # initialize omitido
   
    afterRender: ->
       @stickit()
       @cacheDOMElements()

Neste passo definimos o objeto “bindings”, onde as keys do objeto são os seletores dos elementos no form. Cada key recebe um objeto no qual, que como se pode observar, existem duas propriedades que se repetem para todas as keys do “bindings”, que são: “observe” e “setOptions”. O “observe” é o atributo da model e o “setOptions” é um objeto que pode receber algumas opções, dentre elas, a “validate”. Trata-se do atributo mandatório para fazer o Two-way D. B. Só para fins de demonstração, adicionei dois atributos diferentes, o “attributes” em “#descricao” (que está simulando o “ngDisabled”, quando o atributo “nome” da model for vazio, o stickit adicionará uma classe disabled no meu input descrição) e o “selectOptions” em “#grupoSetor”, que cria as options do meu select com base na collection de grupos que existe na minha view. No afterRender está a chamada para o método “stickit” que utiliza os bindings.

Agora que a model e o formulário da view estão sendo validados e a model já possui os erros do formulário, só falta disponibilizar pro usuário um feedback com os campos que estão inválidos e o motivo. Para isso, vamos sobrescrever dois métodos do Backbone.Validation.callbacks: “valid” e “invalid”. O valid simplesmente adiciona uma classe “success” no control-group e o invalid adiciona a classe “error” e insere a mensagem de erro.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  class SetorFormView extends Core.FormView

    # initialize

    # afterRender
   
    getSelector = (view, attr) ->
      for binding in view._modelBindings
        if binding.config.observe is attr
          selector = binding.config.selector
          break

      selector

    # Extend the callbacks to work with Bootstrap, as used in this example
    # See: http://thedersen.com/projects/backbone-validation/#configuration/callbacks
    _.extend Backbone.Validation.callbacks,
      valid: (view, attr, selector) ->
        $el = view.$ getSelector view, attr
        $group = $el.closest('.control-group')
        $group.addClass 'success'
        $group.removeClass 'error'
        $group.find('.help-block').html('').addClass 'hidden'
        return

      invalid: (view, attr, error, selector) ->
        $el = view.$ getSelector view, attr
        $group = $el.closest('.control-group')
        $group.addClass 'error'
        $group.removeClass 'success'
        $group.find('.help-block').html(error).removeClass 'hidden'
        return

No html do formulário precisamos adicionar um span com a classe “help-block” e “hidden” para inserir a mensagem de erro.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    <div class="control-group">
      <label class="control-label" for="descricao">Descrição<em>*</em></label>
      <div class="controls">
        &lt;textarea type=&quot;text&quot; class=&quot;input-xxlarge&quot; id=&quot;descricao&quot; rows=&quot;6&quot; name=&quot;descricao&quot; label=&quot;Descrição<em>*</em>" required="true"&gt;</textarea>
        <span class="help-block hidden"></span>
      </div>
    </div>

    <div class="control-group">
      <label class="control-label" for="grupoSetor">Grupo<em>*</em></label>
      <div class="controls">
        &lt;select class=&quot;input-xxlarge&quot; id=&quot;grupoSetor&quot; name=&quot;grupoSetor&quot; label=&quot;Grupo<em>*</em>" required="true"&gt;
       
        <span class="help-block hidden"></span>
      </div>
    </div>

Pronto! Agora já conseguimos fazer a validação com Two-way data binding, de forma simples. Ate agora venho utilizando e fazendo testes, e o único problema que tive foi validar nested object attributes como o atributo “grupo” da minha model neste exemplo. Ele é um objeto e, quando recebo um setor em JSON do backend, ele vem como:

1
2
3
4
5
6
7
8
9
{
   id: 1,
   nome: 'Setor 1',
   descricao: 'Descricao',
   grupo: {
      id: 1,
      nome: 'Grupo 1'
   }
}

Para fazer a validação e setar o atributo de forma correta na model, utilizei o Backbone Nested Model, e fiz um pull request para o Backbone.validation arrumando o problema com “validação tardia” que ocorre com os nested attributes. Um ponto interessante que tive que mexer foi a manipulação dos dados na model, quando eu recebia um setor, vinha de acordo com aquele JSON. Mas para enviar ao serviço, precisava ser assim:

1
2
3
4
5
6
{
   id: 1,
   nome: 'Setor 1',
   descricao: 'Descricao',
   grupoId: 1
}

Não recebia o grupo como objeto e sim só o id dele, para isso, sobrescrevi o método de salvar da model para criar esse atributo:

1
2
3
4
5
6
7
8
  class SetorModel extends Backbone.DeepModel
   
    #  validation omitido

    save: -&gt;
      @set 'grupoId', @get 'grupo.id'
      @unset 'grupo'
      super

Dica:

É interessante abstrair a parte que sobrescreve os métodos invalid e valid do Backbone.Validation.callbacks para um arquivo pois esses métodos são usados sempre. Há algumas outras configurações globais que podem ser adicionadas também neste arquivo, alguns handlers e a tradução das mensagens de feedback ao usuário:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
     Backbone.Validation.configure forceUpdate: true, selector: 'name', labelFormatter: 'label'
   
     Backbone.Stickit.addHandler
      selector: 'select.chosen'
      initialize: ($el, model, options) -&gt;
        $el.chosen()

        up = (m, v, opt) -&gt;
          if !opt.bindKey
            $el.trigger 'liszt:updated'
          return

        @listenTo model, 'change:' + options.observe, up
        return

     _.extend Backbone.Validation.messages,
      required: "Este campo é requerido.",
      remote: "Por favor, corrija este campo.",
      email: "Por favor, forneça um endereço de email eletrônico válido.",
      url: "Por favor, forneça uma URL válida.",
      date: "Por favor, forneça uma data válida.",
      dateISO: "Por favor, forneça uma data válida (ISO).",
      dateITA: " Por favor, forneça uma data válida.",
      digits: "Por favor, forneça somente dígitos.",
      creditCard: "Por favor, forneça um cart&atilde;o de cr&eacute;dito válido.",
      equalTo: "Por favor, forneça o mesmo valor novamente.",
      accept: "Por favor, forneça um valor com uma extenção' válida.",
      maxLength: "Por favor, forneça n&atilde;o mais que {0} caracteres.",
      minLength: "Por favor, forneça ao menos {0} caracteres.",
      rangeLength: "Por favor, forneça um valor entre {0} e {1} caracteres de comprimento.",
      range: "Por favor, forneça um valor entre {0} e {1}.",
      max: "Por favor, forneça um valor menor ou igual a {1}.",
      min: "Por favor, forneça um valor maior ou igual a {1}.",
      summary: "Seu formulário contém {0} {1}, verifique os detalhes acima",
      error_singular: 'erro',
      error_plural: 'erros'

Gabriel Suaki – Software Engineer at redspark.
GitHub: GSuaki
Twitter: @gsuaki

Gabriel Suaki
About Gabriel Suaki

Software Engineer at redspark Gabriel Suaki - Software Engineer at redspark.

Leave a Comment