TemplatePro: template inheritance, la feature più potente che molti dimenticano
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 vedonocustomer.id,customer.company_namee tutto ciò che è disponibile nel punto di inclusione. Il{{set dialog_id}}prima dell’includepassa 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