Delphi WebStencils con WiRL

Luca Minuti - Sep 23 - - Dev Community

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

è possibile generare l'HTML corrispondente con questo codice:

    LPerson := TPerson.Create('Luca', 42);
    LStencil.InputFileName := LTemplateFileName;
    LStencil.AddVar('value', LPerson, False);
    LContent := LStencil.Content;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

L'implementazione è ancora più semplice:

function TPersonResource.GetPerson: TPerson;
begin
  Result := TPerson.Create('Luca', 42);
end;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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:

Demo Home

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

Tabella

Inoltre cliccando su una specifica riga si può vedere il dettaglio dell'elemento selezionato:

Demo form

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
        }
    });
Enter fullscreen mode Exit fullscreen mode

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'attributo wirl-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:

https://wirl.delphiblocks.dev/

. . . . . . . . . . . .