Atomic Design met Symfony UX
08 november 2023
5 minutes
Vorig jaar werd Symfony UX geïntroduceerd. Voor wie nog niet van Symfony UX heeft gehoord: het is een initiatief dat een reeks bibliotheken biedt om JavaScript-tools in je applicatie te integreren. De meeste bibliotheken maken het mogelijk om je applicatie interactief te maken vanuit je PHP context, zonder zelf een regel JavaScript code te schrijven. Dat is krachtig!
Aangezien we een paar tools onder de knie hebben die we graag gebruiken in al onze projecten, wilden we een klein experiment doen met Symfony UX: Zou het mogelijk zijn om atomic design, dat we in al onze op React gebaseerde projecten gebruiken, te implementeren met de pas geïntroduceerde Twig components library?
Wat is Atomic design?
In Atomic design kunnen pagina's worden opgesplitst in een reeks herbruikbare - op zichzelf staande - componenten. Een component kan verschillende groottes hebben: een klein icoontje, een grote tabel, ... Om deze verschillende componenten te structureren, kunt u de volgende directory-structuur gebruiken:
atomen: Atomen zijn de kleinste bouwstenen. Dit kunnen eenvoudige onderdelen zijn zoals knoppen, labels, ...
moleculen: Moleculen combineren atomen tot een klein herbruikbaar component dat op verschillende plaatsen in uw toepassing kan worden opgenomen. Een voorbeeld is een zoekformulier dat bestaat uit een zoekveld en een zoekknop.
organismen: Organismen kunnen worden gebruikt om zelfstandige, draagbare, herbruikbare componenten te maken. Deze componenten passen meestal concrete bedrijfslogica toe in uw toepassing. Een veel voorkomend voorbeeld is een gebruikersformulier dat kan worden gebruikt voor zowel het aanmaken als het bijwerken van het model.
sjablonen: Binnen een template kunt u meerdere organismen, modules, ... definiëren in een herbruikbaar template. De template zelf biedt alleen een plaatshouder waarin inhoud kan worden geïnjecteerd.
pagina's: Pagina-componenten kunnen worden gebruikt om specifieke inhoud of organismen in een template te injecteren. Een pagina is een zeer concrete implementatie van een ontwerp, die alle componenten combineert die nodig zijn om tot het uiteindelijke ontwerp van de applicatie te komen.
Als u meer wilt weten over atomair ontwerp, raden wij u aan dit artikel te lezen.
Wat zijn Twig componenten?
Twig components is een nieuwe bouwsteen van Symfony UX, waarmee een ontwikkelaar een herbruikbaar lay-out blok kan maken. Het bestaat uit een eenvoudig PHP object dat de gegevens bevat die worden gebonden aan een afzonderlijke Twig view. Het maakt het mogelijk om template "units" te hergebruiken in alle templates van de applicatie.
Hmmm leuk... Maar wat is het voordeel ten opzichte van de oude Twig include manier van werken? Aangezien Twig componenten bestaan uit zowel een PHP object als een Twig template, kunnen we de attributen die worden doorgegeven valideren, aangepaste logica toevoegen via publieke methodes op het PHP object, betere foutmelding bij ongeldige gegevens, ...
Precies wat we nodig hebben, niet? Laten we eens in een heel klein - maar veel voorkomend - atoom duiken om te zien wat er aan de hand is:
Ontmoet onze knop atoom
Een van de meest elementaire bouwstenen is een knop "atoom". Binnen onze applicatie wilden we een herbruikbare knop component maken die meerdere thema's kan hebben. Het moet mogelijk zijn om het label van de knop van buitenaf op te geven en optioneel een icoontje links en/of rechts van de knop te voorzien. Een vereenvoudigde versie van het data-object van deze twig-component ziet er als volgt uit:
# src/App/Twig/Component/Atom/Button.php
namespace App\Twig\Component\Atom;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\PreMount;
#[AsTwigComponent('atoms:button', 'components/atom/buttons/button.html.twig')]
class ButtonComponent
{
public string $theme = '';
#[PreMount]
public function preMount(array $data): array
{
$resolver = new OptionsResolver();
$resolver->setDefined(['theme', ...array_keys($data)]);
$resolver->setAllowedValues('theme', ['primary' 'secondary']);
$resolver->setDefault('theme', 'primary');
return $resolver->resolve($data);
}
}
Zoals je in de bovenstaande code kan zien, neemt de twig component een thema argument aan dat straks aan de twig view gebonden wordt.
Aangezien we extra button eigenschappen (zoals disabled
, onclick
, ...) willen kunnen doorgeven van een parent component,
moeten bijkomende datasleutels ook aanvaard worden door de options resolver.
De AsTwigComponent
vertelt Symfony aan welk Twig template het object gebonden moet zijn.
Dit maakt de data en methodes van binnen het object toegankelijk in de twig view.
De Twig template voor deze knop kan er zo uitzien:
{# templates/components/atom/button.html.twig #}
{% set styles = {
'primary': {
button: 'py-3 px-4 bg-blue text-white',
iconLeft: 'mr-2 fill-white',
iconRight: 'ml-2 fill-white',
},
'secondary': {
button: 'py-3 px-4 bg-white text-black',
iconLeft: 'mr-2 fill-black',
iconRight: 'ml-2 fill-black',
}
} %}
<button
{{ attributes.defaults({
type: 'button',
class: 'group inline-flex items-center justify-center ' ~ styles[theme].button
}) }}
>
{% if block('iconLeft') is defined %}
<div class="{{ styles[theme].iconLeft }}">
{{ block('iconLeft') }}
</div>
{% endif %}
{%block children %}{%endblock%}
{% if block('iconRight') is defined %}
<div class="{{ styles[theme].iconRight }}">
{{ block('iconRight') }}
</div>
{% endif %}
</button>
Er zijn een paar dingen op te merken over deze Twig code:
Aangezien we Tailwind CSS gebruiken om onze applicaties te stylen, kunnen we met een herbruikbare twig component de stijl voor elke knop in onze applicatie centraal beheren op één plaats. Als we het ontwerp willen veranderen, worden de Tailwind klassen in de knop component veranderd op 1 plaats - en de stijl worden toegepast op elk ander component dat deze knop gebruikt. Geweldig!
Extra attributen worden doorgegeven aan de <button>
-tag met een standaard class
. Hierdoor kunnen componenten die de knop gebruiken extra classes toevoegen aan de knop, terwijl de standaard klassen toch een keer worden toegepast. Het is ook de code die extra attributen die worden doorgegeven van bovenliggende componenten (zoals disable
, onclick
, ...) koppelt aan de knop. Het standaard attribuut type
kan worden overschreven vanuit de bovenliggende component.
Er zijn 3 injecteerbare inhoudsblokken gedefinieerd: Een optioneel iconLeft
en iconRight
blok dat kan worden aangepast vanuit de parent component. En een verplicht children
blok dat bijvoorbeeld een eenvoudig tekstlabel of geavanceerde HTML bevat. Deze HTML kan worden opgebouwd als ruwe HTML of door een ander atoom.
Het gebruik van deze button component, kan er als volgt uitzien:
{% component 'atoms:button' with {
class: 'mr-4',
theme: 'primary',
disabled: false,
onclick: 'alert("hello world")' | escape('html_attr')
} %}
{% block iconLeft %}
{{ component('atoms:svg-icon', {icon: 'check',size: 'normal'}) }}
{% endblock %}
{% block children %}{{ 'Order now'|trans}}{% endblock %}
{% endcomponent %}
De bovenstaande code zal een knop weergeven in het primaire thema (blauwe knop met witte tekst). Als de gebruiker erop klikt, verschijnt een in-browser "alert" met de boodschap "hello world". De knop wordt uitsluitend weergegeven met een pictogram links van het centrale bericht "Bestel nu".
Het vergt enige gewenning aan de twig syntax, maar het resultaat lijkt erg op hoe dingen worden gedaan in een front-end framework als React. Hiermee kunnen we zowel zeer kleine atomen als zeer complexe organismen maken op basis van dezelfde principes die we je hierboven hebben laten zien. Laten we eens onderzoeken hoe we een molecule kunnen bouwen dat een atoom bevat.
Een search molecule bouwen
Stel je voor dat je een molecule wilt bouwen dat je wilt opnemen in bijvoorbeeld een lijstoverzicht organisme. Dit molecule sjabloon kan meerdere atomen combineren tot een groter geheel. Het sjabloon zou er zo uit kunnen zien:
{# templates/components/molecule/search-form.html.twig #}
{% component 'atoms:form' with {action: '/search', method: 'GET'} %}
{% block children %}
{{ component('atoms:input', {placeholder: 'Search', name: 'query'}) }}
{% component 'atoms:button' with {theme: 'primary', type: 'submit'} %}
{% block iconLeft %}
{{ component('atoms:svg-icon', {icon: 'search', size: 'normal'}) }}
{% endblock %}
{% block children %}{{ 'Search'|trans}}{% endblock %}
{% endcomponent %}
{% endblock children %}
{% endcomponent %}
Het combineert een formulier, invoer en knopatoom in een herbruikbaar formulier dat je kunt opnemen in je lijstorganisme.
De huidige implementatie resulteert in een nogal domme component die de pagina zal verversen met een zoekvraag erbij zodra deze is ingediend.
Symfony UX biedt extra hulpmiddelen zoals Turbo frames die deze actie op de achtergrond van de browser uitvoeren.
Als je meer controle wilt over de exacte formulierverzending, kun je de AsTwigComponent
veranderen in AsLiveComponent
. Dit is een andere Symfony UX library waarmee je de gegevens server-side kunt valideren en rerenderen via PHP code in plaats van via Javascript.
Als alternatief, als je deze molecule niet constant wilt rerenderen op je server, kun je deze molecule toch dynamischer maken door er een Stimulus (Javascript) controller aan toe te voegen.
Je vraagt je misschien af: Waarom zou je een formulier binnen twig maken in plaats van een Symfony formulier te gebruiken? Goede vraag! Wij vroegen ons dat zelf ook af en vonden een geweldige oplossing. Het is mogelijk om een aangepast Symfony formulierthema te maken dat elk formulierveld weergeeft als een atomair component. In een volgende blogpost vertellen we meer over deze aanpak!
Conclusie
Symfony UX biedt ons een goed ontworpen en flexibele componentenbibliotheek die we kunnen gebruiken om alle afzonderlijke componenten in onze PHP-applicaties te structureren. Het laat ons toe om onze applicaties te stylen met de Tailwind, zonder dat het op lange termijn moeilijk te onderhouden is. Het toevoegen van atomaire componenten aan onze Symfony-applicaties veranderde de manier waarop we naar Twig kijken op een onderhoudbare en stabiele manier.
Zeker, er zijn wat ruwe randjes en het de syntaxis is niet helemaal hetzelfde als je het zou doen in bijvoorbeeld React. Maar de resulterende set componenten is een echte zegen als je een grote, onderhoudbare, stabiele en consistente PHP-applicatie wilt bouwen.