Las cmsTables (tablas del CMS cuyo contenido MUESTRA un modulo) ya se definen desde Forge y desde el CMS legacy; faltaba que el agente las entendiera. - update_module_metadata acepta cmsTables. El endpoint del server ya la validaba, asi que solo habia que declararla en el schema Zod. - get_module_config_vars la devuelve: sale gratis porque el handler ya resolvia el schema del modulo para uploadFields/varsMeta. - Docs (01, 03, 09) con el matiz que mas se puede confundir: `tables` es donde viven los VALORES de las vars (siempre builder_custom, lo pone el compilador) y `cmsTables` es que contenido MUESTRA el modulo. Para el agente lo util no es solo escribirlas: al recibirlas sabe donde esta de verdad el contenido visible. Si le piden cambiar lo que muestra un listado de noticias, los registros estan en esa tabla, no en las vars del modulo. Se documenta ademas que la metadata del builder.json es la UNICA parte editable del fichero (y solo con esta tool): el resto lo regenera el compilador en cada compilacion.
16 KiB
title, tags, load_priority, load_when, summary
| title | tags | load_priority | load_when | summary | |||||
|---|---|---|---|---|---|---|---|---|---|
| Campos editables del builder |
|
80 |
|
Atributos data-field-* (textfield, headfield, link, upload, list, multiv2, checkbox), agrupar campos en pestañas con data-field-group, c-if/c-for/c-class, c-form, componentes built-in del builder Acai. |
Builder Fields — Campos editables del index-base.tpl
Este documento define los campos editables que el usuario rellena desde el panel del builder de Acai. Cubre el atributo data-field-type con todos sus tipos (textfield, headfield, textbox, wysiwyg, link, upload, uploadMulti, list, multiv2, checkbox, colorpicker), la regla data-field-label → nombre de variable, los atributos Acai (c-if, c-else, c-for, c-class, c-hidden, c-required), el tag <set>, la inclusión de módulos, los formularios c-form y los componentes built-in. Léelo antes de crear o modificar cualquier index-base.tpl.
Reglas de nomenclatura de variables
El atributo data-field-label se convierte automáticamente en el nombre de variable Twig: se ponen minúsculas y se eliminan espacios y caracteres especiales.
| Label | Variable resultante |
|---|---|
Categoría Noticia |
categoranoticia |
Color Principal |
colorprincipal |
Título Producto |
ttuloproducto |
Reglas obligatorias:
- Todo elemento con
data-field-typeDEBE incluir tambiéndata-field-label. - Sin
data-field-label, el builder genera variables temporales o incorrectas y el módulo queda mal configurado. - Usa labels descriptivos y estables; no dejes labels vacíos ni genéricos como "Campo" o "Texto".
- En
index-base.tplevita clases Tailwind con valores arbitrarios (text-[44px],font-['Cinzel'],leading-[1.1]) — pueden romper el parseo. Muévelas astyle.css.
Tipos de campo (data-field-type)
| Tipo | Elemento HTML | Devuelve |
|---|---|---|
textfield |
<p> |
String |
headfield |
<h1>–<h6> |
String + variable extra _tag con la etiqueta elegida |
textbox |
<div> |
String multilínea |
wysiwyg |
<div class="wysiwyg"> |
String HTML |
link |
<a> |
URL string (ya incluye barras) |
upload |
<img> |
Array de {urlPath, info1, info2, info3, info4} |
uploadMulti |
<li> |
Itera sobre archivos subidos |
list (fijo) |
<div data-list-options="..."> |
Valor seleccionado |
list (tabla) |
<div data-list-table="..."> |
num del registro |
multiv2 |
<li> wrapper |
Array de objetos repetibles |
checkbox |
<div> o <input> |
1 o 0 (número) |
colorpicker |
<div> |
Hex color string |
textfield
<p data-field-type="textfield" data-field-label="Título">
Elemento editable
</p>
headfield
Genera 2 variables: la estándar y _tag con la etiqueta elegida (h1…h6).
<p data-field-type="headfield" data-field-label="Titulo" >
Título de la sección
</p>
textbox
<div data-field-type="textbox" data-field-label="Descripción">
Texto largo editable
</div>
wysiwyg
Editor de texto enriquecido. Acceder con | raw para no escapar el HTML.
<div class="wysiwyg" data-field-type="wysiwyg" data-field-label="Contenido Enriquecido">
<p>Texto con <strong>estilos</strong> editables</p>
</div>
link
El campo enlace de Acai ya incluye las barras necesarias — nunca añadas barras extra.
Genera 2 variables: la estándar y _anchor con el anchor del enlace.
<a data-field-type="link" data-field-label="Enlace">
Haz clic aquí
</a>
upload
Devuelve un array. Acceso en Twig: {{ imagen[0].urlPath }}.
<div class="p-1/6 relative">
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
data-field-type="upload"
data-field-label="Imagen Principal"
data-lazy="true"
data-field-info1="titulo"
data-field-width="1400"
alt="">
</div>
Atributos disponibles:
data-lazy="true"— carga perezosadata-field-width="1400"— ancho máximo sugeridodata-field-info1…data-field-info5— labels de los campos de información por imagen
Los data-field-infoN (hasta 5) definen los labels que el builder muestra como campos editables en cada imagen subida (van a infoLabels en el builder.json). Sus valores se leen luego como info1…info4 dentro del array del var.
<img data-field-type="upload"
data-field-label="Imagen Principal"
data-field-info1="Texto alternativo"
data-field-info2="Pie de foto"
data-field-width="1400"
alt="">
<img src="{{ imagenprincipal[0].urlPath }}" alt="{{ imagenprincipal[0].info1 }}">
<figcaption>{{ imagenprincipal[0].info2 }}</figcaption>
Los data-field-infoN funcionan igual en uploadMulti.
uploadMulti
Itera sobre todas las imágenes subidas. Variable iteradora: uploadMulti.
<li data-field-type="uploadMulti" data-field-label="Galería" data-field-info1="titulo">
<div class="relative min-h-screen">
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
data-src="{{ uploadMulti.urlPath | imagec(2100) }}"
alt="{{ uploadMulti.info1 }}">
</div>
</li>
list (opciones fijas)
<div data-field-type="list"
data-field-label="Color Producto"
data-list-options="Rojo,Azul,|Verde,3|Amarillo">
</div>
Formato data-list-options:
opcion1,opcion2→ la opción es etiqueta y valor a la vez|valor3,etiqueta3→ separa valor de etiqueta con|
list (tabla)
Selecciona un registro de otra tabla. Devuelve el num.
<div data-field-type="list"
data-field-label="Noticia Destacada"
data-list-table="noticias"
data-list-value="num"
data-list-label="titulo">
{{ record.titulo }}
</div>
data-list-table— nombre de tabla sin prefijocms_data-list-value— campo a usar como valor (normalmentenum)data-list-label— campo a mostrar como label
multiv2 — Campos repetibles
Crea grupos de campos repetibles. La variable resultante es un array de objetos.
<ul>
<li data-field-type="multiv2" data-field-label="Productos">
<div data-field-type="textfield" data-field-label="Nombre">
Nombre del producto
</div>
<div data-field-type="textbox" data-field-label="Descripción">
Descripción del producto
</div>
<div class="p-1/6 relative">
<img class="absolute top-0 left-0 w-full h-full object-cover lazyload"
data-field-type="upload"
data-field-label="Imagen"
data-lazy="true"
data-field-width="800"
alt="">
</div>
</li>
</ul>
Uso en Twig:
{% for record in productos %}
<div class="producto">
<h3>{{ record.nombre }}</h3>
<p>{{ record.descripcion }}</p>
<img src="{{ record.imagen[0].urlPath }}" alt="">
</div>
{% endfor %}
checkbox
Devuelve 1 o 0 (número), nunca true/false.
colorpicker
Devuelve un string hexadecimal (#ff0000). Almacenado en config-vars (no en builder_custom).
Agrupar campos en pestañas (data-field-group)
Los campos de un módulo se reparten en pestañas dentro del panel de configuración añadiendo data-field-group="Nombre del grupo" al mismo elemento que ya lleva data-field-type.
Mecanismo: cualquier atributo data-field-* extra —aparte de data-field-type, data-field-label y data-field-value— se recoge en el objeto customDataField del var, recortando el prefijo data-field-. Es decir, data-field-group="Estilos" acaba como customDataField: { group: "Estilos" } en el builder.json.
Comportamiento del panel:
- Los vars se agrupan por
customDataField.groupy se muestran en pestañas laterales. - Los vars sin
data-field-groupcaen en la pestaña Principal. - El orden de las pestañas es el de primera aparición de cada grupo en el template.
- Con un solo grupo no se muestran pestañas.
- Funciona igual en vars de primer nivel y dentro de
multiv2.
<div data-field-type="textfield" data-field-label="Título" data-field-group="Contenido">
Título de la sección
</div>
<div data-field-type="textbox" data-field-label="Descripción" data-field-group="Contenido">
Texto de apoyo
</div>
<div c-hidden="true">
<div data-field-type="colorpicker" data-field-label="Color de fondo" data-field-group="Estilos"></div>
<div data-field-type="list"
data-field-label="Alineación"
data-list-options="|Izquierda,1|Centro,2|Derecha"
data-field-group="Estilos"></div>
</div>
Recomendación de uso:
- Agrupa cuando el módulo pase de ~6-8 vars; por debajo, una sola lista se lee mejor.
- Reparto habitual: Contenido (textos e imágenes), Estilos (colores, alineación, bordes), Ajustes (opciones de comportamiento, límites, enlaces).
- Mantén los nombres de grupo consistentes entre módulos y en español. Son literales:
Estilosyestilosgeneran dos pestañas distintas.
Atributos Acai
c-if — Renderizado condicional
Usa = (un solo igual) para comparaciones, no ==.
<div c-if="subtitle">{{ subtitle }}</div>
<div c-if="layout = 'grid'">Grid layout</div>
c-else
Va inmediatamente después del elemento c-if.
<div c-if="image">
<img src="{{ image[0].urlPath }}">
</div>
<div c-else>
<p>No image available</p>
</div>
c-for — Iteración sobre array
<div c-for="item in record.features">
<h3>{{ item.title }}</h3>
</div>
c-for — Iteración sobre tabla de BD
<ul>
<li c-for="producto in productos"
c-where="'visible=1'"
c-order="'num desc'"
c-limit="10">
{{ producto.title }}
</li>
</ul>
Parámetros opcionales: c-where (string SQL), c-order (string de orden), c-limit (entero).
Equivalente Twig:
{% for producto in 'productos' | get('visible=1','num desc',10) %}
<li>{{ producto.title }}</li>
{% endfor %}
Variables del loop: loop.index (1-based), loop.index is odd, loop.index is even.
c-class — Clases CSS condicionales
<!-- Simple -->
<div c-class="{ 'text-center': alineacion == '1', 'text-right': alineacion == '2' }">
<!-- Múltiples condiciones -->
<div c-class="{
'flex-row-reverse': orden == '1',
'cursor-pointer click-a-child': record.enlace_anchor,
'rounded-xl': radioborde == '4'
}">
<!-- Con loop -->
<div c-class="{
'md:order-1': loop.index is odd,
'md:pl-6': loop.index is even
}">
<!-- Combinado con clases estáticas -->
<div class="flex items-center" c-class="{ 'justify-center': centrado }">
c-hidden — Variables ocultas
Elemento que NO se renderiza pero SÍ declara variables builder. Patrón típico para colores y opciones de configuración.
<div c-hidden="true">
<input data-field-type="textfield" data-field-label="Color de fondo" value="">
<div data-field-type="list"
data-field-label="Color titulo resaltado"
data-list-options="|Main color,1|Main color light,2|Main color dark"></div>
</div>
c-required — Validación condicional
<input type="text" name="telefono"
c-required="'2' not in camposquitar"
placeholder="Teléfono">
Tag <set> — Definir variables
<!-- Obtener configuración de la BD -->
<set :tienda="'configuracion_tienda' | get('num != 0')[0]"></set>
<!-- Construir URLs dinámicas -->
<set :logo="tienda.logo.0.urlPath
? 'https://' ~ server.HTTP_HOST ~ tienda.logo.0.urlPath
: 'https://' ~ server.HTTP_HOST ~ '/template/estandar/images/logo.png'">
</set>
<!-- Twig set para expresiones complejas -->
{% set gracias = 'apartados' | get('num = 20').0 %}
Incluir módulos
Para incluir un módulo dentro de otro módulo o dentro de una sección general, usa el moduleId como etiqueta HTML:
<module_id :param1="value1" :param2="'string value'"></module_id>
Ejemplos:
<header_menu :showLogo="true" :menuItems="items"></header_menu>
<product_card :product="selectedProduct" :showPrice="true"></product_card>
El módulo hijo recibe los parámetros como variables en su contexto.
Formularios — c-form
Maneja automáticamente validación, almacenamiento en BD y envío de emails.
<c-form
class="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow"
tableName="'solicitudes'"
mailRecord="['correos', 'CONTACTO']"
sendTo="'contacto@empresa.com'"
sendToClient="'email'"
captcha="true"
honeypot="true"
messageOK="'¡Gracias! Te contactaremos pronto'"
messageKO="'Por favor, completa todos los campos'"
redirect="'/gracias'"
attachFiles="true">
<input name="nombre" type="text" required class="w-full p-2 border rounded">
<input name="email" type="email" required class="w-full p-2 border rounded">
<textarea name="mensaje" required class="w-full p-2 border rounded" rows="5"></textarea>
<label class="flex items-center">
<input name="acepto_politica" type="checkbox" class="mr-2" required>
<span>Acepto la política de privacidad</span>
</label>
<button type="submit" class="bg-teal-500 text-white px-6 py-2 rounded">Enviar</button>
<captcha/>
</c-form>
Atributos c-form
| Atributo | Descripción |
|---|---|
tableName="'tabla'" |
Tabla destino (sin cms_) |
mailRecord="['correos', 'ID']" |
Template de email en tabla correos |
sendTo="'email@dominio.com'" |
Destinatarios (separados por coma) |
sendToClient="'campo_email'" |
Campo del formulario con email del cliente para auto-reply |
captcha="true" |
Activa Google reCAPTCHA |
honeypot="true" |
Campo oculto anti-spam |
messageOK="'texto'" |
Mensaje al enviar correctamente |
messageKO="'texto'" |
Mensaje al fallar validación |
redirect="'/ruta/'" |
Redirección tras envío correcto |
attachFiles="true" |
Adjuntar archivos al email |
showImages="true" |
Mostrar thumbnails en email |
emailMode="'twig'" |
Email en formato Twig |
header="'<div>...'" |
HTML cabecera del email |
footer="'<div>...'" |
HTML footer del email |
styles="'body { ... }'" |
CSS del email |
Para formularios estándar (contacto, postulación), prefiere c-form antes que crear lógica custom de POST/hook. Solo crea una tabla propia si necesitas gestionar esos registros desde el admin.
Componentes built-in
Carousel — c-tns-wrapper
<div class="c-tns-wrapper"
data-responsive='{"0":1,"768":2,"1024":3}'
data-speed="400"
data-nav="true"
data-autoplay-timeout="3000">
<div c-for="slide in record.slides">
<img src="{{ slide.image[0].urlPath }}">
</div>
</div>
Lightbox
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
<img src="{{ image[0].urlPath | imagec(400) }}">
</a>
Breadcrumb
<breadCrumb/>
<breadCrumb class="bg-gray-200 p-3 rounded" c-prevlinks="null"></breadCrumb>
Animate On Scroll (AOS)
<div data-aos="fade-up" data-aos-delay="200" data-aos-duration="800">
Contenido animado
</div>
Valores comunes: fade-up, fade-down, fade-left, fade-right, zoom-in, zoom-in-up, fade-up-right, fade-up-left. Tras cambios dinámicos en JS: AOS.refresh().
Lazy loading
<img class="lazyload" data-src="{{ image[0].urlPath }}">
<!-- O en builder field: -->
<img data-field-type="upload" data-field-label="Imagen" data-lazy="true">
Reglas críticas
- Todo
data-field-typeexigedata-field-label. data-field-labelse transforma a variable: minúsculas, sin espacios ni caracteres especiales.- Campos
uploadretornan arrays — usaimagen[0].urlPath, nuncaimagen. - Variables dentro de
multiv2son propiedades del objeto iterado (record.nombre). c-ifusa=(un igual).{% if %}usa==(doble igual).c-forcon tabla: nombre sin prefijocms_.enlaceya incluye las barras — no añadas slashes extra.- Checkbox guarda
1o0(número), nuncatrue/false. - Evita Tailwind arbitrary-value en
index-base.tpl— muévelos astyle.css. script.jsystyle.cssson estáticos: NO uses sintaxis Twig dentro. Pasa valores dinámicos víadata-*.- Si el módulo lista registros de una tabla del CMS (
c-forsobre ella, o suhook.phpla consulta), declara esa tabla conupdate_module_metadata({ cmsTables: [...] }). No es undata-field-*: no se deduce del HTML, hay que declararla. Ver03-modules-and-sections.md.