CreaRack-SL

Alpine CSP build · :class con getter vs objeto literal inline

Contexto

CreaRack Pro usa Alpine.js en su variante CSP build (Content Security Policy), que impone restricciones sobre cómo se evalúan expresiones en tiempo de ejecución. Esto crea diferencias de comportamiento respecto a la build estándar de Alpine que no siempre son obvias en desarrollo local pero sí afloran en producción.

El footgun

Síntoma

El binding :class con un objeto literal inline:

<div :class="{ active: open, pinned: tutorMode }"></div>

aplica la clase active correctamente (caso feliz de open), pero no aplica de forma fiable la clase pinned cuando tutorMode cambia después del primer render. El panel aparece fijo en local/staging pero se cierra en PROD tras un hard refresh.

Causa raíz confirmada (s55, 2026-05-11)

El Alpine CSP build evalúa los objetos literales inline de forma diferente a los getters que devuelven objetos. Con un literal inline, la reactividad sobre propiedades que cambian post-render no está garantizada. Con un getter, Alpine recalcula el objeto completo en cada cambio de dependencia.

Esto se manifestó en el Help Widget / Modo IT Tutor: el panel no quedaba “pinned” (fijo) en producción aunque tutorMode=true, lo que obligaba al usuario a hacer click dentro del panel para evitar que se cerrara.

Fix: extraer a getter

// alpine-components.js
get helpPanelClasses() {
    return { active: this.open, pinned: this.tutorMode };
},
<!-- templates/base.html -->
<div class="help-overlay" :class="helpPanelClasses" @click="!tutorMode && close()"></div>
<div class="help-panel"   :class="helpPanelClasses" id="help-panel"></div>

El getter es recalculado por Alpine cada vez que cualquiera de sus dependencias (this.open, this.tutorMode) cambia, independientemente de si el cambio ocurre en el primer render o después.

Patrón obligatorio en este proyecto

Regla: en Alpine CSP build, nunca uses objetos literales inline en :class cuando alguna de sus propiedades puede cambiar tras el primer render. Extrae siempre a un getter en el componente Alpine.

Incorrecto (CSP unreliable)Correcto (CSP-safe)
:class="{ active: open, pinned: tutorMode }":class="helpPanelClasses" + getter en JS
:class="{ visible: isLoading }" (si isLoading cambia post-render):class="loadingClasses" + getter

Este patrón ya estaba implícito en otras partes del helpWidget (ver tutorButtonClass, tutorButtonTitle) pero no se había aplicado al overlay y panel hasta la sesión 55.

Historial de debugging (s54–s55)

CommitIntentoResultado
Pre-s55Binding por concatenación de strings (openClass + ' ' + pinnedClass)Fallo: la concatenación no garantizaba re-evaluación reactiva
2d7bfe42Objeto literal inline (:class="{ active: open, pinned: tutorMode }")Fallo en PROD pese a hard refresh — footgun CSP confirmado
3ac0675Getter helpPanelClasses + botón “Cerrar” explícito en bar TutorFix confirmado

Defensa doble: botón “Cerrar”

Como salvaguarda adicional, se añadió un botón “Cerrar” visible únicamente en modo Tutor (x-show="tutorMode") junto al toggle “IT Tutor”. Esto garantiza que el usuario siempre tiene una salida explícita e inequívoca aunque futuras regresiones en el binding vuelvan a afectar al overlay.

<button type="button"
    class="btn btn-xs btn-neutral"
    x-show="tutorMode"
    @click.stop="close()"
    title="Close IT Tutor panel">Cerrar</button>

Véase también

  • [[entity—workspace—component—search-inline]]
  • [[crearack—conceptos—rack-editor-conceptos]]