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.
504 lines
16 KiB
Markdown
504 lines
16 KiB
Markdown
---
|
||
title: "Campos editables del builder"
|
||
tags: [builder, twig, html, modules]
|
||
load_priority: 80
|
||
load_when: [always]
|
||
summary: "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-type` DEBE incluir también `data-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.tpl` evita clases Tailwind con valores arbitrarios (`text-[44px]`, `font-['Cinzel']`, `leading-[1.1]`) — pueden romper el parseo. Muévelas a `style.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
|
||
|
||
```html
|
||
<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).
|
||
|
||
```html
|
||
<p data-field-type="headfield" data-field-label="Titulo" >
|
||
Título de la sección
|
||
</p>
|
||
```
|
||
|
||
### textbox
|
||
|
||
```html
|
||
<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.
|
||
|
||
```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.
|
||
|
||
```html
|
||
<a data-field-type="link" data-field-label="Enlace">
|
||
Haz clic aquí
|
||
</a>
|
||
```
|
||
|
||
### upload
|
||
|
||
Devuelve un array. Acceso en Twig: `{{ imagen[0].urlPath }}`.
|
||
|
||
```html
|
||
<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 perezosa
|
||
- `data-field-width="1400"` — ancho máximo sugerido
|
||
- `data-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.
|
||
|
||
```html
|
||
<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="">
|
||
```
|
||
|
||
```twig
|
||
<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`.
|
||
|
||
```html
|
||
<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)
|
||
|
||
```html
|
||
<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`.
|
||
|
||
```html
|
||
<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 prefijo `cms_`**
|
||
- `data-list-value` — campo a usar como valor (normalmente `num`)
|
||
- `data-list-label` — campo a mostrar como label
|
||
|
||
### multiv2 — Campos repetibles
|
||
|
||
Crea grupos de campos repetibles. La variable resultante es un array de objetos.
|
||
|
||
```html
|
||
<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:
|
||
|
||
```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.group` y se muestran en pestañas laterales.
|
||
- Los vars sin `data-field-group` caen 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`.
|
||
|
||
```html
|
||
<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: `Estilos` y `estilos` generan dos pestañas distintas.
|
||
|
||
## Atributos Acai
|
||
|
||
### `c-if` — Renderizado condicional
|
||
|
||
Usa `=` (un solo igual) para comparaciones, no `==`.
|
||
|
||
```html
|
||
<div c-if="subtitle">{{ subtitle }}</div>
|
||
<div c-if="layout = 'grid'">Grid layout</div>
|
||
```
|
||
|
||
### `c-else`
|
||
|
||
Va inmediatamente después del elemento `c-if`.
|
||
|
||
```html
|
||
<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
|
||
|
||
```html
|
||
<div c-for="item in record.features">
|
||
<h3>{{ item.title }}</h3>
|
||
</div>
|
||
```
|
||
|
||
### `c-for` — Iteración sobre tabla de BD
|
||
|
||
```html
|
||
<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:
|
||
```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
|
||
|
||
```html
|
||
<!-- 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.
|
||
|
||
```html
|
||
<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
|
||
|
||
```html
|
||
<input type="text" name="telefono"
|
||
c-required="'2' not in camposquitar"
|
||
placeholder="Teléfono">
|
||
```
|
||
|
||
## Tag `<set>` — Definir variables
|
||
|
||
```html
|
||
<!-- 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:
|
||
|
||
```html
|
||
<module_id :param1="value1" :param2="'string value'"></module_id>
|
||
```
|
||
|
||
Ejemplos:
|
||
```html
|
||
<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.
|
||
|
||
```html
|
||
<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`
|
||
|
||
```html
|
||
<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
|
||
|
||
```html
|
||
<a href="{{ image[0].urlPath }}" class="glightbox" data-gallery="gallery1">
|
||
<img src="{{ image[0].urlPath | imagec(400) }}">
|
||
</a>
|
||
```
|
||
|
||
### Breadcrumb
|
||
|
||
```html
|
||
<breadCrumb/>
|
||
<breadCrumb class="bg-gray-200 p-3 rounded" c-prevlinks="null"></breadCrumb>
|
||
```
|
||
|
||
### Animate On Scroll (AOS)
|
||
|
||
```html
|
||
<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
|
||
|
||
```html
|
||
<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
|
||
|
||
1. Todo `data-field-type` exige `data-field-label`.
|
||
2. `data-field-label` se transforma a variable: minúsculas, sin espacios ni caracteres especiales.
|
||
3. Campos `upload` retornan **arrays** — usa `imagen[0].urlPath`, nunca `imagen`.
|
||
4. Variables dentro de `multiv2` son propiedades del objeto iterado (`record.nombre`).
|
||
5. `c-if` usa `=` (un igual). `{% if %}` usa `==` (doble igual).
|
||
6. `c-for` con tabla: nombre **sin prefijo `cms_`**.
|
||
7. `enlace` ya incluye las barras — no añadas slashes extra.
|
||
8. Checkbox guarda `1` o `0` (número), nunca `true`/`false`.
|
||
9. Evita Tailwind arbitrary-value en `index-base.tpl` — muévelos a `style.css`.
|
||
10. `script.js` y `style.css` son estáticos: NO uses sintaxis Twig dentro. Pasa valores dinámicos vía `data-*`.
|
||
11. Si el módulo lista registros de una tabla del CMS (`c-for` sobre ella, o su `hook.php` la consulta), declara esa tabla con `update_module_metadata({ cmsTables: [...] })`. No es un `data-field-*`: no se deduce del HTML, hay que declararla. Ver `03-modules-and-sections.md`.
|