Template hooks

Table of contents

Introduction

A hook is a named slot in a template. Features register callbacks for it, and the template renders whatever they return, so your feature adds UI to a page without editing the page’s file.

{% for hook in get_template_hooks("notes.create.form_extra") %}
    {{ hook() | safe }}
{% endfor %}
# hooks.py of another feature
from splent_framework.hooks.template_hooks import register_template_hook

register_template_hook("notes.create.form_extra", tags_form_field)

A slot nobody fills renders nothing.


How a slot fills

Three features fill the home.section slot of the public homepage FEATURES REGISTER THE TEMPLATE RENDERS THE PAGE splent_feature_shop hooks.py, order=20 splent_feature_notes hooks.py, order=30 splent_feature_gallery hooks.py, order=40 home/index.html home.section every callback, by order, then by feature load order The homepage its own content Shop Latest notes Latest photos
The homepage knows only the slot's name. A product without one of these features simply renders one section fewer.

Hooks add up. Any number of features can register for one slot. Callbacks run sorted by their order argument (default 100), then in feature load order, and their HTML is joined.

register_template_hook("home.section", latest_notes, order=30)

Slots live in two places, with the same API.

Level Slot lives in Example
Product hooks The product’s base_template.html layout.scripts
Feature hooks A feature’s own templates home.section, notes.create.form_extra

Filling the product’s layout

The product’s base_template.html renders the layout slots, and its pyproject.toml lists them.

[tool.splent.layout]
base_template = "base_template.html"
hooks = [
    "layout.head.css",
    "layout.sidebar.top",
    "layout.anonymous_sidebar",
    "layout.authenticated_sidebar",
    "layout.sidebar.authenticated.items",
    "layout.navbar.anonymous",
    "layout.navbar.authenticated",
    "layout.footer",
    "layout.scripts",
]

A feature adds its compiled JavaScript through layout.scripts, so adding or removing the feature adds or removes its bundle.

def notes_scripts():
    return render_template("hooks/scripts.html")

register_template_hook("layout.scripts", notes_scripts)

Declare menu entries in the menu registry rather than in the sidebar and navbar slots. A shell draws the registry or the menu slots, never both. See What the application shell provides.


Opening a slot in your feature

Render get_template_hooks(name) where other features may add something. Name the slot feature.view.slot, starting with your feature’s short name.

Passing context

Callbacks can take arguments.

{% for note in notes %}
    <h5>{{ note.title }}</h5>
    {% for hook in get_template_hooks("notes.index.note_extra") %}
        {{ (hook(note) or "") | safe }}
    {% endfor %}
{% endfor %}
def note_tag_badges(note):
    tags = note.get_tags_list()
    if not tags:
        return ""
    return render_template("hooks/note_tag_badges.html", tags=tags)

register_template_hook("notes.index.note_extra", note_tag_badges)

or "" tolerates a callback that returns None.

Accepting form fields from a hook

When a hook injects a form field, the base route passes the extra data through without knowing it.

FORM_BASE_FIELDS = {"title", "content"}

@notes_bp.route("/notes/create", methods=["GET", "POST"])
@login_required
def create():
    if request.method == "POST":
        title = request.form.get("title", "").strip()
        content = request.form.get("content", "").strip()
        extra = {k: v for k, v in request.form.items() if k not in FORM_BASE_FIELDS}
        notes_service.create(user_id=current_user.id, title=title, content=content, **extra)

Without a feature filling the slot, extra is empty.

Keeping your own markup until a feature fills the slot

render_template_hooks(name) calls every callback and returns their joined output as one Markup string, empty when nobody rendered anything.

{% set hero = render_template_hooks("home.hero") %}
{% if hero %}{{ hero }}{% else %}…the page's own hero…{% endif %}

feature:contract records the slots you open with either function in your contract.


Replacing a base’s callbacks

A refiner can swap what its base puts in a slot.

from splent_framework.hooks.template_hooks import replace_template_hook

replace_template_hook("layout.navbar.authenticated", navbar_with_coupon_badge)

It removes the callbacks the caller’s base registered there, keeps everyone else’s, and adds yours.

  • The refiner must list the slot in [tool.splent.refinement.overrides] hooks, or startup stops with FeatureError. One refiner of a base may replace a given slot.
  • A replacement asked for while features load waits until every feature has loaded, so the base’s callbacks are there to replace.
  • A declared override never applied logs a warning, since the base’s callbacks stay.
  • A call from outside any feature, such as a test or a script, replaces the whole slot.

Prefer filling a slot to replacing it. A replacement claims the slot for one refiner, while any number of features can add to it.


Reference

Function Does
register_template_hook(name, func, order=100) Adds func to the slot
replace_template_hook(name, func, order=100) Swaps the base’s callbacks for func
remove_template_hook(name, func) Takes func out of the slot
get_template_hooks(name) The slot’s callbacks in order. A Jinja global
render_template_hooks(name) Their joined output, empty when nothing rendered. A Jinja global
Layout hook Where Typical use
layout.head.css <head> Feature CSS
layout.sidebar.top Sidebar, above the first section Home link
layout.anonymous_sidebar, layout.authenticated_sidebar Sidebar, signed out or in Sign-in links, admin screens
layout.sidebar.authenticated.items Sidebar, signed in, below the main items A feature’s own pages
layout.navbar.anonymous, layout.navbar.authenticated Navbar, signed out or in Sign-in button, user name
layout.footer Footer Footer links
layout.scripts Before </body> Feature JavaScript bundles

See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es