Skip to content

De website laten aanvoelen als een SPA

De website is snel, maar elke klik laadt nog steeds een volledige HTML-pagina opnieuw. Het traditionele antwoord is om een Single-Page Application (SPA) te bouwen: een JavaScript-applicatie, geschreven in een andere stack, die met de API praat. Dat betekent een tweede applicatie om te ontwikkelen, te beveiligen, te deployen en synchroon te houden met de hoofdapplicatie.

Symfony heeft een ander antwoord. Symfony UX is een initiatief dat de SPA-ervaring naar server-gerenderde applicaties brengt: blijf Twig-templates en Symfony-controllers schrijven en voeg alleen JavaScript toe waar het waarde toevoegt.

Symfony UX ontdekken

Het goede nieuws? We gebruiken Symfony UX al sinds de allereerste stap van dit boek. Het webapp-skelet wordt geleverd met twee van zijn packages: symfony/stimulus-bundle en symfony/ux-turbo. Bekijk importmap.php:

1
2
3
4
5
6
return [
    'app' => ['path' => './assets/app.js', 'entrypoint' => true],
    '@hotwired/stimulus' => ['version' => '3.2.2'],
    '@symfony/stimulus-bundle' => ['path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js'],
    '@hotwired/turbo' => ['version' => '8.0.23'],
];

Stimulus is een klein JavaScript-framework dat JavaScript-controllers verbindt met HTML-elementen via data- attributen. Het assets/stimulus_bootstrap.js bestand start de Stimulus-applicatie en registreert automatisch elke controller die in de assets/controllers/ directory is opgeslagen.

Turbo bouwt voort op deze basis om navigatie direct te maken.

Direct navigeren met Turbo

Turbo Drive is al die tijd stilletjes aan het werk geweest: het onderschept elke klik op een link en elke formulierinzending, haalt de pagina op de achtergrond op en vervangt de <body> zonder een volledige paginaherlading. De browser houdt dezelfde JavaScript en CSS in leven, waardoor navigatie direct aanvoelt.

Open de developer tools van de browser, ga naar het "Network" tabblad en klik door de website: paginawijzigingen zijn fetch requests, geen volledige documentladingen.

Turbo vraagt slechts één ding terug: correcte HTTP-semantiek voor formulieren. Een geslaagde inzending moet redirecten (de onze doet dat, dankzij redirectToRoute()), en een mislukte moet een 4xx statuscode gebruiken. Symfony handelt dat laatste automatisch af: wanneer een ingediend formulier ongeldig is, antwoordt render() met een 422 Unprocessable Entity statuscode.

Een deel van een pagina bijwerken met Turbo Frames

Turbo Drive voorkomt volledige paginaherladingen. Turbo Frames gaan een stap verder: ze laten een fragment van de pagina onafhankelijk van de rest navigeren.

De lijst met reacties is een perfecte kandidaat. Bij het doorbladeren van reactiepagina's via de "Vorige" en "Volgende" links zou alleen de lijst moeten veranderen; het opnieuw renderen van de header, het formulier en de footer is verspilde moeite. Verpak de reacties in een frame:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--- i/templates/conference/show.html.twig
+++ w/templates/conference/show.html.twig
@@ -17,3 +17,4 @@
     <div class="row">
         <div class="col-12 col-lg-8">
+            <turbo-frame id="comments" data-turbo-action="advance">
             {% if comments|length > 0 %}
@@ -56,5 +57,6 @@
                 No comments have been posted yet for this conference.
             </div>
         {% endif %}
+            </turbo-frame>
         </div>
         <div class="col-12 col-lg-4">

Links binnen een <turbo-frame> element werken alleen dat frame bij: bij het klikken op "Volgende" haalt Turbo de pagina op de achtergrond op en vervangt het overeenkomende frame, waarbij de rest van de pagina onaangeroerd blijft.

Het data-turbo-action="advance" attribuut promoveert de frame-navigatie tot een volledig bezoek: de URL in de adresbalk wordt bijgewerkt (inclusief de offset query-parameter), zodat gepagineerde reacties deelbaar blijven en de terug-knop van de browser werkt zoals verwacht.

De reactiefoto voorvertonen met Stimulus

Tijd om onze eerste regels JavaScript van het hele boek te schrijven. Bezoekers die een foto insturen, kunnen deze niet zien voordat ze de reactie plaatsen. Laten we een voorvertoning tonen wanneer ze een bestand selecteren.

Maak een Stimulus-controller (de bestandsnaam bepaalt de controllernaam, photo-preview):

assets/controllers/photo_preview_controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    show() {
        const [photo] = this.element.files;
        if (!photo) {
            this.preview?.remove();
            this.preview = null;

            return;
        }

        if (!this.preview) {
            this.preview = document.createElement('img');
            this.preview.className = 'img-thumbnail mt-2';
            this.preview.style.maxHeight = '150px';
            this.element.insertAdjacentElement('afterend', this.preview);
        }

        this.preview.src = URL.createObjectURL(photo);
    }
}

Dat is alle JavaScript die we nodig hebben. Stimulus ontdekt en registreert de controller automatisch. Verbind hem met het fotoveld van het reactieformulier via data- attributen:

1
2
3
4
5
6
7
8
9
10
11
12
13
--- i/src/Form/CommentType.php
+++ w/src/Form/CommentType.php
@@ -26,6 +26,10 @@ class CommentType extends AbstractType
             ->add('photo', FileType::class, [
                 'required' => false,
                 'mapped' => false,
+                'attr' => [
+                    'data-controller' => 'photo-preview',
+                    'data-action' => 'change->photo-preview#show',
+                ],
                 'constraints' => [
                     new Image(maxSize: '1024k')
                 ],

Het data-controller attribuut bindt de controller aan de bestandsinvoer, en data-action roept de show() methode aan telkens wanneer de invoerwaarde verandert. Selecteer een foto op een conferentiepagina en geniet van de voorvertoning.

Stimulus-controllers werken hand in hand met Turbo: omdat Drive en Frames de pagina bijwerken zonder deze opnieuw te laden, worden controllers automatisch verbonden en losgekoppeld wanneer elementen de DOM binnenkomen en verlaten.

Hoe zit het met mobiele applicaties?

We hebben de SPA-ervaring gekregen zonder een tweede applicatie te bouwen: geen aparte JavaScript-stack, geen tweede webserver, geen CORS-configuratie, niets nieuws om te deployen.

En als je ooit een native mobiele applicatie nodig hebt, is de API die in de vorige stap is gemaakt het juiste startpunt: deze is publiek, gedocumenteerd en framework-agnostisch. De website en de mobiele applicatie kunnen onafhankelijk van elkaar evolueren terwijl ze dezelfde backend delen.

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version