Da pochi giorni è uscita la versione 12.2 di Delphi, nonostante sia una dot release, dove il focus è principalmente sulla risoluzione dei bug, ci sono diverse novità interessanti. Le principali sono il supporto per vari sistemi di AI generativa e l'introduzione di una libreria denominata WebStenclis per la creazione HTML attraverso dei template.
In questo articolo volevo parlare di quest'ultima, in particola di come integrarla con WiRL. In realtà, con qualche accorgimento, il contenuto di questo articolo può essere adattato anche ad altre tecnologie.
WebStencils
Se volete un guida approfondita su WebStencils vi consiglio l'ottimo articolo di Marco Cantù, ma di base si tratta di un sistema per creare del codice HTML in base ad un template da applicare ad un oggetto Delphi.
Supponendo di avere un oggetto TPerson
fatto in questo modo:
TPerson = class
private
FName: string;
FAge: Integer;
public
property Name: string read FName write FName;
property Age: Integer read FAge write FAge;
constructor Create(const AName: string; AAge: Integer);
end;
e un template come questo (notare i valori che cominciano con @
):
<section id="output1">
<div>Name: <strong>@value.name</strong></div>
<div>Age: <strong>@value.age</strong></div>
</section>
è possibile generare l'HTML corrispondente con questo codice:
LPerson := TPerson.Create('Luca', 42);
LStencil.InputFileName := LTemplateFileName;
LStencil.AddVar('value', LPerson, False);
LContent := LStencil.Content;
I template non si limitano a fare una sorta di "search & replace" ma possono avere anche del codice condizionale come:
- Loop: per ripetere parti del template in caso di oggetto che contengono vari elementi come dataset o liste;
- If: per includere una parte del template in base a qualche condizione;
- Include: per includere porzioni di HTML;
- loginrequired: per gestire le autorizzazioni;
- e altro ancora.
WiRL
Ma come possiamo usare questa tecnologia con WiRL? Lo scopo di WiRL è quello di implementare una API ReST e in questo caso non stiamo affatto parlando di ReST. Certo, nessuno ci impedisce di restituire un HTML generato con WebStelcils da una risorsa ReST, ma non avrebbe molto senso.
A pensarci bene però i WebStencils, nell'ottica di WiRL, sono in pratica del Message Body Writer, cioè dei moduli che sono in grado di serializzare degli oggetti in un determinato formato, in questo caso HTML.
L'idea sarebbe quella di creare risorse WiRL in maniera tradizionale, quindi restituendo oggetti e trasformarli in HTML tramite un Message Body Writer specifico che utilizzi WebStencils.
La risorsa
La risorsa che andremo a creare sarà quindi una normalissima risorsa WiRL che restituisce un oggetto TPerson
:
[Path('/person')]
TPersonResource = class(TObject)
public
[GET]
[Produces(TMediaType.TEXT_HTML)]
[Produces(TMediaType.APPLICATION_JSON)]
function GetPerson(): TPerson;
end;
L'implementazione è ancora più semplice:
function TPersonResource.GetPerson: TPerson;
begin
Result := TPerson.Create('Luca', 42);
end;
Notate che nella dichiarazione abbiamo indicato che l'oggetto TPerson
può essere serializzato sia un JSON che HTML (WiRL sceglierà uno o altro in base al header Accept della richiesta). Ora, mentre per il JSON non ci sono problemi, dato che WiRL lo supporta nativamente, per HTML dobbiamo dirgli come comportarsi. Ed è qui che entra in gioco il Message Body Writer.
Message Body Writer
Vediamo subito il codice:
procedure TWiRLStencilsWriter.WriteTo(const AValue: TValue;
const AAttributes: TAttributeArray; AMediaType: TMediaType;
AHeaders: IWiRLHeaders; AContentStream: TStream);
var
LStencilName: string;
LStencil: TWebStencilsProcessor;
LContent: string;
LBuffer: TBytes;
begin
inherited;
// Verifica se ci è stato passato un template specifico nell'header 'x-stencil'
LStencilName := FRequest.Headers['x-stencil'];
// altrimenti ne usa uno di default (nome della classe senza 'T')
if LStencilName = '' then
LStencilName := Copy(AValue.AsObject.ClassName, 2, 100).ToLower + '.html';
LStencil := TWebStencilsProcessor.Create(nil);
try
// Carica il template
LStencil.InputFileName := TPath.Combine(ExtractFileDir(ParamStr(0)), 'www', LStencilName);
// Carica l'oggetto
LStencil.AddVar('value', AValue.AsObject, False);
// Applica il template
LContent := LStencil.Content;
// Mette il risultato nello stream della risposta HTTP
LBuffer := TEncoding.UTF8.GetBytes(LContent);
AContentStream.WriteBuffer(LBuffer[0], Length(LBuffer));
finally
LStencil.Free;
end;
end;
L'idea è che per serializzare l'oggetto TPerson
WiRL deve usare un template di nome person.html
oppure il chiamante può indicare un template specifico tramite un header custom chiamato x-stencil
. Il Message Body Writer non fa nient'altro che caricare il template e l'oggetto da serializzare nel WebStencils e chiedergli il risultato.
Come si può vedere il codice del Message Body Writer è molto semplice.
Questo uso di template e WebStencils può integrarsi con qualsiasi progetto Web ma è particolarmente interessante accoppiato con HTMX.
HTMX
Anche in questo caso non mi voglio soffermare troppo su HTMX, trovate diversi post di Embarcadero sull'argomento qui. Ma l'idea di base è quella di estendere l'HTML e riuscire a creare pagine dinamiche che interagiscono col server senza l'uso di JavaScript, quindi con un approccio puramente dichiarativo.
Per esempio il seguente frammento di HTMX alla pressione del pulsante chiamerà l'URL /clicked
e sostituira il nodo con nome outerHTML
con quanto arrivato dal server.
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
</button>
Quindi si presume che il server generi frammenti di HTML, che è proprio quello che abbiamo fatto con i WebStencils.
Qui trovate il progetto di esempio con i sorgenti completi: https://github.com/lminuti/WebStencilsDemo
Nel progetto troverete una semplice pagina di esempio (ho usato PicoCSS giusto per avere un aspetto un minimo decente), dove ci sono tre DEMO:
Il primo contiene un pulsante fatto così:
<button hx-get="rest/default/person"
hx-trigger="click"
hx-target="#output1"
hx-swap="outerHTML">
Click Me!
</button>
In questo caso al click
del pulsante viene chiamata la risorsa rest/default/person
e il risultato va a sostituire l'elemento HTML chiamato output1
. Il secondo è identico ma usa un template personalizzato:
<button hx-get="rest/default/person"
wirl-stencil="person_it.html"
hx-trigger="click"
hx-target="#output2"
hx-swap="outerHTML">
Click Me!
</button>
Infatti con l'attributo wirl-stencil
inviamo a WiRL in nome del template da usare. Il terzo è un po' più complesso, infatti il template permette di trasformare un dataset in una tabella HTML:
Inoltre cliccando su una specifica riga si può vedere il dettaglio dell'elemento selezionato:
Configurazione
Per ottenere questa integrazione tra HTMX e WiRL serve una piccola configurazione lato HTMX che è la seguente:
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.headers ['accept'] = 'text/html'; // force text/html
if (evt.target.getAttribute('wirl-stencil')) {
evt.detail.headers['x-stencil'] = evt.target.getAttribute('wirl-stencil'); // add x-stencil header
}
});
In pratica stiamo dicendo a HTMX che ogni volta che esegue una chiamata Ajax deve aggiungere due header:
-
accept
: che indica a WiRL di restituire la risorsa serializzata in HTML. Questo perché WiRL di default la serializzerebbe in JSON; -
x-stencil
: in questo modo se abbiamo usato l'attributowirl-stencil
sul pulsante HTMX invia il nome dello template attraveso questo header.
Conclusioni
Penso che questo uso degli stencils con WiRL sia interessante e soprattutto che dimostri come è semplice personalizzare WiRL per fargli fare anche cose per cui chiaramente non è nato.
Trovate tutto il codice mostrato qui:
https://github.com/lminuti/WebStencilsDemo
Per poterlo compilare e provare è necessario scaricare e installare WiRL:
https://github.com/delphi-blocks/WiRL
La documentazione ufficiale di WiRL: