Become a member!

TemplatePro: template inheritance, the most powerful feature many forget

🌐
This article is also available in other languages:
🇮🇹 Italiano  •  🇪🇸 Español  •  🇩🇪 Deutsch

A few days ago I was at a client’s site for a consulting session on a web application built with DMVCFramework, TemplatePro and HTMX. We were doing a code review when I opened the templates/ folder and stopped.

Twenty HTML files. In each of those files all twenty dialogs were present - copied and pasted. The outer structure was always the same: header with title, form, two buttons. The form fields and the destination endpoint changed from dialog to dialog, but the HTML shell was identical, repeated 20 times per file, across 20 files. Four hundred markup blocks for what was logically a single component with variants.

I asked: “Why did you duplicate all the dialogs in every file?”

The junior developer, raised on React, answered: “Sure, with React I could have done better…”

“In what way?” I asked.

“You know, these old languages, like HTML and CSS, force you to repeat yourself…”

I kept quiet. I counted to a hundred. Then I moved on.

“Before we continue, let’s talk about the most powerful feature of TemplatePro.”

It is not the first time. Cases like this happen to me with a certain frequency, and every time I find myself re-explaining the same things. That is what convinced me to write this post - even though the official documentation covers it extensively, and even though there is a guide of over 200 pages that covers TemplatePro in every detail. Hopefully a narrative post, starting from a real case, will help fix the concept in a different way.

TL;DR: In TemplatePro, {{extends "base.html"}} inherits a parent template; {{block "name"}} defines an overridable region (like a Delphi virtual method); {{inherited}} calls the parent’s implementation. Inheritance is multi-level without limits (A←B←C). Reusable partials are included with {{include "file.html"}} and variable mapping for isolated scope.

Template inheritance: the concept

Template inheritance is the mechanism by which a template can extend another template, inherit its structure and override only the parts that change. The idea comes from Jinja2 and other modern template engines. In TemplatePro it is realised with three constructs:

  • {{extends "other_template.html"}} - declares that this template extends another
  • {{block "name"}}...{{endblock}} - defines a replaceable region
  • {{inherited}} - includes the content of the parent’s block without overriding it

If you are a Delphi programmer, you already have all the mental model needed to understand this mechanism: every {{block}} is exactly like a virtual method. The parent template declares it with a default implementation. The child template can override it - or not, in which case the parent’s implementation applies. The grandchild template can in turn override the child’s. And {{inherited}} corresponds exactly to the inherited keyword in Delphi: it calls the implementation from the upper level in the chain and adds something to it.

Delphi - virtual methods:

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 - blocks:

{{# _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}}

The structure is identical. Only the domain changes: classes and methods on one side, templates and blocks on the other.

Case 1: single inheritance - shared layout

The most common use is separating the common page structure (navbar, footer, CSS, JS) from the part that changes view by view.

_layout.html - the parent template:

<!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 - the child template:

{{extends "_layout.html"}}

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

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

When TemplatePro renders customers.html, it takes the structure of _layout.html and replaces the blocks with what is defined in the child. Blocks not redefined (extra_css, extra_scripts) use the parent’s content - in this case empty.

One rule to keep in mind: content outside blocks in the child template is ignored. If you write free HTML in customers.html outside a {{block}}, it will not be rendered.

{{inherited}}: extending without overriding

Sometimes you do not want to replace the parent’s block - you want to add something to it. That is what {{inherited}} is for.

Suppose the base layout already includes the standard meta tags in the <head>, and for the reports page we want to add Chart.js without losing the common tags:

_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}} inserts the parent block’s content, then the new tags are added. The rendered result is:

<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>

No duplication. The reports page inherits everything from the parent and adds only what it needs.

Case 2: multi-level inheritance - A <- B <- C

And here we get to the part the client did not know at all.

TemplatePro supports inheritance at unlimited depth. A template B can extend A, and a template C can extend B. The resolution chain traverses all levels from top to bottom.

This is enormously useful in real applications, where you typically have:

  • a base layout shared across the entire application
  • a section layout that adds specific elements (e.g. the admin area sidebar)
  • the concrete pages that extend the section layout

_base.html - the absolute root (level 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 - the admin area layout, extends _base.html (level 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 knows nothing about the concrete pages. It only knows that a sidebar exists, a navbar exists, and there is a content slot to fill.

customers.html - the customers page, extends _admin.html (level 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}}

Three files. Three distinct responsibilities. customers.html does not know how the sidebar works. _admin.html does not know what data the concrete page will display. _base.html does not even know an admin area exists.

This is exactly how it should work.

Back to the twenty dialogs

Once the concept of inheritance is clear, the problem of the twenty dialogs is solved with include + variable mapping - the complementary feature to inheritance for componentising reusable partials.

The native HTML element for dialogs is <dialog>, supported by all modern browsers. No library needed: showModal() to open it, close() to close it via JavaScript.

In the client’s concrete case, the twenty dialogs shared the outer structure - the <dialog> tag, a title, the form, the two buttons - but everything inside changed: the endpoint action, the dialog title, and the number and nature of the form fields themselves.

include with variable mapping is not enough when the content to vary is structured HTML: you cannot pass markup blocks as a parameter. Template inheritance is needed again.

The solution is to create a base partial for the dialog shell, and a child partial for each dialog type that inherits its structure and overrides the variable blocks.

partials/_dialog_base.html - the shared shell (level 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>

Every {{block}} is a virtual method: the child overrides it, or uses the parent’s default value.

partials/delete_customer_dialog.html - the delete dialog (level 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 - the edit dialog (level 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}}

In the {{block "content"}} of customers.html, each row includes its specific partials:

{{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}}

The twenty copy-paste dialogs become: one base shell, one child partial per type, and one include line per occurrence. If the shell structure changes - an icon in the header, an ARIA attribute, an additional hidden field - only _dialog_base.html needs to be touched.

Data scope propagates normally through include: child partials see customer.id, customer.company_name and everything available at the point of inclusion. The {{set dialog_id}} before the include passes the value the base shell needs.

The complete mental model

I left this diagram with the client:

_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

Five levels. Each one knows only what it is responsible for. When something changes, only one file needs to be touched.

Template inheritance is not an advanced feature to use only in large projects. It is the correct way to structure any non-trivial web application with TemplatePro. The twenty copied dialogs were the visible symptom of a deeper problem: the absence of this structure.


TemplatePro is open source (Apache 2.0). The official documentation, including the complete section on template inheritance, is available here. The repository is on GitHub.


Want to go deeper?

I have written a complete guide to TemplatePro: TemplatePro 1.1 - The Definitive Guide - 209 pages, 12 complete projects. It covers everything we have seen here, and much more: macros, custom filters, internationalisation, integration with WebBroker, compiled template caching, production deployment.

The book is available in four languages on Patreon:

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


Happy coding!
Daniele Teti

Comments

comments powered by Disqus