Budowanie własnych cząstek (Particles) w Gantry 5

Gantry 5 pozwala tworzyć własne bloki treści zwane particles — karuzele, galerie, liczniki, formularze

— zarządzane bezpośrednio z panelu administracyjnego, bez pisania PHP. W tym artykule pokazuję jak je budować od zera i czego unikać.


Struktura plików

Każda particle to obowiązkowo dwa pliki w katalogu custom/particles/ szablonu. Opcjonalnie można wydzielić CSS i JS do osobnych plików:

templates/{szablon}/custom/
├── particles/
│   ├── nazwa.yaml           ← pola w panelu admina Gantry (wymagany)
│   └── nazwa.html.twig      ← szablon HTML + CSS + JS (wymagany)
├── css/
│   └── nazwa.css            ← opcjonalny zewnętrzny plik CSS
└── js/
    └── nazwa.js             ← opcjonalny zewnętrzny plik JS

Zewnętrzne pliki CSS i JS ładuje się w szablonie Twig przez protokół gantry-theme://:

{% block stylesheets %}
    <link rel="stylesheet" href="/{{ url('gantry-theme://css/nazwa.css') }}">
{% endblock %}

{% block javascript_footer %}
    <script src="/{{ url('gantry-theme://js/nazwa.js') }}"></script>
{% endblock %}

Pliki w katalogu custom/ nadpisują domyślne particle szablonu i nie są usuwane przy aktualizacji. Katalog particles/ jest obowiązkowy — bez niego Gantry nie wykryje particle.


Plik YAML — definicja panelu admina

YAML definiuje wszystkie pola widoczne podczas edycji particle w Gantry Layout Manager. Minimalna wymagana struktura:

name: Moja Particle
description: Opis widoczny w panelu.
type: particle
icon: fa-star

configuration:
  caching:
    type: static

form:
  overrideable: false
  fields:
    enabled:
      type: input.checkbox
      label: Enabled
      default: true

    copyright:
      type: separator.note
      class: alert alert-success
      content: 'Moja Particle — v1.0'

Zakładki

Pola warto grupować w zakładki — panel staje się znacznie czytelniejszy przy większej liczbie opcji. Wymaga Gantry 5.3.2+.

_tabs:
  type: container.tabs
  fields:
    _tab_content:
      label: Treść
      fields:
        title:
          type: input.text
          label: Tytuł

    _tab_advanced:
      label: Zaawansowane
      fields:
        class:
          type: input.text
          label: Dodatkowe klasy CSS

Najważniejsze typy pól

TypZastosowanie
input.text Zwykłe pole tekstowe
input.checkbox Tak / Nie
input.number Liczba z zakresem (min/max/step)
input.imagepicker Wybieracz obrazu z mediów Joomla
input.colorpicker Kolor (hex)
select.select Lista rozwijana z opcjami
textarea.textarea Wieloliniowy tekst
collection.list Lista powtarzalnych elementów (slajdy, karty, galeria)
collection.keyvalue Dodatkowe atrybuty HTML (data-*, style)
separator.note Wizualny nagłówek sekcji w panelu

Lista powtarzalnych elementów (collection.list)

items:
  type: collection.list
  array: true
  label: Elementy
  value: name
  ajax: true
  overrideable: false
  fields:
    .name:
      type: input.text
      label: Tytuł
    .image:
      type: input.imagepicker
      label: Zdjęcie
    .disable:
      type: input.checkbox
      label: Ukryj element
      default: false

Plik Twig — cztery bloki szablonu

Każda particle rozszerza bazowy szablon Gantry i może definiować cztery niezależne bloki:

{% extends '@nucleus/partials/particle.html.twig' %}

{% block stylesheets %}
    {# CSS — renderowany w head #}
{% endblock %}

{% block particle %}
    {# Właściwy HTML particle — wymagany #}
{% endblock %}

{% block javascript %}
    {# JS w head (rzadziej używany) #}
{% endblock %}

{% block javascript_footer %}
    {# JS przed </body> — zalecany dla wydajności #}
{% endblock %}

Pułapka nr 1 — zasięg zmiennych między blokami

Zmienne ustawione wewnątrz bloku NIE są widoczne w innych blokach. To najczęstsza przyczyna błędów w particles Gantry 5.

Zmienne potrzebne w wielu blokach ustaw poza blokami, na poziomie szablonu:

{% extends '@nucleus/partials/particle.html.twig' %}

{# Dostępne we WSZYSTKICH blokach #}
{% set active_items = [] %}
{% for item in particle.items|default([]) %}
    {% if not item.disable %}
        {% set active_items = active_items|merge([item]) %}
    {% endif %}
{% endfor %}
{% set component_id = 'g-myparticle-' ~ id %}

{% block particle %}
    {# active_items i component_id dostępne tutaj #}
{% endblock %}

{% block javascript_footer %}
    {# i tutaj też #}
{% endblock %}

Pułapka nr 2 — URL z imagepicker

input.imagepicker przechowuje ścieżki w formacie gantry-media://.... Gantry tłumaczy ten protokół na prawdziwy URL wyłącznie przez funkcję url():

{# Poprawnie — url() tłumaczy gantry-media:// na prawdziwy URL #}
<img src="/{{ url(item.image)|e }}" alt="{{ item.alt|e }}">

{# Błędnie — przeglądarka dostaje "gantry-media://..." i nie może załadować obrazu #}
<img src="/{{ item.image|raw }}" alt="">

Gdy URL zdjęcia trafia do JavaScript (np. lightbox), przekaż go przez atrybut data-*:

{# Twig tłumaczy URL podczas renderowania — JS czyta gotową ścieżkę #}
<div data-src="{{ url(item.image)|e }}"
     data-title="{{ item.name|default('')|e }}"></div>
// JavaScript — czyta gotowy URL, nie surowy protokół
var src = klikniety.dataset.src;
obrazEl.src = src;

Nigdy nie serializuj URL-i imagepicker przez json_encode. JSON zachowa surowy protokół gantry-media://, którego przeglądarka nie rozumie.

CSS zależny od ustawień — block stylesheets

{% block stylesheets %}
{% if particle.enabled %}
<style>
#{{ component_id }} { background: {{ particle.bg_color|default('#fff')|e }}; }
#{{ component_id }} .item { min-height: {{ particle.height|default(300) }}px; }
</style>
{% endif %}
{% endblock %}

Siatka responsywna — CSS Grid zamiast Bootstrap row/col

Gantry 5 oparty na UIkit nie ładuje Bootstrap CSS. Klasy row, col-4 itp. nie działają bez Bootstrap. Rozwiązanie: własne CSS Grid w {% block stylesheets %}:

{% set cols_xs = particle.cols_xs|default(1) %}
{% set cols_md = particle.cols_md|default(2) %}
{% set cols_lg = particle.cols_lg|default(3) %}

{% block stylesheets %}
<style>
#{{ component_id }} .g-grid {
    display: grid;
    grid-template-columns: repeat({{ cols_xs }}, 1fr);
    gap: 16px;
}
@media (min-width: 768px) {
    #{{ component_id }} .g-grid { grid-template-columns: repeat({{ cols_md }}, 1fr); }
}
@media (min-width: 992px) {
    #{{ component_id }} .g-grid { grid-template-columns: repeat({{ cols_lg }}, 1fr); }
}
</style>
{% endblock %}

Ładowanie Bootstrap JS w Joomla

Joomla nie ładuje automatycznie komponentów Bootstrap JS. Każdy komponent to osobny plik ES moduł w katalogu vendor:

/media/vendor/bootstrap/js/carousel.min.js
/media/vendor/bootstrap/js/modal.min.js
/media/vendor/bootstrap/js/collapse.min.js

Poprawne podejście to dynamic-import z URL-em root pobranym z konfiguracji Joomla:

{% block javascript_footer %}
<script type="module">
    // Pobierz root strony — obsługa instalacji w podkatalogu
    const root = Joomla.getOptions('system.paths').root;

    // Załaduj komponent z vendor — eksportuje klasę jako module.C
    const { C: Carousel } = await import(root + '/media/vendor/bootstrap/js/carousel.min.js');

    new Carousel(document.getElementById('{{ component_id }}'), {
        interval : 5000,
        ride     : 'carousel',
        touch    : true,
        wrap     : true
    });
</script>
{% endblock %}
KomponentEksport (minified)Fallback
carousel.min.js module.C module.C || module.Carousel || module.default
modal.min.js module.M module.M || module.Modal || module.default

Bootstrap CSS — Joomla 4 vs Joomla 5 vs Gantry 5

Przed użyciem klas Bootstrap (.modal, .btn, .d-flex) sprawdź <head> strony. Jeśli nie ma bootstrap.min.css — dodaj potrzebne style inline w {% block stylesheets %} particle.


Studium przypadku — galeria z lightboxem

Budując particle galerii dla szablonu Gantry 5 na Joomla 4, napotkałem kilka nieoczywistych problemów:

Problem 1

Zdjęcia nie ładowały się w lightboxie — imagepicker zwraca gantry-media://, który JS przekazywał bezpośrednio jako src.

Rozwiązanie: Atrybut data-src="{{ url(item.image)|e }}" zamiast JSON.

Problem 2

Siatka nie działała — Bootstrap row/col-* bez załadowanego Bootstrap CSS.

Rozwiązanie: CSS Grid w {% block stylesheets %} — niezależny od żadnego frameworka.

Problem 3

Modal nie miał wyglądu — Bootstrap Modal CSS nie ładuje się na Joomla 4 z Gantry/UIkit.

Rozwiązanie: Minimalne style modalu wbudowane inline w particle.


Bonus — naprawa instalacji szablonu Gantry na Joomla 6

Starsze szablony Gantry 5 podczas instalacji na Joomla 6 zgłaszają błąd — używają klasy usuniętej w nowej wersji. Naprawa w pliku install.php szablonu:

// Było (Joomla 3/4):
use Joomla\CMS\Filesystem\Folder;

// Powinno być (Joomla 5+/6):
use Joomla\Filesystem\Folder;

Podsumowanie

  • Zmienne globalne (poza blokami) są dostępne wszędzie — zmienne w bloku tylko w nim
  • URL-e z imagepicker zawsze przez url() — nigdy |raw ani JSON
  • Siatka responsywna przez CSS Grid, nie Bootstrap row/col
  • Bootstrap JS ładowany przez dynamic-import z katalogu vendor Joomla
  • Bootstrap CSS sprawdź w <head> — nie zakładaj że jest załadowany

Potrzebujesz niestandardowej funkcjonalności na swojej stronie Joomla?

Chętnie pomogę — sprawdź moje portfolio lub napisz bezpośrednio.

Ten adres pocztowy jest chroniony przed spamowaniem. Aby go zobaczyć, konieczne jest włączenie w przeglądarce obsługi JavaScript.