Add complete Acai development documentation
- CLAUDE.md: expanded with critical rules, hook syntax, DB conventions, web-base endpoint - docs/modular-system.md: modules, general sections, global vars, multiv2 - docs/builder-fields.md: all field types, c-if/c-for/c-class, c-form, built-in components - docs/twig-filters.md: get, hook, module, queryDB, imagec, translate, raw, etc. - docs/hooks-and-api.md: PHP hooks, CmsApi CRUD, table schemas, field formats - docs/css-js-conventions.md: Tailwind, BEM, CSS vars, Vue 3 integration, CmsApi JS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,32 +1,213 @@
|
||||
# Builder Fields & Attributes
|
||||
# Builder Fields & Acai Attributes
|
||||
|
||||
## Field Types (`data-field-type`)
|
||||
|
||||
The builder uses `data-field-type` attributes to define editable areas in module templates.
|
||||
The builder uses `data-field-type` attributes on HTML elements to define editable areas.
|
||||
|
||||
<!-- TODO: Document available field types (text, image, richtext, link, repeater, etc.) -->
|
||||
| Type | Description | Returns |
|
||||
|------|-------------|---------|
|
||||
| `textfield` | Single line text | String |
|
||||
| `headfield` | Heading text (generates `_tag` variable for semantic tag: h1-h6) | String |
|
||||
| `textbox` | Multi-line text | String |
|
||||
| `wysiwyg` | Rich text editor (HTML output) | HTML string |
|
||||
| `link` | URL field (already includes slashes) | String |
|
||||
| `upload` | Single image/file | Array: `[0].urlPath`, `[0].info1` (alt), `[0].info2-4` |
|
||||
| `uploadMulti` | Multiple images | Iterable: `item.urlPath` |
|
||||
| `list` | Dropdown (fixed options or from table) | String or foreign key num |
|
||||
| `multiv2` | Repeatable group of fields | Array of objects |
|
||||
|
||||
### headfield Example
|
||||
|
||||
```html
|
||||
<{{ title_tag | default('h2') }} data-field-type="headfield" class="text-3xl font-bold">
|
||||
Section Title
|
||||
</{{ title_tag | default('h2') }}>
|
||||
```
|
||||
|
||||
### upload Example
|
||||
|
||||
```html
|
||||
<!-- Single image -->
|
||||
<img data-field-type="upload"
|
||||
src="{{ image[0].urlPath }}"
|
||||
alt="{{ image[0].info1 }}"
|
||||
class="w-full rounded" />
|
||||
|
||||
<!-- Multiple images -->
|
||||
<div c-for="photo in gallery">
|
||||
<img src="{{ photo.urlPath }}" alt="{{ photo.info1 }}" />
|
||||
</div>
|
||||
```
|
||||
|
||||
### list Example
|
||||
|
||||
```html
|
||||
<!-- Fixed options -->
|
||||
<select data-field-type="list" data-list-options="option1,option2,option3">
|
||||
<option>option1</option>
|
||||
</select>
|
||||
|
||||
<!-- From table -->
|
||||
<select data-field-type="list" data-list-table="categories" data-list-field="name">
|
||||
<option>Category</option>
|
||||
</select>
|
||||
```
|
||||
|
||||
|
||||
## Conditional Attributes
|
||||
## Acai Attributes
|
||||
|
||||
### `c-if`
|
||||
### `c-if` — Conditional Rendering
|
||||
|
||||
<!-- TODO: Document c-if syntax and usage for conditional rendering -->
|
||||
```html
|
||||
<!-- Boolean check -->
|
||||
<div c-if="showBanner">Banner content</div>
|
||||
|
||||
### `c-for`
|
||||
<!-- Equality check (uses = not ==) -->
|
||||
<div c-if="layout = 'grid'">Grid layout</div>
|
||||
|
||||
<!-- TODO: Document c-for syntax for repeating elements -->
|
||||
<!-- Variable exists / is not empty -->
|
||||
<div c-if="subtitle">{{ subtitle }}</div>
|
||||
```
|
||||
|
||||
### `c-class`
|
||||
### `c-else`
|
||||
|
||||
<!-- TODO: Document c-class for conditional CSS classes -->
|
||||
Must immediately follow the `c-if` element:
|
||||
|
||||
```html
|
||||
<div c-if="image">
|
||||
<img src="{{ image[0].urlPath }}" />
|
||||
</div>
|
||||
<div c-else>
|
||||
<p>No image available</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-for` — Iteration
|
||||
|
||||
```html
|
||||
<!-- Over array/multiv2 -->
|
||||
<div c-for="item in record.features">
|
||||
<h3>{{ item.title }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Over database table -->
|
||||
<div c-for="product in products" c-where="active = 1" c-order="orden ASC" c-limit="6">
|
||||
<h3>{{ product.nombre }}</h3>
|
||||
</div>
|
||||
```
|
||||
|
||||
Available inside loop: `loop.index` (1-based), `loop.index is odd`, `loop.index is even`
|
||||
|
||||
### `c-class` — Dynamic CSS Classes
|
||||
|
||||
```html
|
||||
<div c-class="{ 'bg-blue-500': isActive, 'text-white': isActive, 'hidden': !showElement }">
|
||||
Content
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-hidden` — Hidden Elements
|
||||
|
||||
Element is not rendered but can declare builder variables:
|
||||
|
||||
```html
|
||||
<div c-hidden="true">
|
||||
<input data-field-type="textfield" value="default config value" />
|
||||
</div>
|
||||
```
|
||||
|
||||
### `c-required` — Conditional Required Fields
|
||||
|
||||
```html
|
||||
<input type="text" name="company" c-required="userType = 'business'" />
|
||||
```
|
||||
|
||||
|
||||
## Forms (`c-form`)
|
||||
|
||||
<!-- TODO: Document c-form usage for builder-integrated forms -->
|
||||
Complete form handling with automatic validation, storage, and email sending.
|
||||
|
||||
```html
|
||||
<form c-form
|
||||
tableName="'contacto'"
|
||||
mailRecord="['correos', 'CONTACTO']"
|
||||
sendTo="'admin@domain.com'"
|
||||
sendToClient="'email'"
|
||||
captcha="true"
|
||||
honeypot="true"
|
||||
messageOK="'Mensaje enviado correctamente'"
|
||||
messageKO="'Error al enviar el mensaje'"
|
||||
redirect="'/gracias/'"
|
||||
attachFiles="true"
|
||||
showImages="true"
|
||||
>
|
||||
<input type="text" name="nombre" required placeholder="Nombre" />
|
||||
<input type="email" name="email" required placeholder="Email" />
|
||||
<textarea name="mensaje" required placeholder="Mensaje"></textarea>
|
||||
<captcha/>
|
||||
<button type="submit">Enviar</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### c-form Attributes
|
||||
|
||||
| Attribute | Description |
|
||||
|-----------|-------------|
|
||||
| `tableName="'table'"` | Store submissions in database table |
|
||||
| `mailRecord="['correos', 'ID']"` | Email template from `correos` table |
|
||||
| `sendTo="'email@domain.com'"` | Recipient email(s), comma-separated |
|
||||
| `sendToClient="'fieldname'"` | Field containing client's email for auto-reply |
|
||||
| `captcha="true"` | Enable Google reCAPTCHA |
|
||||
| `honeypot="true"` | Anti-spam hidden field |
|
||||
| `messageOK="'text'"` | Success message |
|
||||
| `messageKO="'text'"` | Error message |
|
||||
| `redirect="'/path/'"` | Redirect after successful submit |
|
||||
| `attachFiles="true"` | Attach uploaded files to email |
|
||||
| `showImages="true"` | Show image thumbnails in email |
|
||||
|
||||
|
||||
## Builder Variables Access
|
||||
## Built-in Components
|
||||
|
||||
<!-- TODO: Document how to access and use builder variables in templates -->
|
||||
### 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/>
|
||||
```
|
||||
|
||||
### Animate On Scroll (AOS)
|
||||
|
||||
```html
|
||||
<div data-aos="fade-up" data-aos-delay="200">
|
||||
Animated content
|
||||
</div>
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```html
|
||||
<img class="lazyload" data-src="{{ image[0].urlPath }}" />
|
||||
<!-- or -->
|
||||
<img data-lazy="true" src="{{ image[0].urlPath }}" />
|
||||
```
|
||||
|
||||
101
docs/css-js-conventions.md
Normal file
101
docs/css-js-conventions.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# CSS & JavaScript Conventions
|
||||
|
||||
## CSS
|
||||
|
||||
### Tailwind First
|
||||
|
||||
Use Tailwind CSS as the primary styling method. Only use custom CSS when Tailwind is insufficient.
|
||||
|
||||
```html
|
||||
<!-- Good: Tailwind classes -->
|
||||
<div class="flex items-center gap-4 p-6 bg-white rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Title</h2>
|
||||
</div>
|
||||
|
||||
<!-- Custom CSS only when needed (animations, complex selectors, etc.) -->
|
||||
```
|
||||
|
||||
### BEM for Custom CSS
|
||||
|
||||
When custom CSS is needed, scope everything under a root class using BEM naming:
|
||||
|
||||
```css
|
||||
/* Root class in kebab-case */
|
||||
.hero-section { }
|
||||
.hero-section__title { }
|
||||
.hero-section__image { }
|
||||
.hero-section--dark { }
|
||||
```
|
||||
|
||||
Never use global classes without a module prefix.
|
||||
|
||||
### CSS Variables
|
||||
|
||||
Acai provides theme variables:
|
||||
|
||||
```css
|
||||
var(--main-color) /* Primary brand color */
|
||||
var(--main-color-light) /* Lighter variant */
|
||||
var(--main-color-dark) /* Darker variant */
|
||||
```
|
||||
|
||||
### Utility Classes (Built-in)
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
| `transition3s` | 0.3s smooth transition |
|
||||
| `click-a-child` | Makes parent clickable via child `<a>` tag |
|
||||
| `line-clamp2` / `line-clamp3` / `line-clamp5` | Text truncation with ellipsis |
|
||||
| `filter-white` | CSS filter to make images/icons white |
|
||||
| `lazyload` | Lazy loading (use with `data-src`) |
|
||||
|
||||
|
||||
## JavaScript
|
||||
|
||||
### Module Scripts (`script.js`)
|
||||
|
||||
Keep JavaScript scoped to the module. Use `section_id` for targeting:
|
||||
|
||||
```js
|
||||
// Scope to this module instance
|
||||
const section = document.getElementById('{{ section_id }}');
|
||||
if (section) {
|
||||
const buttons = section.querySelectorAll('.btn');
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### CmsApi (Client-Side)
|
||||
|
||||
```js
|
||||
// Call a hook
|
||||
CmsApi.hook('/hooks/module_id/', { action: 'getData', id: 123 }, function(response) {
|
||||
console.log(response);
|
||||
});
|
||||
```
|
||||
|
||||
### Vue 3 Integration
|
||||
|
||||
For complex interactivity, use Vue 3 via CDN with Composition API:
|
||||
|
||||
```html
|
||||
<div id="app-{{ section_id }}">
|
||||
<p>${ message }</p>
|
||||
<button @click="increment">${ count }</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp, ref } = Vue;
|
||||
createApp({
|
||||
delimiters: ['${', '}'], // Avoid conflict with Twig {{ }}
|
||||
setup() {
|
||||
const message = ref('Hello');
|
||||
const count = ref(0);
|
||||
const increment = () => count.value++;
|
||||
return { message, count, increment };
|
||||
}
|
||||
}).mount('#app-{{ section_id }}');
|
||||
</script>
|
||||
```
|
||||
|
||||
**Important:** Use `'${'` and `'}'` as Vue delimiters to avoid conflicts with Twig's `{{ }}` syntax.
|
||||
@@ -2,43 +2,141 @@
|
||||
|
||||
## Hooks
|
||||
|
||||
Hooks are PHP files in the `hooks/` directory that execute server-side logic at specific lifecycle points.
|
||||
Hooks are PHP files in the `hooks/` directory that execute server-side logic. They can also live inside a module at `template/estandar/modulos/<module-id>/hook.php`.
|
||||
|
||||
### Hook Types
|
||||
### How to Call Hooks
|
||||
|
||||
<!-- TODO: Document hook types (before_page, after_page, on_form, on_api, cron, etc.) -->
|
||||
**From Twig:**
|
||||
```twig
|
||||
{{ 'hooks/module_id/' | hook({param1: 'value1', param2: variable}) }}
|
||||
```
|
||||
|
||||
### Hook File Naming
|
||||
**From HTML (with result):**
|
||||
```html
|
||||
<hook result="myVar" endpoint="/hooks/module_id/" :param1="value1" :param2="'string'"></hook>
|
||||
<p>{{ myVar }}</p>
|
||||
```
|
||||
|
||||
<!-- TODO: Document naming conventions and how the CMS discovers hooks -->
|
||||
**From JavaScript:**
|
||||
```js
|
||||
CmsApi.hook('/hooks/module_id/', { param1: 'value1' }, function(response) {
|
||||
console.log(response);
|
||||
});
|
||||
```
|
||||
|
||||
### Hook Examples
|
||||
**From c-form:**
|
||||
Hooks are automatically triggered on form submission when configured.
|
||||
|
||||
<!-- TODO: Add practical examples of common hooks -->
|
||||
### Hook Parameters
|
||||
|
||||
Parameters are received as PHP variables:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Called with: 'hooks/my_hook/' | hook({category: 'electronics', limit: 10})
|
||||
// Available as:
|
||||
$category; // 'electronics'
|
||||
$limit; // 10
|
||||
```
|
||||
|
||||
### Hook Return Values
|
||||
|
||||
Hooks can `echo` or `return` values. When called from Twig or `<hook>` tag, the output is captured into the result variable.
|
||||
|
||||
|
||||
## CmsApi
|
||||
## CmsApi (PHP)
|
||||
|
||||
The `CmsApi` class provides methods to interact with the CMS from hooks.
|
||||
Server-side API for database operations. Available in all hooks.
|
||||
|
||||
<!-- TODO: Document main CmsApi methods (getPage, getRecord, createRecord, updateRecord, etc.) -->
|
||||
### Read Records
|
||||
|
||||
```php
|
||||
// Get all records
|
||||
$products = CmsApi::get('productos');
|
||||
|
||||
// With WHERE condition
|
||||
$active = CmsApi::get('productos', ['active' => 1]);
|
||||
|
||||
// With order and limit
|
||||
$latest = CmsApi::get('noticias', [], 'fecha DESC', 5);
|
||||
|
||||
// With operators
|
||||
$expensive = CmsApi::get('productos', ['precio' => ['>=' => 100]]);
|
||||
$search = CmsApi::get('productos', ['nombre' => ['LIKE' => '%keyword%']]);
|
||||
$inList = CmsApi::get('productos', ['categoria_num' => ['IN' => [1, 2, 3]]]);
|
||||
```
|
||||
|
||||
### Insert Records
|
||||
|
||||
```php
|
||||
$newRecord = CmsApi::insert('contacto', [
|
||||
'nombre' => 'John',
|
||||
'email' => 'john@example.com',
|
||||
'mensaje' => 'Hello',
|
||||
]);
|
||||
```
|
||||
|
||||
### Update Records
|
||||
|
||||
```php
|
||||
CmsApi::update('productos',
|
||||
['precio' => 29.99, 'activo' => 1], // fields to update
|
||||
['num' => 42] // where condition
|
||||
);
|
||||
```
|
||||
|
||||
### Delete Records
|
||||
|
||||
```php
|
||||
CmsApi::delete('productos', ['num' => 42]);
|
||||
```
|
||||
|
||||
### Important Rules
|
||||
|
||||
- Table names **without** `cms_` prefix
|
||||
- Primary key is always `num`, never `id`
|
||||
- Upload fields are handled separately (not via insert/update)
|
||||
- Operators: `=`, `!=`, `>`, `>=`, `<`, `<=`, `LIKE`, `IN`
|
||||
|
||||
|
||||
## CmsApi (JavaScript — Client-Side)
|
||||
|
||||
```js
|
||||
// Hook call
|
||||
CmsApi.hook('/hooks/module_id/', { param: 'value' }, function(response) {
|
||||
// response is the hook output
|
||||
});
|
||||
|
||||
// Record operations (if exposed via hooks)
|
||||
CmsApi.get('tableName', { where: conditions }, function(records) {
|
||||
// records array
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## CocoDB
|
||||
|
||||
`CocoDB` is the database abstraction layer for Acai.
|
||||
Low-level database abstraction layer. Use when CmsApi is too high-level.
|
||||
|
||||
<!-- TODO: Document CocoDB usage (query, insert, update, delete, transactions) -->
|
||||
<!-- TODO: Document CocoDB methods (query, insert, update, delete, transactions) -->
|
||||
|
||||
|
||||
## Database Operations
|
||||
## Table Schemas
|
||||
|
||||
### Direct Queries
|
||||
Table schemas are stored as JSON in `cms/data/schema/`. Each file defines:
|
||||
- Field names and types
|
||||
- Validation rules
|
||||
- Relationships (foreign keys)
|
||||
- Display configuration
|
||||
|
||||
<!-- TODO: Document how to run raw SQL queries from hooks -->
|
||||
### Field Format Types
|
||||
|
||||
### Table Schemas
|
||||
|
||||
Table schemas are stored as JSON files in `cms/data/schema/`. Each file defines the fields, types, and configuration of a CMS table.
|
||||
|
||||
<!-- TODO: Document schema format and how to create/modify tables -->
|
||||
| Type | PHP Format | Notes |
|
||||
|------|-----------|-------|
|
||||
| Text | String | Plain text |
|
||||
| Date/time | `YYYY-MM-DD HH:mm:ss` | MySQL datetime format |
|
||||
| Checkbox | `1` or `0` | Boolean as integer |
|
||||
| WYSIWYG | HTML string | Rich text with Tailwind classes |
|
||||
| List | String or num | Foreign key if linked to table |
|
||||
| Multivalores | JSON string | Serialized array |
|
||||
| Upload | — | Handled separately, never in insert/update |
|
||||
|
||||
@@ -4,51 +4,102 @@
|
||||
|
||||
Modules are the visual building blocks of Acai websites. Each module lives in `template/estandar/modulos/<module-id>/`.
|
||||
|
||||
### File structure
|
||||
### File Structure
|
||||
|
||||
```
|
||||
<module-id>/
|
||||
├── index-base.tpl # Source Twig template (EDIT THIS)
|
||||
├── index-base.tpl # Source template (EDIT THIS)
|
||||
├── index.tpl # Compiled output (auto-generated, do NOT edit)
|
||||
├── index-twig.tpl # Compiled Twig output (auto-generated, do NOT edit)
|
||||
├── builder.json # Compiled builder vars (auto-generated, do NOT edit)
|
||||
├── style.css # Module-scoped styles
|
||||
└── script.js # Module JavaScript
|
||||
```
|
||||
|
||||
### Twig Templates
|
||||
### Template Syntax
|
||||
|
||||
<!-- TODO: Document Twig syntax, variable access, builder vars -->
|
||||
Templates use a hybrid of **Twig** and **Acai attributes**. The source file is always `index-base.tpl`.
|
||||
|
||||
### Custom Twig Filters
|
||||
```html
|
||||
<section class="hero-section" id="{{ section_id }}">
|
||||
<div class="container mx-auto px-4">
|
||||
<h2 data-field-type="headfield" class="text-3xl font-bold">
|
||||
Title here
|
||||
</h2>
|
||||
<p data-field-type="textbox" class="text-lg text-gray-600">
|
||||
Description text
|
||||
</p>
|
||||
<img data-field-type="upload" src="placeholder.jpg" class="w-full rounded-lg" />
|
||||
<a data-field-type="link" href="#" class="btn">Call to action</a>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
<!-- TODO: Document custom filters available in Acai (e.g., |translate, |slugify, etc.) -->
|
||||
### Including Modules from Other Modules
|
||||
|
||||
### Builder Variables
|
||||
```html
|
||||
<module_id :param1="value1" :param2="'string value'"></module_id>
|
||||
```
|
||||
|
||||
<!-- TODO: Document how builder vars work, how to access them in templates -->
|
||||
Parameters are received as variables inside the included module.
|
||||
|
||||
### Calling Modules from Other Modules
|
||||
### Global Variables
|
||||
|
||||
<!-- TODO: Document how to include/call modules from other modules or general sections -->
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `section_id` | Unique ID per module instance (use for anchors, JS scoping) |
|
||||
| `interno` | `true` when viewing in CMS editor, `false` on public site |
|
||||
| `server.HTTP_HOST` | Current domain |
|
||||
| `loop.index` | 1-based iteration index (inside `c-for`) |
|
||||
| `loop.index is odd` / `loop.index is even` | For alternating layouts |
|
||||
|
||||
|
||||
## General Sections
|
||||
|
||||
General sections are reusable layout blocks (headers, footers, sidebars) shared across pages.
|
||||
General sections are database-backed templates used for record views, headers, footers, and reusable layouts. They use the same template engine as modules.
|
||||
|
||||
<!-- TODO: Document how general sections differ from modules, where they live, how to create/edit them -->
|
||||
### Key Differences from Modules
|
||||
|
||||
- Access record data via the `thisrecord` variable
|
||||
- Upload fields return **arrays**: `thisrecord.image[0].urlPath`
|
||||
- Additional upload metadata: `info1` (alt text), `info2`, `info3`, `info4`
|
||||
- Foreign key fields use `_num` suffix: `thisrecord.category_num`
|
||||
- Saved via `save_general_section()` (not `save_module()`)
|
||||
- Parser type 2 = Twig (recommended), 0 = Acai legacy syntax
|
||||
|
||||
### Example: Record Template
|
||||
|
||||
```html
|
||||
<article class="product-card">
|
||||
<img src="{{ thisrecord.imagen[0].urlPath }}"
|
||||
alt="{{ thisrecord.imagen[0].info1 }}"
|
||||
class="w-full h-64 object-cover" />
|
||||
<h3 class="text-xl font-semibold">{{ thisrecord.nombre }}</h3>
|
||||
<p class="text-gray-600">{{ thisrecord.descripcion | raw }}</p>
|
||||
<span class="text-2xl font-bold">{{ thisrecord.precio }}€</span>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Variable Assignment
|
||||
|
||||
Use `<set>` tag to create variables from queries:
|
||||
|
||||
```html
|
||||
<set :categories="'categorias' | get()"></set>
|
||||
<set :featured="'productos' | get({destacado: 1}, 'orden ASC', 3)"></set>
|
||||
```
|
||||
|
||||
|
||||
## CSS & JavaScript
|
||||
## Repeatable Content (multiv2)
|
||||
|
||||
### Module Styles (`style.css`)
|
||||
The `multiv2` builder field type creates repeatable groups of fields:
|
||||
|
||||
<!-- TODO: Document scoping rules, CSS conventions -->
|
||||
```html
|
||||
<div c-for="item in record.items">
|
||||
<h3 data-field-type="textfield">{{ item.title }}</h3>
|
||||
<p data-field-type="textbox">{{ item.description }}</p>
|
||||
<img data-field-type="upload" src="{{ item.image }}" />
|
||||
</div>
|
||||
```
|
||||
|
||||
### Module Scripts (`script.js`)
|
||||
|
||||
<!-- TODO: Document JS conventions, lifecycle, event handling -->
|
||||
|
||||
|
||||
## Builder Field Types
|
||||
|
||||
<!-- TODO: Document data-field-type usage, c-form, c-if, c-for, c-class attributes -->
|
||||
Access individual items: `record.items[0].title`, `record.items[1].image`, etc.
|
||||
|
||||
118
docs/twig-filters.md
Normal file
118
docs/twig-filters.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Twig Filters Reference
|
||||
|
||||
Acai uses Twig **filters** (with `|` pipe syntax). Never use Twig functions — only filters are supported.
|
||||
|
||||
## Database Queries
|
||||
|
||||
### `get` — Query Table
|
||||
|
||||
```twig
|
||||
{# All records #}
|
||||
{% set products = 'productos' | get() %}
|
||||
|
||||
{# With WHERE #}
|
||||
{% set active = 'productos' | get({activo: 1}) %}
|
||||
|
||||
{# With WHERE + ORDER + LIMIT #}
|
||||
{% set latest = 'noticias' | get({publicado: 1}, 'fecha DESC', 6) %}
|
||||
|
||||
{# Single record (first result) #}
|
||||
{% set product = 'productos' | get({num: 42}) %}
|
||||
{{ product[0].nombre }}
|
||||
```
|
||||
|
||||
### `queryDB` — Raw SQL Query
|
||||
|
||||
```twig
|
||||
{% set results = 'SELECT * FROM cms_productos WHERE precio > 100 ORDER BY precio ASC' | queryDB() %}
|
||||
```
|
||||
|
||||
Note: Raw SQL uses the full table name WITH `cms_` prefix.
|
||||
|
||||
## Module & Hook Execution
|
||||
|
||||
### `hook` — Execute PHP Hook
|
||||
|
||||
```twig
|
||||
{# Call hook and output result #}
|
||||
{{ 'hooks/module_id/' | hook({param1: 'value', param2: variable}) }}
|
||||
|
||||
{# Capture into variable #}
|
||||
{% set result = 'hooks/module_id/' | hook({action: 'getData'}) %}
|
||||
```
|
||||
|
||||
### `module` — Render Another Module
|
||||
|
||||
```twig
|
||||
{{ 'other_module_id' | module({param1: value1}) }}
|
||||
```
|
||||
|
||||
## Text & Content
|
||||
|
||||
### `translate` — Translation
|
||||
|
||||
```twig
|
||||
{{ 'Hello' | translate }}
|
||||
{{ variable | translate }}
|
||||
```
|
||||
|
||||
### `raw` — Render HTML Without Escaping
|
||||
|
||||
```twig
|
||||
{{ record.description | raw }}
|
||||
```
|
||||
|
||||
### `truncate` — Text Truncation
|
||||
|
||||
```twig
|
||||
{{ record.description | truncate(150) }}
|
||||
```
|
||||
|
||||
### `json_decode` — Parse JSON String
|
||||
|
||||
```twig
|
||||
{% set data = jsonString | json_decode %}
|
||||
{{ data.key }}
|
||||
```
|
||||
|
||||
## Images
|
||||
|
||||
### `imagec` — Image Optimization/Resize
|
||||
|
||||
```twig
|
||||
{# Resize to width #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(400) }}" />
|
||||
|
||||
{# In srcset #}
|
||||
<img src="{{ record.image[0].urlPath | imagec(800) }}"
|
||||
srcset="{{ record.image[0].urlPath | imagec(400) }} 400w,
|
||||
{{ record.image[0].urlPath | imagec(800) }} 800w" />
|
||||
```
|
||||
|
||||
## Operators & Syntax
|
||||
|
||||
### Concatenation
|
||||
|
||||
Twig uses `~` for string concatenation (not `.` or `+`):
|
||||
|
||||
```twig
|
||||
{{ 'Hello ' ~ name ~ '!' }}
|
||||
{% set url = '/products/' ~ product.slug ~ '/' %}
|
||||
```
|
||||
|
||||
### Ternary / Default
|
||||
|
||||
```twig
|
||||
{{ title | default('Default Title') }}
|
||||
{{ isActive ? 'active' : 'inactive' }}
|
||||
```
|
||||
|
||||
### Comparisons in Twig
|
||||
|
||||
```twig
|
||||
{% if items | length > 0 %}
|
||||
{% if type == 'premium' %}
|
||||
{% if name is not empty %}
|
||||
```
|
||||
|
||||
Note: In `c-if` attributes, use `=` (single equals) for equality. In Twig `{% if %}` blocks, use `==` (double equals).
|
||||
Reference in New Issue
Block a user