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#
- Create the folder on the pattern above — the component, the factory
interface,
Templates/Default/. - Split the logic into traits once it outgrows one file (loading apart from actions).
- Register the factory in the configuration and add
createComponentXxx()to the presenter. - Write the template into
Templates/Default/— so every theme inherits it. - Translate texts in PHP, send them to the template ready.
- 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.