Skip to main content
Version: 4.xx.xx

UCHTML Jinja2 Templating

What is Jinja2?

Jinja2 is a modern, designer-friendly templating engine for Python. It is used to generate dynamic web content, such as HTML, XML, or other text-based formats. Jinja2 makes it easy to separate business logic from presentation logic by providing a rich set of features including variable interpolation, control structures, and template inheritance. In the context of UCHTML, Jinja2 works together with HTML to greatly enhance the expressivity of the system, allowing developers to embed backend functions and create reactive interfaces. For a full reference, see the official Jinja2 documentation.

Most Used Capabilities

Setting Variables

Define variables within the template to be used later.

Example:

{% set title = "Welcome to the InGame Wiki" %}

Expression Interpolation

Insert variable values or evaluate expressions directly into the HTML output.

Example:

<h1>{{ title }}</h1>

Macros

Create reusable template fragments that can be invoked with different parameters.

  • First you define a macro:
    `{% macro macro_name(parameters) %} ... {% endmacro %}`
  • Tehn you can call a macro:
{{ macro_name(arguments) }}

Example:

{% macro render_header(text) %}
<header><h1>{{ text }}</h1></header>
{% endmacro %}

{{ render_header("My Page Title") }}

Include

Embed the contents of one template into another, promoting modularity.

Example:

{% include "templates/footer.html" %}

Extend

Build upon a base template by overriding defined blocks, facilitating template inheritance.

Example:

  • Base Template Example (base.html):
<head>
<meta charset="UTF-8">
<title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
<main>
{% block content %}
<!-- Default content goes here -->
{% endblock %}
</main>
<footer>
{% block footer %}
<p>Footer content here</p>
{% endblock %}
</footer>
</body>
{% extends "base.html" %}
{% block content %}
<p>This is my custom content.</p>
{% endblock %}

Function Embedding

Invoke backend or custom Python functions within the template. Functions are passed via the template context.

{% set user_name = get_user_name(user_id) %}
<p>Welcome, {{ user_name }}!</p>

Jinja2 Templating Step-by-Step

Below is an example UCHTML page that demonstrates how Jinja2 templating is integrated into the system and the step-by-step explaination of the code.

{% import "templates/headers.html" as header %}
{% import "templates/page.html" as page %}

{% set vnum = fun.getvi("query:vnum") %}
{% set tab = fun.getv("query:tab") %}
{% set can_show_stats = item.can_show_stats(vnum) %}
....

{{ page.title(["Oggetti"], item.item_name(vnum)) }}
<main>
<filter-group type="tabs" filter-target="query:tab" width="auto" default="info">
<filter type="tab" height="25" width="100" padding="5" filter-value="info">
<txt color="wikititle" align="center">Informazioni</txt>
</filter>
{% if has_upgrade or has_epic_upgrade %}
<filter type="tab" height="25" width="100" padding="5" filter-value="up">
<txt color="wikititle" align="center">Miglioramento</txt>
</filter>
{% endif %}
{% if fun.notempty(cubes_as_material) or fun.notempty(cubes_as_result) %}
<filter type="tab" height="25" width="100" padding="5" filter-value="craft">
<txt color="wikititle" align="center">Craft</txt>
</filter>
{% endif %}
</filter-group>

{% if tab == "info" %}
{% if can_show_stats %}
<item-stats width=auto vnum="{{ vnum }}" />
{% endif %}
<div display="flex" name="flex_outer" width="auto" justify-content="space-between">
<div width="59%">
{{ header.h2("Informazioni") }}
<inline>
{% if is_costume %}
<model item-vnum="{{ vnum }}" camera-move="1" height="150" width="150" margin-right="10"/>
{% endif %}
<div width="auto">
<slot-item vnum="{{ vnum }}" margin-bottom="5"/>
<div width="auto">
{% if desc != "" %}
{{ desc }}
{% endif %}
{% if summary %}
<spacer />
{{ summary }}
{% endif %}
</div>
</div>
</inline>
{% include "p/items/templates/{}.html".format(vnum) ignore missing with context %}

{% if fun.notempty(in_chest) %}
<spacer />
{{ header.h2("Si trova dentro le casse") }}
<items-grid width=auto height=32 vnums="{{ in_chest }}" />
{% endif %}

{% if fun.notempty(chest_content) %}
<spacer/>
<div height="200" scrollbar-gap="5">
<chest-content width="auto" vnum="{{ vnum }}"/>
</div>
{% endif %}

{% if fun.notempty(upgrade_for) %}
<spacer/>
{{ header.h2("Utilizzato come oggetto da miglioramento da") }}
<items-grid sort-size="True" height="128" width="auto" vnums="{{ upgrade_for }}"/>
{% endif %}
</div>
<div width="40%">
{% if fun.notempty(sold_by) %}
{{ header.h2("Comprabile dagli NPC") }}
{% for seller in sold_by %}
<bullet-list margin-bottom="5">
<hmob vnum="{{ seller.npc_vnum }}"/>
</bullet-list>
{% endfor %}
{% endif %}
{{ header.h2("Trovabile come drop") }}
<div height="auto" display="block">
<item-obtainable-from width="auto" vnum="{{ vnum }}"/>
</div>
</div>
</div>
{% endif %}

{% if tab == "up" %}
{% if has_upgrade %}
<equip-up width="auto" vnum="{{ vnum }}"/>
{% endif %}

{% if has_epic_upgrade %}
<equip-epic-up width="auto" vnum="{{ vnum }}"/>
{% endif %}
{% endif %}

{% if tab == "craft" %}
{% if fun.notempty(cubes_as_material) %}
{{ header.h2("Usato come materiale nei seguenti craft") }}
<lazy-div preload="15" display="flex" width=auto spacing="5" justify-content="space-evenly">
{% for value in cubes_as_material %}
{% if loop.changed(value["npc"]) %}
<h2 width="100%" pattern="d:/ymir work/ui/quest_re/quest_tab">Crafter <hmob vnum="{{ value.npc }}" /></h2>
{% endif %}
<cube-card cost="{{ value['gold'] }}" pct="{{ value['percent'] }}">
<cube-result vnum="{{ value['reward']['vnum'] }}" count="{{ value['reward']['count'] }}"></cube-result>
{% for mat in value["materials"] %}
<cube-material vnum="{{ mat['vnum'] }}" count="{{ mat['count'] }}"></cube-material>
{% endfor %}
</cube-card>
{% endfor %}
</lazy-div>
{% endif %}
{% endif %}
</main>

1. Template Imports

The template starts by importing external templates:

{% import "templates/headers.html" as header %}
{% import "templates/page.html" as page %}
  • Purpose:
    These imports allow the template to use reusable components (macros) defined in the imported files.
  • The header alias is used to access header-related macros (e.g., header.h2).
  • The page alias provides access to layout and title macros for setting up the page structure.

2. Variable Initialization

A series of {% set %} statements initialize variables that hold dynamic data about an item.
All function used here are embedded by the backend to the html page.

{% set vnum = fun.getvi("query:vnum") %}
{% set tab = fun.getv("query:tab") %}
{% set can_show_stats = item.can_show_stats(vnum) %}
{% set is_costume = item.is_any_of_type(vnum, item.ITEM_TYPE_COSTUME, item.ITEM_TYPE_COSTUME_PET, item.ITEM_TYPE_COSTUME_MOUNT) %}
...

Explanation:

  • vnum and tab retrieve query parameters to determine which item is being viewed and which tab should be active.
  • The following variables (e.g., can_show_stats, is_costume, desc, etc.) fetch various properties and related data for the item identified by vnum.
  • These variables drive conditional rendering later in the template, ensuring that only relevant sections are displayed.

3. Dynamic Page Title Rendering

The template sets a dynamic page title using a macro from the imported page template:

{{ page.title(["Oggetti"], item.item_name(vnum)) }}

Explanation:

  • This line calls the page.title macro with two parameters: a list containing the section label "Oggetti" and the item’s name (obtained via item.item_name(vnum)).
  • The result is a dynamically generated title that reflects the current item.

4. Main Content Structure

The main content of the page is wrapped within the <main> element. Inside, the template defines different sections based on the active tab.

4.1. Tab Filter Group

A filter group is created to allow the user to switch between different views:

<filter-group type="tabs" filter-target="query:tab" width="auto" default="info">
<filter type="tab" height="25" width="100" padding="5" filter-value="info">
<txt color="wikititle" align="center">Informazioni</txt>
</filter>
{% if has_upgrade or has_epic_upgrade %}
<filter type="tab" height="25" width="100" padding="5" filter-value="up">
<txt color="wikititle" align="center">Miglioramento</txt>
</filter>
{% endif %}
{% if fun.notempty(cubes_as_material) or fun.notempty(cubes_as_result) %}
<filter type="tab" height="25" width="100" padding="5" filter-value="craft">
<txt color="wikititle" align="center">Craft</txt>
</filter>
{% endif %}
</filter-group>

Explanation:

  • The filter group creates tabs for "Informazioni", "Miglioramento", and "Craft".
  • The "Miglioramento" tab is rendered only if the item has an upgrade.
  • The "Craft" tab appears only if there is crafting-related data.
  • These tabs allow users to change the tab query parameter, which in turn controls which content section is displayed.
4.2. Conditional Rendering Based on the Active Tab

The template uses conditionals to render different content blocks depending on the value of tab.

4.2.1. When tab == "info"
{% if tab == "info" %}
{% if can_show_stats %}
<item-stats width=auto vnum="{{ vnum }}" />
{% endif %}
<div display="flex" name="flex_outer" width="auto" justify-content="space-between">
<div width="59%">
{{ header.h2("Informazioni") }}
<inline>
{% if is_costume %}
<model item-vnum="{{ vnum }}" camera-move="1" height="150" width="150" margin-right="10"/>
{% endif %}
<div width="auto">
<slot-item vnum="{{ vnum }}" margin-bottom="5"/>
<div width="auto">
{% if desc != "" %}
{{ desc }}
{% endif %}
{% if summary %}
<spacer />
{{ summary }}
{% endif %}
</div>
</div>
</inline>
{% include "p/items/templates/{}.html".format(vnum) ignore missing with context %}

{% if fun.notempty(in_chest) %}
<spacer />
{{ header.h2("Si trova dentro le casse") }}
<items-grid width=auto height=32 vnums="{{ in_chest }}" />
{% endif %}

{% if fun.notempty(chest_content) %}
<spacer/>
<div height="200" scrollbar-gap="5">
<chest-content width="auto" vnum="{{ vnum }}"/>
</div>
{% endif %}

{% if fun.notempty(upgrade_for) %}
<spacer/>
{{ header.h2("Utilizzato come oggetto da miglioramento da") }}
<items-grid sort-size="True" height="128" width="auto" vnums="{{ upgrade_for }}"/>
{% endif %}
</div>
<div width="40%">
{% if fun.notempty(sold_by) %}
{{ header.h2("Comprabile dagli NPC") }}
{% for seller in sold_by %}
<bullet-list margin-bottom="5">
<hmob vnum="{{ seller.npc_vnum }}"/>
</bullet-list>
{% endfor %}
{% endif %}
{{ header.h2("Trovabile come drop") }}
<div height="auto" display="block">
<item-obtainable-from width="auto" vnum="{{ vnum }}"/>
</div>
</div>
</div>
{% endif %}

Explanation:

  • Item Stats: If can_show_stats is true, the <item-stats> component is rendered.
  • Left Column (59% width):
    • Displays a header "Informazioni" using a macro from the header template.
    • If the item is a costume, a 3D model is rendered via the <model> tag.
    • The item details are displayed within an inline container:
      • A <slot-item> component shows a slot for the item.
      • Descriptive text (desc) and summary (summary) are conditionally rendered.
    • An external template is included for additional item details.
    • Additional sections (e.g., items found in chests, chest content, upgrade material) are rendered if the corresponding data is present.
  • Right Column (40% width):
    • Conditionally renders a list of NPC sellers.
    • Always renders a section for items obtainable as drops via <item-obtainable-from>.
4.2.2. When tab == "up"
{% if tab == "up" %}
{% if has_upgrade %}
<equip-up width="auto" vnum="{{ vnum }}"/>
{% endif %}

{% if has_epic_upgrade %}
<equip-epic-up width="auto" vnum="{{ vnum }}"/>
{% endif %}
{% endif %}

Explanation:

  • This block renders the upgrade section.
  • If the item has a standard upgrade (has_upgrade), the <equip-up> component is displayed.
  • If an epic upgrade exists (has_epic_upgrade), the <equip-epic-up> component is rendered.
4.2.3. When tab == "craft"
{% if tab == "craft" %}
{% if fun.notempty(cubes_as_material) %}
{{ header.h2("Usato come materiale nei seguenti craft") }}
<lazy-div preload="15" display="flex" width=auto spacing="5" justify-content="space-evenly">
{% for value in cubes_as_material %}
{% if loop.changed(value["npc"]) %}
<h2 width="100%" pattern="d:/ymir work/ui/quest_re/quest_tab">Crafter <hmob vnum="{{ value.npc }}" /></h2>
{% endif %}
<cube-card cost="{{ value['gold'] }}" pct="{{ value['percent'] }}">
<cube-result vnum="{{ value['reward']['vnum'] }}" count="{{ value['reward']['count'] }}"></cube-result>
{% for mat in value["materials"] %}
<cube-material vnum="{{ mat['vnum'] }}" count="{{ mat['count'] }}"></cube-material>
{% endfor %}
</cube-card>
{% endfor %}
</lazy-div>
{% endif %}
{% endif %}

Explanation:

  • This block deals with the crafting information.
  • If crafting material data (cubes_as_material) exists, a header is shown.
  • A <lazy-div> container is used to efficiently load the crafting components.
  • A loop iterates over cubes_as_material:
    • When the npc value changes, a header is inserted to indicate the crafter.
    • Each craft is presented within a <cube-card> that displays cost, reward details, and required materials.

Reactive Zones for High-Performance Updates

UCHTML supports reactive zones - self-contained sections of your template that can re-render independently when specific variables change. This provides 10-100x performance improvement over full page reloads and preserves scroll position, resulting in a dramatically better user experience.

What are Reactive Zones?

Reactive zones allow you to mark sections of your HTML template that should automatically update when certain Jinja2 variables change, without reloading the entire page. When you update a watched variable using SetVar() or SetVars(), only the affected zones re-render, leaving the rest of the page untouched.

Benefits:

  • 10-100x faster updates - Only the zone re-renders, not the entire page
  • 📜 Scroll position preserved - Users don't lose their place
  • No visual flicker - Smooth, in-place updates
  • 🎯 Automatic optimization - System intelligently chooses between zone update and full reload

Marking Reactive Zones

To create a reactive zone, add two attributes to a <div> element:

  1. id - Unique identifier for the zone (required)
  2. reactive - Comma-separated list of variable names this zone watches (required)

Example:

<div id="progress-zone" reactive="mh_1_done,mh_1_need">
{% set mh_1_done = fun.getv('mh_1_done', 0) %}
{% set mh_1_need = fun.getv('mh_1_need', 0) %}

Progress: {{ mh_1_done }}/{{ mh_1_need }}
<progress max="{{ mh_1_need }}" count="{{ mh_1_done }}" />
</div>

Requirements:

  • ✅ Reactive zones must have both id and reactive attributes
  • ✅ The id must be unique within the page
  • ✅ The reactive attribute lists variable names separated by commas (no spaces)
  • ✅ Variables listed in reactive should be fetched using fun.getv() or fun.getvi() within the zone
  • ⚠️ Note: If reactive is present without id, the validator will report an error

Using Reactive Zones with Jinja2

Inside a reactive zone, you can use all standard Jinja2 features - variables, conditionals, loops, macros, and filters. The key is to fetch your reactive variables using the fun.getv() or fun.getvi() functions:

<div id="mission-status" reactive="state,objective_count,objective_max">
{% set state = fun.getv('state', 'available') %}
{% set objective_count = fun.getvi('objective_count', 0) %}
{% set objective_max = fun.getvi('objective_max', 10) %}

{% if state == "available" %}
<txt color="green">Quest Available</txt>
{% elif state == "task_wip" %}
<txt color="yellow">In Progress: {{ objective_count }}/{{ objective_max }}</txt>
<progress max="{{ objective_max }}" count="{{ objective_count }}" />
{% elif state == "completed" %}
<txt color="gold">Completed!</txt>
{% endif %}
</div>

Updating Variables

To trigger reactive zone updates from Python code, use the SetVar() or SetVars() methods on your HTMLLayoutLoader instance:

Single variable update:

# Updates only zones watching "mh_1_done"
loader.SetVar("mh_1_done", 15)

Multiple variables update (optimized):

# Updates all affected zones with a single template render
loader.SetVars({
"mh_1_done": 15,
"mh_1_need": 20,
"state": "task_wip"
})

How it works:

  1. The system detects which reactive zones watch the changed variable(s)
  2. If zones are affected, the template is rendered once and all zones update from the same render
  3. Only the zone's content is rebuilt - the rest of the page remains untouched
  4. Scroll position is automatically preserved
  5. If no zones watch the variable, the system falls back to a full reload with scroll preservation

Complex Example with Multiple Zones

<main>
<!-- Static header (never changes) -->
<h2>Mission Dashboard</h2>

<!-- Reactive zone 1: Progress tracker -->
<div id="progress-zone" reactive="mh_1_done,mh_1_need">
{% set mh_1_done = fun.getv('mh_1_done', 0) %}
{% set mh_1_need = fun.getv('mh_1_need', 0) %}

{{ header.h2("Corna di Tauro raccolte") }}
<txt>Progress: {{ mh_1_done }}/{{ mh_1_need }}</txt>
<progress max="{{ mh_1_need }}" count="{{ mh_1_done }}" />
</div>

<!-- Reactive zone 2: Status display -->
<div id="status-zone" reactive="state">
{% set state = fun.getv('state', 'available') %}

{% if state == "available" %}
<txt color="green">Available to start</txt>
{% elif state == "task_wip" %}
<txt color="yellow">Mission in progress</txt>
{% elif state == "completed" %}
<txt color="gold">Mission completed!</txt>
{% endif %}
</div>

<!-- Static instructions (never changes) -->
<div>
<h3>Instructions</h3>
<txt>Hunt monsters and collect materials...</txt>
</div>
</main>

Updating from Python:

# Update progress (only progress-zone updates)
loader.SetVar("mh_1_done", 11)

# Update multiple values efficiently (both zones update with single render)
loader.SetVars({
"mh_1_done": 15,
"state": "task_wip"
})

Performance Comparison

ScenarioFull ReloadReactive ZoneImprovement
Progress counter update~100ms<1ms100x faster
Simple text change~100ms<1ms100x faster
List update (20 items)~100ms~5ms20x faster
Scroll preservation❌ Lost✅ Preserved∞ better UX

Best Practices

Do:

  • ✅ Use reactive zones for frequently updated sections (counters, progress bars, status displays)
  • ✅ Keep zones focused on specific variables - avoid watching too many variables in one zone
  • ✅ Use fun.getv() to fetch variables with sensible defaults
  • ✅ Use SetVars() when updating multiple variables at once

Don't:

  • ❌ Make the entire page one giant reactive zone (defeats the purpose)
  • ❌ Create reactive zones for sections that rarely change
  • ❌ Forget to add both id and reactive attributes
  • ❌ Use reactive zones for structural changes that require full page rebuild

When to Use Reactive Zones vs Full Reload

Use Reactive Zones for:

  • Progress counters and status updates
  • Real-time data displays
  • Frequently changing text values
  • Simple list additions/removals within a zone

Use Full Reload for:

  • Major layout changes
  • Tab switching that changes page structure
  • Initial page load
  • Variables not watched by any reactive zone (automatic fallback)

The system automatically handles the decision for you - if a variable change affects reactive zones, they update; otherwise, the page reloads with scroll preserved.