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
headeralias is used to access header-related macros (e.g.,header.h2). - The
pagealias 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:
vnumandtabretrieve 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 byvnum. - 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.titlemacro with two parameters: a list containing the section label"Oggetti"and the item’s name (obtained viaitem.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
tabquery 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_statsis true, the<item-stats>component is rendered. - Left Column (59% width):
- Displays a header "Informazioni" using a macro from the
headertemplate. - 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.
- A
- 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.
- Displays a header "Informazioni" using a macro from the
- 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
npcvalue 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.
- When the
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:
id- Unique identifier for the zone (required)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
idandreactiveattributes - ✅ The
idmust be unique within the page - ✅ The
reactiveattribute lists variable names separated by commas (no spaces) - ✅ Variables listed in
reactiveshould be fetched usingfun.getv()orfun.getvi()within the zone - ⚠️ Note: If
reactiveis present withoutid, 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:
- The system detects which reactive zones watch the changed variable(s)
- If zones are affected, the template is rendered once and all zones update from the same render
- Only the zone's content is rebuilt - the rest of the page remains untouched
- Scroll position is automatically preserved
- 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
| Scenario | Full Reload | Reactive Zone | Improvement |
|---|---|---|---|
| Progress counter update | ~100ms | <1ms | 100x faster |
| Simple text change | ~100ms | <1ms | 100x faster |
| List update (20 items) | ~100ms | ~5ms | 20x 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
idandreactiveattributes - ❌ 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.
- What is Jinja2?
- Most Used Capabilities
- Setting Variables
- Expression Interpolation
- Macros
- Include
- Extend
- Function Embedding
- Jinja2 Templating Step-by-Step
- 1. Template Imports
- 2. Variable Initialization
- 3. Dynamic Page Title Rendering
- 4. Main Content Structure
- Reactive Zones for High-Performance Updates
- What are Reactive Zones?
- Marking Reactive Zones
- Using Reactive Zones with Jinja2
- Updating Variables
- Complex Example with Multiple Zones
- Performance Comparison
- Best Practices
- When to Use Reactive Zones vs Full Reload