AlpineJS

ActiveAdmin + AlpineJS

AlpineJS es una excelente alternativa para agregar algo de inteligencia a nuestras vistas de ActiveAdmin. En la mayoría de los casos podemos definir todo lo que usamos directamente en nuestro recurso de ActiveAdmin, sin necesidad de archivos JS externos.

Instalando AlpineJS

# Si estamos usando Shakapacker, Webpack 5+ u otro bundler moderno
yarn add alpinejs

# Si estamos usando Webpacker
yarn add alpinejs@2

Después tenemos que agregar lo siguiente al archivo donde ActiveAdmin se inicializa, normalmente el que tiene la linea import '@activeadmin/activeadmin';

import '@activeadmin/activeadmin';

import Alpine from 'alpinejs';

window.Alpine = Alpine;
Alpine.start();

Si estás usando Alpine 2 (por Webpacker), tienes que usar lo siguiente:

import '@activeadmin/activeadmin';

import 'alpinejs'

💡 Todos los ejemplos en esta guía y el repositorio asociado usan AlpineJS 3 pero deberían funcionar en AlpineJS 2.

Intro

Un componente de AlpineJS es un elemento HTML con el atributo (también llamado directiva) x-data con todas las variables que vamos a usar dentro de un objeto de javascript.

<div x-data="{open: false}"></div>

Para que esto funcione en ActiveAdmin, tenemos que agregar el atributo x-data a inputs, input o cualquier otro elemento "wrapper"

f.inputs 'x-data':  CGI.escapeHTML("{...#{f.resource.attributes.to_json}}") do
  f.input :name
end

💡 Tenemos que usar CGI.escapeHTML para evitar que el objeto producido por Rails no escape del atributo x-data, lo que normalmente pasa por una comilla doble.

💡 f.resource.attributes nos da acceso a todos los atributos del modelo. En vez de usarlo, también puedes declarar los valores a mano, siempre recordando que el resultado final debe ser un objeto válido de javascript.

💡 Si un ejemplo en la guía no incluye el atributo x-data, de todas maneras se asume que fue declarado

Una vez que hemos inicializado el componente con x-data podemos empezar a usar las otras directivas de AlpineJS.

f.inputs 'x-data':  CGI.escapeHTML("{...#{f.resource.attributes.to_json}}") do
  f.input :name, input_html: { 'x-model': 'name' }
end

Formatear el valor de un campo al escribir

Casos de uso: Números de teléfono, valores en formato de moneda local, números de cédula de identidad, etc.

Para empezar tenemos que agregar la función para formatear en el mismo archivo donde inicializamos Alpine y exponer la función a la página en ActiveAdmin.

window.Alpine = Alpine;
Alpine.start();

window.formatters = {
  // Formats a number to currency
  currency: new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP' }),
  // Removes everything that's not a number from a string
  numberCleaner(value) {
    return value.replaceAll(/\\D/g, '');
  },
};

Después, dentro de nuestro recurso en ActiveAdmin, podemos usar x-on-input (o @input) para formatear el valor cada vez que escribimos un valor en el campo de texto.

💡 Para este ejemplo en especifico, tenemos que limpiar el número (para obtener 1000 en vez de 1.000), asi que ejecutamos numberCleaner antes de `format.

f.input :amount, input_html: {
  'x-model': 'amount',
  'x-on:input': 'amount = formatters.currency.format(formatters.numberCleaner($event.target.value));'
}

Si actualizamos la página el monto no estará formateado porque el formateador solo se ejecuta con el evento input. Para arreglar esto tenemos que editar el valor iniciar en x-data.


f.inputs 'x-data': CGI.escapeHTML("{ amount: formatters.currency.format('#{f.resource.attributes['amount']}')}") do

  f.input :amount, input_html:
    'x-model': 'amount',
    'x-on:input': 'amount = formatters.currency.format(formatters.numberCleaner($event.target.value));
    '

En el caso que el atributo del modelo sea un integer en la base de datos, no podremos guardar el valor formateado. Para lograr guardar el valor actualizado pero mantener el formato al editarlo tenemos que agregar un par de cosas para tener dos campos: el campo formateador y el campo "real" que se guarda en la base de datos.

En el modelo agregamos:

class FormatFieldExample < ApplicationRecord
  attr_accessor :active_admin_amount
end

💡 attr_accessor es necesario ya que ActiveAdmin no permite mostrar valores que no existen como campos en su formulario.

Después, en el archivo de ActiveAdmin, agregamos amount directamente y además agregamos el campo active_admin_amount formateado a x-data.

f.inputs 'x-data': CGI.escapeHTML("{
    amount: #{f.resource.attributes['amount']},
    active_admin_amount: formatters.currency.format('#{f.resource.attributes['amount']}')},
  }") do

Reemplazamos el campo amount por active_admin_amount y en el evento x-on:input agregamos que también se actualice amount.

  f.input :active_admin_amount, input_html:
    'x-model': 'active_admin_amount',
    'x-on:input': '
      active_admin_amount = formatters.currency.format(formatters.numberCleaner($event.target.value));
      amount = formatters.numberCleaner(active_admin_amount);
    '

Finalmente agregamos un campo oculto con el amount real para que se guarde en la base de datos como número.

f.input :amount, as: :hidden, input_html: {
  'x-bind:value': 'amount'
}

ejemplo

Validar un campo

Casos de uso: Prevenir que un formulario se pueda guardar si un valor no es válido, mostrar cuando un campo es obligatorio o tiene un valor inválido.

Al igual que en el ejemplo anterior, tenemos que agregar la función de validación a la variable window para que esté disponible en la página de ActiveAdmin.

import { rutValidate } from 'rut-helpers';

window.validators = {
  // Formats a value to the standard RUT format.
  rut: rutValidate
};

En este ejemplo queremos cambiar la clase CSS del campo cuando el valor no es válido. Para esto necesitamos usar x-bind:class(o :class) para que la clase error sea agregada dinámicamente cuando validators.rut(rut) sea false:

f.input :rut, input_html: {
  'x-model': 'rut',
  'x-bind:class': '{error: !validators.rut(rut)}'
}

Si también queremos desactivar el botón para guardar, podemos editar la acción submit para agregar el atributo disabled. x-bind:disabled (o :disabled) automáticamente agregan el atributo cuando la validación falla.

f.actions do
  f.action :submit, button_html: { 'x-bind:disabled': "!validators.rut(rut)" }
end

ejemplo

Esconder y mostrar un campo

Para poder mostrar y esconder un campo podemos usar la directiva x-show.

Primero necesitamos un campo con x-model para tener acceso a su valor.

f.input :has_description, input_html: {
  'x-model': 'has_description'
}

Después agregamos la directiva x-show al campo que queremos mostrar o esconder dependiendo del valor que tenga el campo has_description.

f.input :description, wrapper_html: {
  'x-show': 'has_description'
}

💡 Tenemos que usar wrapper_html en vez de input_html para esconder toda la fila, tanto el label como el input.

ejemplo

Campos Select2

ActiveAdmin Addons transforma todos los select para que usen Select2, para facilitar el uso de colección grandes o tags. Sin embargo, AlpineJS no tiene idea qué hacer con los elementos de Select2 y viceversa.

Para que funcionen los elementos select con atributos x-model tenemos que instalar active-admin-alpine-fixes.

yarn add active-admin-alpine-fixes

Después tenemos que agregar el fix a la variable window para que esté disponible en la página de ActiveAdmin

import { select2 } from 'active-admin-alpine-fixes';

window.alpineFixes = { select2 };

Finalmente agregamos el fix a nuestro component agregando la directiva x-init para que ejecute el fix apenas el componente sea evaluado por el navegador.

f.inputs 'x-init': 'alpineFixes.select2.init', 'x-data':  CGI.escapeHTML("{...#{f.resource.attributes.to_json}}") do
  f.input :choices, input_html: { 'x-model': 'choices' }
end

ejemplo

Has Many

ActiveAdmin nos permite tener formularios anidados cuando un recurso tiene un has_many. Pero cuando hacemos click en el botón para crear un recurso nuevo en este formulario anidado ActiveAdmin usa jQuery para crear los campos nuevos y AlpineJS se confunde.

Para que funcionen tenemos que instalar active-admin-alpine-fixes.

yarn add active-admin-alpine-fixes

Después tenemos que agregar el fix a la variable window para que esté disponible en la página de ActiveAdmin

import { hasMany } from 'active-admin-alpine-fixes';

window.alpineFixes = { hasMany };

En nuestro componente tenemos que agregar el fix a la directiva x-init y en nuestro x-data tenemos que agregar explícitamente el recurso anidado. Dentro del has_many tenemos que usar x-model con el índice que nos da para que AlpineJS sepa a qué campo corresponde qué elemento en el arreglo.

f.inputs 'x-init': 'alpineFixes.hasMany.init',
          'x-data': CGI.escapeHTML("{
            ...#{f.resource.attributes.to_json},
            children: #{f.resource.children.to_json}
          }") do
  f.has_many :children, allow_destroy: true do |co, i|
    # has_many index starts with 1 while javascript's starts with 0 so we subtract one
    co.input :name, input_html: {
      'x-model': "children[#{i - 1}].name"
    }
  end
end

ejemplo

Formularios Complejos (Solo AlpineJS 3 - Alpine.data)

Si nuestro formulario es muy complejo o tiene funcionalidad que puede ser fácilmente re-usada, podemos usar Alpine.data en nuestro javascript para declarar un objeto que puede ser usado en nuestro formulario sin tener que usar window. En otras palabras, podemos tener un archivo JS separado con todo lo que necesitamos.

// activeadmin/complex_example.js

export default (attributes = {}) => {
  function init() {
    // We need to pass the Alpine context (this) so it can find the element
    select2.init.bind(this)();
  }

  const currencyFormat = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP' });

  function numberCleaner(value) {
    return value.replaceAll(/\\D/g, '');
  }


  // We return an object that will be available inside our component
  return { ...attributes, init, currencyFormat, numberCleaner };
};
import complexExample from './activeadmin/complex_example';

Alpine.data('complexExample', complexExample);
Alpine.start();

Una vez hecho lo anterior, podemos usar complexExample en nuestro x-data, el que recibe los atributos que necesitamos para inicializar el objeto que usa Alpine.

💡 Como complexExample todavía no se ejecuta, currencyFormat todavía no está disponible para ser usado en x-data. Puedes agregar la función a la variable window, procesar los atributos dentro de complexExample o, como en este caso, usar Ruby para lograr el mismo resultado.

form do |f|
  f.inputs 'x-data': "complexExample(#{CGI.escapeHTML("{
      ...#{f.resource.attributes.to_json},
      active_admin_amount: '#{number_to_currency(f.resource.attributes['amount'])}'
    }")})" do
    f.input :name

    f.input :active_admin_amount, input_html: {
      'x-model': 'active_admin_amount',
      # We can use currencyFormat and numberCleaner directly since the are available inside
      # the data object returned by the complexExample function.
      'x-on:input': '
        active_admin_amount = currencyFormat.format(numberCleaner($event.target.value));
        amount = numberCleaner(active_admin_amount);
      '
    }

    f.input :choices, input_html: { 'x-model': 'choices' }

    f.input :amount, as: :hidden, input_html: {
      'x-bind:value': 'amount'
    }

    f.actions do
      f.action :submit
    end
  end
end

ejemplo

Última actualización