← Volver al blog
·9 min de lectura

Fragmentos personalizados en Liferay DXP: guia practica

LiferayFrontend

Que son los fragmentos en Liferay

Los fragmentos (fragments) son la unidad basica de construccion visual en Liferay DXP. Son componentes autocontenidos compuestos por HTML, CSS, JavaScript y un archivo de configuracion JSON. Los editores de contenido los arrastran y sueltan en las paginas para construir layouts sin necesidad de escribir codigo.

A diferencia de los widgets (portlets), que son aplicaciones Java completas desplegadas como modulos OSGi, los fragmentos son ligeros. No requieren un backend propio ni un ciclo de despliegue complejo. Se crean, editan y publican directamente desde el panel de administracion de Liferay o se importan como archivos ZIP desde un entorno de desarrollo.

Esta diferencia es importante para entender cuando usar cada uno:

  • Fragmentos: Componentes visuales y de presentacion. Hero banners, tarjetas de contenido, secciones de texto con imagen, testimonios, CTAs. Todo lo que es primariamente layout y estilo.
  • Widgets: Funcionalidad compleja con logica de negocio. Formularios con validaciones avanzadas, integraciones con APIs externas, aplicaciones interactivas con estado.

En la practica, la mayoria de las paginas de contenido en Liferay se construyen combinando fragmentos. Los widgets se reservan para funcionalidad especifica.

Crear una coleccion de fragmentos

Los fragmentos se organizan en colecciones (Fragment Sets). Cada coleccion agrupa fragmentos relacionados. Desde el panel de administracion:

  1. Navega a Design > Fragments
  2. Haz clic en el boton + para crear una nueva coleccion
  3. Asigna un nombre descriptivo (ej. "Componentes Marketing", "Hero Sections")

Dentro de la coleccion, puedes crear fragmentos individuales. Cada fragmento tiene cuatro archivos editables:

  • HTML (index.html): Estructura y contenido editable
  • CSS (index.css): Estilos del fragmento
  • JavaScript (index.js): Comportamiento interactivo
  • Configuration (index.json): Opciones configurables por el editor

Estructura HTML y elementos editables

El HTML de un fragmento usa atributos especiales lfr-editable para marcar las zonas que los editores de contenido pueden modificar:

<div class="custom-hero">
    <div class="custom-hero__content">
        <span class="custom-hero__label"
            lfr-editable-id="label"
            lfr-editable-type="text">
            Etiqueta
        </span>
        <h1 class="custom-hero__title"
            lfr-editable-id="title"
            lfr-editable-type="rich-text">
            Titulo principal del hero
        </h1>
        <p class="custom-hero__description"
            lfr-editable-id="description"
            lfr-editable-type="text">
            Descripcion breve que acompana al titulo.
        </p>
        <a class="custom-hero__cta"
            lfr-editable-id="cta-link"
            lfr-editable-type="link"
            href="#">
            Llamada a la accion
        </a>
    </div>
    <div class="custom-hero__image">
        <img lfr-editable-id="hero-image"
             lfr-editable-type="image"
             src="data:image/png;base64,iVBORw0KGgo="
             alt="Hero image">
    </div>
</div>

Los tipos de lfr-editable-type disponibles son:

  • text: Texto plano, sin formato
  • rich-text: Texto con formato (negrita, cursiva, listas)
  • image: Imagen reemplazable desde la biblioteca de medios
  • link: Enlace con URL y texto editables
  • html: HTML libre (usar con precaucion)

Cada lfr-editable-id debe ser unico dentro del fragmento. Es el identificador que Liferay usa para almacenar el contenido personalizado de cada instancia del fragmento en la pagina.

CSS del fragmento

Los estilos se escriben directamente en el archivo CSS. Liferay encapsula los estilos del fragmento, pero es buena practica usar una clase raiz unica para evitar colisiones con otros fragmentos o con estilos globales del tema:

.custom-hero {
    display: flex;
    align-items: center;
    gap: 3rem;
    padding: 4rem 2rem;
    min-height: 500px;
}

.custom-hero__content {
    flex: 1;
    max-width: 600px;
}

.custom-hero__label {
    display: inline-block;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--hero-accent-color, #c9553d);
    margin-bottom: 1rem;
}

.custom-hero__title {
    font-size: clamp(2rem, 4vw, 3.5rem);
    font-weight: 700;
    line-height: 1.1;
    margin-bottom: 1.5rem;
    color: var(--hero-title-color, #1a1614);
}

.custom-hero__description {
    font-size: 1.125rem;
    line-height: 1.6;
    color: #5c5550;
    margin-bottom: 2rem;
}

.custom-hero__cta {
    display: inline-block;
    padding: 0.75rem 2rem;
    background-color: var(--hero-accent-color, #c9553d);
    color: #ffffff;
    text-decoration: none;
    border-radius: 6px;
    font-weight: 600;
    transition: background-color 0.2s ease;
}

.custom-hero__cta:hover {
    opacity: 0.9;
}

.custom-hero__image {
    flex: 1;
}

.custom-hero__image img {
    width: 100%;
    height: auto;
    border-radius: 8px;
    object-fit: cover;
}

@media (max-width: 768px) {
    .custom-hero {
        flex-direction: column;
        text-align: center;
        padding: 2rem 1rem;
    }
}

Observa el uso de CSS custom properties como var(--hero-accent-color, #c9553d). Estas variables se conectan con el sistema de configuracion del fragmento, permitiendo que los editores cambien colores sin tocar CSS.

Configuracion JSON

El archivo index.json define las opciones que los editores de contenido ven en el panel lateral cuando seleccionan el fragmento. Esto es lo que hace a los fragmentos verdaderamente reutilizables:

{
    "fieldSets": [
        {
            "label": "Apariencia",
            "fields": [
                {
                    "name": "accentColor",
                    "label": "Color de acento",
                    "type": "colorPicker",
                    "dataType": "object",
                    "defaultValue": {
                        "cssClass": "",
                        "color": "#c9553d",
                        "rgbValue": "201,85,61"
                    }
                },
                {
                    "name": "titleColor",
                    "label": "Color del titulo",
                    "type": "colorPicker",
                    "dataType": "object",
                    "defaultValue": {
                        "cssClass": "",
                        "color": "#1a1614",
                        "rgbValue": "26,22,20"
                    }
                },
                {
                    "name": "layout",
                    "label": "Disposicion",
                    "type": "select",
                    "dataType": "string",
                    "defaultValue": "image-right",
                    "typeOptions": {
                        "validValues": [
                            { "value": "image-right", "label": "Imagen a la derecha" },
                            { "value": "image-left", "label": "Imagen a la izquierda" },
                            { "value": "image-bg", "label": "Imagen de fondo" }
                        ]
                    }
                },
                {
                    "name": "fullWidth",
                    "label": "Ancho completo",
                    "type": "checkbox",
                    "dataType": "bool",
                    "defaultValue": false
                },
                {
                    "name": "minHeight",
                    "label": "Altura minima (px)",
                    "type": "text",
                    "dataType": "int",
                    "defaultValue": 500,
                    "typeOptions": {
                        "placeholder": "500"
                    }
                }
            ]
        }
    ]
}

Los tipos de campo disponibles son:

  • text: Campo de texto libre (string o int)
  • select: Desplegable con opciones predefinidas
  • checkbox: Booleano on/off
  • colorPicker: Selector de color integrado con la paleta del tema
  • itemSelector: Selector de contenido de Liferay (Web Content, documentos)
  • collectionSelector: Selector de colecciones de contenido

Acceder a la configuracion desde HTML con FreeMarker

Aqui es donde la potencia de los fragmentos se revela. El HTML de un fragmento no es HTML plano: Liferay lo procesa con FreeMarker, el motor de plantillas de Java. Esto permite acceder a los valores de configuracion y renderizar contenido dinamico:

[#assign accentColor = configuration.accentColor.color!'#c9553d']
[#assign titleColor = configuration.titleColor.color!'#1a1614']
[#assign layout = configuration.layout!'image-right']
[#assign fullWidth = configuration.fullWidth?then('custom-hero--full', '')]
[#assign minHeight = configuration.minHeight!500]

<div class="custom-hero custom-hero--${layout} ${fullWidth}"
     style="--hero-accent-color: ${accentColor}; --hero-title-color: ${titleColor}; min-height: ${minHeight}px;">
    <div class="custom-hero__content">
        <span class="custom-hero__label"
            lfr-editable-id="label"
            lfr-editable-type="text">
            Etiqueta
        </span>
        <h1 class="custom-hero__title"
            lfr-editable-id="title"
            lfr-editable-type="rich-text">
            Titulo principal del hero
        </h1>
        <p class="custom-hero__description"
            lfr-editable-id="description"
            lfr-editable-type="text">
            Descripcion breve que acompana al titulo.
        </p>
        <a class="custom-hero__cta"
            lfr-editable-id="cta-link"
            lfr-editable-type="link"
            href="#">
            Llamada a la accion
        </a>
    </div>

    [#if layout != 'image-bg']
    <div class="custom-hero__image">
        <img lfr-editable-id="hero-image"
             lfr-editable-type="image"
             src="data:image/png;base64,iVBORw0KGgo="
             alt="Hero image">
    </div>
    [/#if]
</div>

La sintaxis [#assign variable = valor] declara variables FreeMarker. El operador ! proporciona un valor por defecto si la configuracion no existe. ?then('valor_true', 'valor_false') es el operador ternario de FreeMarker.

Lo que hace este codigo es:

  1. Lee los valores de configuracion (colores, layout, ancho, altura)
  2. Los inyecta como CSS custom properties via el atributo style
  3. Agrega clases CSS condicionales segun el layout seleccionado
  4. Muestra u oculta la imagen segun el tipo de layout

El CSS necesita clases adicionales para las variantes de layout:

.custom-hero--image-left {
    flex-direction: row-reverse;
}

.custom-hero--image-bg {
    background-size: cover;
    background-position: center;
    position: relative;
}

.custom-hero--image-bg .custom-hero__content {
    position: relative;
    z-index: 1;
    max-width: 700px;
    margin: 0 auto;
    text-align: center;
}

.custom-hero--full {
    max-width: 100%;
    padding-left: 5%;
    padding-right: 5%;
}

JavaScript en fragmentos

El archivo JavaScript se ejecuta en el contexto del fragmento. Liferay proporciona la variable fragmentElement que referencia al elemento DOM raiz del fragmento:

const hero = fragmentElement;
const layout = configuration.layout || 'image-right';

// Ejemplo: Parallax sutil en el hero
if (layout === 'image-bg') {
    const content = hero.querySelector('.custom-hero__content');

    function handleScroll() {
        const rect = hero.getBoundingClientRect();
        const scrolled = -rect.top;
        const rate = scrolled * 0.3;

        if (rect.top < window.innerHeight && rect.bottom > 0) {
            hero.style.backgroundPositionY = `${rate}px`;
        }
    }

    window.addEventListener('scroll', handleScroll, { passive: true });
}

// Ejemplo: Animacion de entrada
const observerOptions = {
    threshold: 0.2,
    rootMargin: '0px 0px -50px 0px'
};

const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            entry.target.classList.add('custom-hero--visible');
            observer.unobserve(entry.target);
        }
    });
}, observerOptions);

observer.observe(hero);

La variable configuration tambien esta disponible en JavaScript, permitiendo acceder a los valores de configuracion para logica condicional.

Ten en cuenta que el JavaScript de fragmentos se ejecuta cada vez que la pagina carga. No hay gestion de ciclo de vida como en React o Vue. Si agregas event listeners, Liferay no los limpia automaticamente al navegar entre paginas en modo SPA. Para fragmentos simples esto rara vez es un problema, pero en fragmentos complejos considera limpiar listeners si detectas que la pagina usa navegacion SPA.

Importar y exportar fragmentos

Para trabajo en equipo y control de versiones, los fragmentos se exportan como archivos ZIP con una estructura definida:

mi-coleccion/
  collection.json
  hero-banner/
    index.html
    index.css
    index.js
    index.json
    thumbnail.png
  tarjeta-servicio/
    index.html
    index.css
    index.js
    index.json
    thumbnail.png

El archivo collection.json define los metadatos de la coleccion:

{
    "name": "Componentes Marketing",
    "description": "Fragmentos para paginas de marketing y landing pages"
}

Para exportar: desde el panel de Fragments, selecciona la coleccion y haz clic en Export. Para importar: haz clic en el icono de importacion y sube el ZIP. Liferay detecta fragmentos existentes y permite actualizarlos o crear nuevos.

Esta estructura se integra bien con Git. Puedes mantener los fragmentos en un repositorio, hacer code review de los cambios y desplegarlos como parte de tu pipeline CI/CD.

Buenas practicas

Mantener fragmentos autocontenidos. Cada fragmento debe funcionar independientemente. No dependas de estilos globales del tema ni de scripts externos que podrian no estar cargados. Si necesitas una libreria externa, incluyela en el JavaScript del fragmento.

Evitar contaminacion CSS global. Usa una clase raiz unica para tu fragmento y estiliza todo como descendiente de esa clase. Nunca uses selectores genericos como h1, p o a sin prefijar con la clase del fragmento. Un h1 { color: red; } en tu fragmento afectaria todos los titulos de la pagina.

Diseno responsivo dentro del fragmento. No asumas un ancho especifico. El fragmento puede estar dentro de un contenedor de 12 columnas o de 4. Usa unidades relativas y media queries basadas en breakpoints estandar.

Configuracion con valores por defecto sensatos. Todo campo de configuracion debe tener un defaultValue que produzca un fragmento visualmente completo. El editor deberia poder arrastrar el fragmento a la pagina y ver algo presentable antes de personalizarlo.

Thumbnails representativos. El archivo thumbnail.png es lo que los editores ven en el panel de fragmentos. Un thumbnail claro y representativo acelera significativamente el trabajo de los editores de contenido.

Los fragmentos son la herramienta que conecta a desarrolladores con editores de contenido en Liferay DXP. Un fragmento bien construido, con opciones de configuracion bien pensadas, permite que los editores creen paginas profesionales sin depender del equipo de desarrollo para cada cambio visual. Esa autonomia es el verdadero objetivo.