Become a member!

TemplatePro: template inheritance, la feature più potente che molti dimenticano

🌐
Questo articolo è disponibile anche in altre lingue:
🇬🇧 English  •  🇪🇸 Español  •  🇩🇪 Deutsch

Qualche giorno fa ero da un cliente per una sessione di consulenza su un’applicazione web costruita con DMVCFramework, TemplatePro e HTMX. Stavamo facendo una review del codice quando apro la cartella templates/ e mi fermo.

Venti file HTML. In ognuno di quei file c’erano tutte e venti le dialog - copiate e incollate. La struttura esterna era sempre la stessa: header con titolo, form, due pulsanti. I campi della form e l’endpoint di destinazione cambiavano da dialog a dialog, ma la shell HTML era identica, ripetuta 20 volte per file, per 20 file. Quattrocento blocchi di markup per quello che logicamente era un componente con varianti.

Chiedo: “Come mai avete duplicato tutte le dialog in ogni file?”

Il programmatore junior, cresciuto a pane e React, risponde: “Certo, con React avrei potuto fare meglio…”

“In che modo?” chiedo.

“Sai, questi linguaggi vecchi, come HTML e CSS, ti obbligano a ripeterti…”

Mi sono taciuto. Ho contato fino a cento. Poi sono andato avanti.

“Prima di andare avanti, parliamo della feature più potente di TemplatePro.”

Non è la prima volta. Casi come questo mi capitano con una certa frequenza, e ogni volta mi ritrovo a rispiegare le stesse cose. È quello che mi ha convinto a scrivere questo post - nonostante la documentazione ufficiale ne parli ampiamente, e nonostante esista una guida di oltre 200 pagine che copre TemplatePro in ogni dettaglio. Speriamo che un post narrativo, che parte da un caso reale, aiuti a fissare il concetto in modo diverso.

In breve: In TemplatePro, {{extends "base.html"}} eredita un template padre; {{block "name"}} definisce una regione sovrascrivibile (come un metodo virtuale Delphi); {{inherited}} chiama l’implementazione del padre. L’ereditarietà è multi-livello senza limiti (A←B←C). I partial riutilizzabili si includono con {{include "file.html"}} e variable mapping per scope isolato.

Template inheritance: il concetto

La template inheritance è il meccanismo con cui un template può estendere un altro template, ereditarne la struttura e sovrascrivere solo le parti che cambiano. L’idea viene da Jinja2 e da altri motori di template moderni. In TemplatePro si realizza con tre costrutti:

  • {{extends "other_template.html"}} - dichiara che questo template estende un altro
  • {{block "name"}}...{{endblock}} - definisce una regione sostituibile
  • {{inherited}} - include il contenuto del blocco del padre senza sovrascriverlo

Se sei un programmatore Delphi, hai già tutto il modello mentale necessario per capire questo meccanismo: ogni {{block}} è esattamente come un metodo virtuale. Il template padre lo dichiara con un’implementazione di default. Il template figlio può sovrascriverlo - oppure no, nel qual caso vale l’implementazione del padre. Il template nipote può a sua volta sovrascrivere quello del figlio. E {{inherited}} corrisponde esattamente alla parola chiave inherited di Delphi: chiama l’implementazione del livello superiore nella catena, e ci aggiunge qualcosa.

Delphi - metodi virtuali:

type
  TBaseView = class
  public
    procedure RenderTitle; virtual;
    procedure RenderContent; virtual;
  end;

  TChildView = class(TBaseView)
  public
    procedure RenderContent; override; // overridden
    // RenderTitle is NOT overridden: parent's implementation applies
  end;

implementation

procedure TBaseView.RenderTitle;
begin
  WriteLn('<title>My App</title>');
end;

procedure TBaseView.RenderContent;
begin
  WriteLn('<!-- default content -->');
end;

procedure TChildView.RenderContent;
begin
  inherited; // calls TBaseView.RenderContent - optional
  WriteLn('<!-- adds its own content -->');
end;

TemplatePro - blocchi:

{{# _base.html #}}
<title>{{block "title"}}My App{{endblock}}</title>

{{block "content"}}
  <!-- default content -->
{{endblock}}
{{# child.html #}}
{{extends "_base.html"}}

{{# "title" is not overridden: parent content applies #}}

{{block "content"}}
  {{inherited}}  {{# calls parent block - optional #}}
  <!-- adds its own content -->
{{endblock}}

La struttura è identica. Cambia solo il dominio: classi e metodi da una parte, template e blocchi dall’altra.

Caso 1: ereditarietà singola - layout condiviso

L’uso più comune è separare la struttura comune della pagina (navbar, footer, CSS, JS) dalla parte che cambia view per view.

_layout.html - il template padre:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{block "title"}}My App{{endblock}}</title>
  <link href="/assets/bootstrap/css/bootstrap.min.css" rel="stylesheet">
  {{block "extra_css"}}{{endblock}}
</head>
<body>
  <nav class="navbar navbar-dark bg-dark">
    <div class="container">
      <a class="navbar-brand" href="/">My App</a>
    </div>
  </nav>

  <div class="container mt-4">
    {{block "content"}}{{endblock}}
  </div>

  <script src="/assets/bootstrap/js/bootstrap.bundle.min.js"></script>
  {{block "extra_scripts"}}{{endblock}}
</body>
</html>

customers.html - il template figlio:

{{extends "_layout.html"}}

{{block "title"}}Customers{{endblock}}

{{block "content"}}
<h2>Customer list</h2>
<table class="table table-striped">
  ...
</table>
{{endblock}}

Quando TemplatePro renderizza customers.html, prende la struttura di _layout.html e sostituisce i blocchi con quanto definito nel figlio. I blocchi non ridefiniti (extra_css, extra_scripts) usano il contenuto del padre - in questo caso vuoto.

Una sola regola da tenere a mente: il contenuto fuori dai blocchi nel template figlio viene ignorato. Se scrivi HTML libero in customers.html al di fuori di un {{block}}, non verrà renderizzato.

{{inherited}}: estendere senza sovrascrivere

A volte non vuoi sostituire il blocco del padre - vuoi aggiungerci qualcosa. Per questo esiste {{inherited}}.

Supponiamo che il layout base includa già i meta tag standard nell’<head>, e che per la pagina dei report vogliamo aggiungere Chart.js senza perdere i tag comuni:

_layout.html:

<head>
  {{block "head"}}
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link href="/assets/bootstrap/css/bootstrap.min.css" rel="stylesheet">
  {{endblock}}
</head>

reports.html:

{{extends "_layout.html"}}

{{block "head"}}
{{inherited}}
<link href="/assets/css/charts.css" rel="stylesheet">
<script src="/assets/js/chartjs.min.js"></script>
{{endblock}}

{{inherited}} inserisce il contenuto del blocco padre, poi vengono aggiunti i nuovi tag. Il risultato renderizzato è:

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link href="/assets/bootstrap/css/bootstrap.min.css" rel="stylesheet">
  <link href="/assets/css/charts.css" rel="stylesheet">
  <script src="/assets/js/chartjs.min.js"></script>
</head>

Nessuna duplicazione. La pagina dei report eredita tutto il padre e aggiunge solo quello di cui ha bisogno.

Caso 2: ereditarietà multipla - A ← B ← C

Ed eccoci alla parte che il cliente non conosceva affatto.

TemplatePro supporta l’ereditarietà a profondità illimitata. Un template B può estendere A, e un template C può estendere B. La catena di risoluzione percorre tutti i livelli dall’alto verso il basso.

Questo è enormemente utile nelle applicazioni reali, dove tipicamente hai:

  • un layout base uguale per tutta l’applicazione
  • un layout di sezione che aggiunge elementi specifici (es. la sidebar dell’area admin)
  • le pagine concrete che estendono il layout di sezione

_base.html - la radice assoluta (livello A):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{block "title"}}App{{endblock}}</title>
  {{block "head_extra"}}{{endblock}}
  <link href="/assets/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  {{block "body"}}{{endblock}}
  <script src="/assets/bootstrap/js/bootstrap.bundle.min.js"></script>
  {{block "scripts_extra"}}{{endblock}}
</body>
</html>

_admin.html - il layout dell’area amministrativa, estende _base.html (livello B):

{{extends "_base.html"}}

{{block "head_extra"}}
<link href="/assets/css/admin.css" rel="stylesheet">
{{endblock}}

{{block "body"}}
<div class="d-flex">
  <nav class="sidebar bg-dark text-white" style="min-height:100vh; width:220px">
    <ul class="nav flex-column p-3">
      <li><a class="nav-link text-white" href="/admin/customers">Customers</a></li>
      <li><a class="nav-link text-white" href="/admin/orders">Orders</a></li>
      <li><a class="nav-link text-white" href="/admin/reports">Reports</a></li>
    </ul>
  </nav>
  <div class="flex-grow-1">
    <nav class="navbar navbar-light bg-light border-bottom px-3">
      <span class="navbar-text">Welcome back, {{:username}}</span>
    </nav>
    <div class="container-fluid p-4">
      {{block "content"}}{{endblock}}
    </div>
  </div>
</div>
{{endblock}}

_admin.html non sa nulla delle pagine concrete. Sa solo che esiste una sidebar, una navbar, e uno slot content da riempire.

customers.html - la pagina clienti, estende _admin.html (livello C):

{{extends "_admin.html"}}

{{block "title"}}Customers - Admin{{endblock}}

{{block "content"}}
<h2>Customer list</h2>
<p class="text-muted">{{:total_customers}} registered customers</p>

<table class="table table-striped table-hover">
  <thead>
    <tr>
      <th>Code</th><th>Company name</th><th>City</th><th></th>
    </tr>
  </thead>
  <tbody>
    {{for customer in customers}}
    <tr>
      <td><code>{{:customer.code}}</code></td>
      <td>{{:customer.company_name}}</td>
      <td>{{:customer.city}}</td>
      <td>
        <button onclick="document.getElementById('deleteDialog-{{:customer.id}}').showModal()">
          Delete
        </button>
      </td>
    </tr>
    {{endfor}}
  </tbody>
</table>
{{endblock}}

Tre file. Tre responsabilità distinte. customers.html non sa come funziona la sidebar. _admin.html non sa quali dati mostrerà la pagina concreta. _base.html non sa nemmeno che esiste un’area admin.

Questo è esattamente come dovrebbe funzionare.

Torniamo alle venti dialog

Una volta che il concetto di inheritance è chiaro, il problema delle venti dialog si risolve con include + variable mapping - la feature complementare all’inheritance per componentizzare i partial riutilizzabili.

L’elemento HTML nativo per i dialog è <dialog>, supportato da tutti i browser moderni. Nessuna libreria necessaria: showModal() per aprirlo, close() per chiuderlo via JavaScript.

Nel caso concreto del cliente, le venti dialog condividevano la struttura esterna - il tag <dialog>, un titolo, la form, i due pulsanti - ma dentro cambiava tutto: l’action dell’endpoint, il titolo della dialog, e il numero e la natura dei campi della form stessa.

include con variable mapping non basta quando il contenuto da variare è HTML strutturato: non puoi passare blocchi di markup come parametro. Serve di nuovo la template inheritance.

La soluzione è creare un partial base per la shell della dialog, e un partial figlio per ogni tipo di dialog che ne eredita la struttura e sovrascrive i blocchi variabili.

partials/_dialog_base.html - la shell condivisa (livello A):

<dialog id="{{:dialog_id}}">
  <h3>{{block "dialog_title"}}{{endblock}}</h3>
  <form method="post" action="{{block "form_action"}}{{endblock}}">
    <div class="dialog-body">
      {{block "form_fields"}}{{endblock}}
    </div>
    <div class="dialog-footer">
      <button type="button" onclick="this.closest('dialog').close()">Cancel</button>
      <button type="submit" class="btn-{{block "submit_style"}}primary{{endblock}}">
        {{block "submit_label"}}Confirm{{endblock}}
      </button>
    </div>
  </form>
</dialog>

Ogni {{block}} è un metodo virtuale: il figlio lo sovrascrive, oppure usa il valore di default del padre.

partials/delete_customer_dialog.html - la dialog di eliminazione (livello B):

{{extends "partials/_dialog_base.html"}}

{{block "dialog_title"}}Delete customer{{endblock}}
{{block "form_action"}}/admin/customers/{{:customer.id}}/delete{{endblock}}
{{block "submit_style"}}danger{{endblock}}
{{block "submit_label"}}Delete permanently{{endblock}}

{{block "form_fields"}}
<p>You are about to delete <strong>{{:customer.company_name}}</strong>.</p>
<p>This action cannot be undone.</p>
<input type="hidden" name="customer_id" value="{{:customer.id}}">
{{endblock}}

partials/edit_customer_dialog.html - la dialog di modifica (livello B):

{{extends "partials/_dialog_base.html"}}

{{block "dialog_title"}}Edit customer{{endblock}}
{{block "form_action"}}/admin/customers/{{:customer.id}}/edit{{endblock}}
{{block "submit_label"}}Save changes{{endblock}}

{{block "form_fields"}}
<div class="form-group">
  <label>Company name</label>
  <input type="text" name="company_name"
         value="{{:customer.company_name}}" class="form-control">
</div>
<div class="form-group">
  <label>City</label>
  <input type="text" name="city"
         value="{{:customer.city}}" class="form-control">
</div>
{{endblock}}

Nel {{block "content"}} di customers.html, ogni riga include i suoi partial specifici:

{{for customer in customers}}
<tr>
  <td><code>{{:customer.code}}</code></td>
  <td>{{:customer.company_name}}</td>
  <td>
    <button onclick="document.getElementById('editDialog-{{:customer.id}}').showModal()">
      Edit
    </button>
    <button onclick="document.getElementById('deleteDialog-{{:customer.id}}').showModal()">
      Delete
    </button>
  </td>
</tr>

{{set dialog_id := @("editDialog-" + customer.id)}}
{{include "partials/edit_customer_dialog.html"}}

{{set dialog_id := @("deleteDialog-" + customer.id)}}
{{include "partials/delete_customer_dialog.html"}}

{{endfor}}

Le venti dialog copy-paste diventano: una shell base, un partial figlio per tipo, e una riga di include per ogni occorrenza. Se la struttura della shell cambia - un’icona nell’header, un attributo ARIA, un campo hidden aggiuntivo - si tocca _dialog_base.html soltanto.

Lo scope dei dati si propaga normalmente nell’include: i partial figli vedono customer.id, customer.company_name e tutto ciò che è disponibile nel punto di inclusione. Il {{set dialog_id}} prima dell’include passa il valore che serve alla shell base.

Il modello mentale completo

Al cliente ho lasciato questo schema:

_base.html                              ← absolute base (DOCTYPE, global CSS, JS)
  └── _admin.html                       ← section layout (sidebar, admin navbar)
        └── customers.html              ← concrete page (content only)
              └── partials/
                  ├── _dialog_base.html            ← dialog shell (virtual blocks)
                  ├── delete_customer_dialog.html  ← override: title, action, fields
                  └── edit_customer_dialog.html    ← override: title, action, fields

Cinque livelli. Ognuno sa solo quello che gli compete. Quando qualcosa cambia, si tocca un file solo.

La template inheritance non è una feature avanzata da usare solo nei progetti grossi. È il modo corretto di strutturare qualsiasi applicazione web non banale con TemplatePro. Le venti dialog copiate erano il sintomo visibile di un problema più profondo: l’assenza di questa struttura.


TemplatePro è open source (Apache 2.0). La documentazione ufficiale, inclusa la sezione completa sulla template inheritance, è disponibile qui. Il repository è su GitHub.


Vuoi andare più in profondità?

Ho scritto una guida completa su TemplatePro: TemplatePro 1.1 - The Definitive Guide - 209 pagine, 12 progetti completi. Copre tutto quello che abbiamo visto qui, e molto di più: macro, filtri personalizzati, internazionalizzazione, integrazione con WebBroker, caching dei template compilati, deployment in produzione.

Il libro è disponibile in quattro lingue su Patreon:

🇮🇹 Italiano  •  🇬🇧 English  •  🇩🇪 Deutsch  •  🇪🇸 Español


Buon coding!
Daniele Teti

Comments

comments powered by Disqus