¿Cómo funciona...? vanilla_nested

Para iniciar el blog, me pareció que lo más correcto es empezar por la gema que creé yo mismo.

Link: https://rubygems.org/gems/vanilla_nested

¿Qué hace la gema?
La función de la gema es facilitar la implementación de agregar campos dinámicamente en formularios anidados.

Un ejemplo muy simple sería que un modelo User tienen una relación has_many con el modelo Pet, entonces, un usuario tiene una o más mascotas. Al momento de cargar las mascotas, al ser una cantidad variable, es más user friendly que se puedan agregar mascotas usando javascript.

¿Cómo lo hace?

No es el objetivo de este blog explicar mucho en detalle qué hace la gema sino cómo lo hace, así que hay que empezar por el principio, rails hace autoload de la gema. Analizemos el punto de entrada:
# lib/vanilla_nested.rb

require 'vanilla_nested/view_helpers'

module VanillaNested
  class Engine < ::Rails::Engine
    initializer 'vanilla_nested.initialize' do |_app|
      ActiveSupport.on_load :action_view do
        ActionView::Base.send :include, VanillaNested::ViewHelpers
      end
    end
  end
end

Vemos dos cosas importantes: primero se requiere un archivo "view_helpers", y luego vemos que el initializer espera hasta que ActiveSupport cargue el módulo de ActionView. Una vez cargado, incluye los helpers definidos por la gema.

Los helpers son algunas funciones que queremos poder invocar desde la vista, por eso se incluye este módulo en ActionView::Base, en ambos métodos se usa código que provee ActionView.

Veamos el archivo que define estos helpers:
# lib/vanilla_nested/view_helpers.rb

module VanillaNested
  module ViewHelpers
    def link_to_add_nested(form, association, container_selector, link_text: nil, link_classes: '', insert_method: :append, partial: nil, partial_form_variable: :form)
      # código...
    end

    def link_to_remove_nested(form, link_text: 'X', fields_wrapper_selector: nil, undo_link_timeout: nil, undo_link_text: 'Undo', undo_link_classes: '')
      # código...
    end
  end
end

Gracias al include, en la vista tenemos acceso a estos dos métodos que provee la gema para generar los campos y elementos HTML necesarios tanto para agregar nuevos campos como para eliminarlos.


Para la parte dinámica de la gema, los dos view helpers no son suficientes porque sólo generan el HTML, necesitamos código javascript. Veamos el archivo .js del proyecto:
// app/assets/javascripts/vanilla_nested.js

(function(){
  window.addVanillaNestedFields = function(event) {
    // codigo...
  }

  window.removeVanillaNestedFields = function(event) {
    // codigo...
  }

  function hideWrapper(wrapper) {
    // codigo...
  }

  function unhideFields(wrapper) {
    // codigo...
  }

  // más funciones...
})()

Lo primero que vemos es que el código javascript está encerrado en una función anónima que se ejecuta apenas se carga el código. Esto se hace para que lo que se defina adentro no "ensucie" el main scope de javascript (normalmente el objeto window).

Como necesitamos algunas funciones globales para que los botones de los view helpers puedan invocarlas, se asignan explícitamente al objeto window (window.addVanillaNestedFields = ...), el resto de las funciones que se usan internamente quedan dentro del scope definido por esta función anónima.


Finalmente vemos cómo el código js conecta los links generados por el helper con las funciones de javascript:
// app/assets/javascripts/vanilla_nested.js

  document.addEventListener('DOMContentLoaded', function(){
    document.querySelectorAll('.vanilla-nested-add').forEach(el => {
      el.addEventListener('click', addVanillaNestedFields);
    })

    document.querySelectorAll('.vanilla-nested-remove').forEach(el => {
      el.addEventListener('click', removeVanillaNestedFields);
    })
  })

Algo importante al incorportar assets a una gema es dónde se ubican para que rails los encuentre.

Conectando los cables
Las instrucciones de la gema son estas:

1- agregar la gema `gem 'vanilla_nested'` y correr bundler
Por al mecanismo de autoload de rails, al iniciar va a ejecutar el archivo lib/vanilla_nested.rb y se inicia el proceso que comenté al principio.

2- agregar al archivo application.js `//= require vanilla_nested`
Gracias al método de búsqueda que usa Sprockets para requerir archivos, va a buscar tanto en el directorio app/assets de la aplicación como en app/assets de cada una de las gemas, por eso la gema lo incluye en app/assets/javascripts.

Finalmente, el uso de la gema es mediante los helper methods, por ejemplo:
= link_to_add_nested(user_form, :pets, '#pets')

Esto genera código html para las acciones, luego el javascript se ejecuta y agrega los event listener para los eventos `click`. De esta forma se integran las distintas partes.

¿Posibles mejoras?
- Actualmente la integración con Webpacker require copiar y mantener actualizada la copia del archivo .js. La única solución que proveen es generar, además de la gema, un paquete de NPM/Yarn, y agregar el paquete en el proyecto junto con la gema.

- No es posible customizar los elementos generados por los helpers, son tags A, sería más flexible permitir usar tag A o BUTTON o SPAN por ejemplo para permitir adaptarse a distintas necesidades.

- La firma de los helper methods tiene demasiados parámetros, tal vez sería mejor que los métodos acepten un hash de opciones y dentro del método obtener los valores o proveer fallbacks. No es un problema porque son opcionales, pero si resulta difícil de leer.

- Seguramente no se cumplan todas las buenas prácticas al crear una gema (archivo versions.rb, engine.rb, realitie.rb, podría haber un método installer para copiar el .js si se usa webpacker, etc).

Comentarios

Entradas populares