Skip to content
V
For developers
Architecture, conventions, the core and security
UI layer / Components

Components#

A reusable piece of a page with its own logic, template and possibly its own signals. The base: app/Core/Base/BaseComponent.php.

When to make a component at all#

Situation What to do
A chunk of markup repeats but has no logic a partial in the template (_something.latte)
It repeats together with logic (loads data, reacts to clicks) a component
I need to redraw part of the page over AJAX a component with a {snippet}
It is one page and nowhere else leave it in the presenter

A component is not an extra folder — it is a boundary

What is inside may have its own state and signals and can be redrawn on its own. When you do not need that, a partial is enough and cheaper to understand.

Folder structure#

Components/AdvertListComponent/
  AdvertList.php            the component
  AdvertListFactory.php     the factory interface
  Traits/                   logic split up (loading, filters, actions)
  Templates/
    Default/
      Style_1.latte         the default look
      Style_2.latte         another look of the same thing
      Elements/
        Actions.latte       a piece shared between the styles

Shared pieces into Elements/, not three copies

Whatever repeats across all styles (price, buttons, the user badge) belongs in Elements/. Three copies announce themselves by a change being made in two places out of three — and it looks right until somebody switches the style.

How one comes into being#

A component is never constructed directly. You write a factory interface, the container generates the implementation, and the presenter asks for it:

// AdvertListFactory.php
interface AdvertListFactory
{
    public function create(): AdvertList;
}

// in the presenter
public function __construct(private readonly AdvertListFactory $advertListFactory) {}

protected function createComponentAdvertList(): AdvertList
{
    return $this->advertListFactory->create();
}

In the template it is then {control advertList}, or {control advertList, $argument}.

Two traits injecting a property of the same name will NOT merge

It surfaces as an error while the container is being built, not while you write the code — so it looks like a configuration problem even though it is a name clash in traits.

How a component finds its template#

In three steps, first hit wins:

1. Templates/<Theme>/<Variant>/Style_1.latte    (exactly this theme and device)
2. Templates/<Theme>/default/Style_1.latte      (this theme, all devices)
3. Templates/Default/Style_1.latte              (the shared default)

The look is chosen by an argument when rendering; renderStyle2() resolves to Style_2.latte automatically, so no method has to be written for it.

A new theme need not copy components

It inherits Templates/Default/ and overrides only what it wants different.

A component template has NO domain-aware translator

Unlike a presenter. Texts are translated in PHP and arrive in the template ready.

Paths in {include} are relative to the file

Moving a template breaks every nested include it has.

The shared modal window#

The modal is not drawn in Latte. A shared shell is mounted once in the layout and components merely put content into it and open it.

$this->getModalWindow()          // from ModalAwareTrait
    ->setTitle('Reply to the ad')
    ->setSize('modal-lg')
    ->setContent($html)          // finished HTML, typically a rendered partial
    ->open();
Property How it is
Where the shell is once in the admin @layout.latte as {control modalWindow}
Who fills it any presenter or component beneath it
State none — neither title nor content is stored in the session, they live for this request only
Opening over AJAX only {snippet modalBody} is redrawn and modalShow with an id goes into the response; modals.js does the showing
Several windows at once not supported — it is one shared shell

That is why a form from another component can go into it

The content is an ordinary HTML string. A component renders its own partial into a string and passes it to the modal; the modal knows nothing about it and does not need to.

The component must run under a presenter that provides the modal

getModalWindow() checks this and otherwise throws, naming the component. It is not pedantry — without the shell in the layout the window would have nowhere to appear.

A required field in a hidden modal blocks the WHOLE form from submitting

The browser refuses to submit because of a field the user cannot see, and does not say why — the page simply stops responding to the submit. Requiredness therefore has to be toggled by whether the modal is open.

Signals#

A handle*() method serves a component action (delete, toggle, load more). It is called by an ordinary URL the component generates for itself.

A signal is a public URL — it must verify the request's origin

Without that check it can be triggered from a foreign page in the name of the signed-in user. Same-origin checking on handle*() is mandatory, not optional.

AJAX redraws only the INSIDE of a snippet

Attributes of the element the snippet sits on (classes, data-) keep their original values. Whatever must change has to be inside.

A new component, step by step#

  1. Create the folder on the pattern above — the component, the factory interface, Templates/Default/.
  2. Split the logic into traits once it outgrows one file (loading apart from actions).
  3. Register the factory in the configuration and add createComponentXxx() to the presenter.
  4. Write the template into Templates/Default/ — so every theme inherits it.
  5. Translate texts in PHP, send them to the template ready.
  6. Click through it — including the AJAX path if the component redraws.

Fixing a shared base is a change in every module that uses it

A component with a shared ancestor for three modules means a fix for one hits all three. Walk all of them — not just the one you are fixing.

Finished components worth reading#

Component Where What makes it interesting
DataGrid app/UI/Admin/System/Components/ a table with filters, ordering and bulk actions
Picker same place picking an entity instead of a <select>
Modal window same place the shared shell described above
Advert listing app/UI/Front/Bazaar/Components/AdvertRendererComponent/ three looks plus Elements/

The visual side of the front-end components is in the component catalogue.