Generate full page or HTML fragment based on request header (HTMX)

Question:

When using HTMX framework with Python Flask, you have to be able to:

  • serve a request as a HTML fragment if it’s done by HTMX (via AJAX)

  • server a request as a full page if it’s done by the user (e.g. entered directly in the browser URL bar)

See Single-page-application with fixed header/footer with HTMX, with browsing URL history or Allow Manual Page Reloading
for more details.

How to do this with the Flask template system?

from flask import Flask, render_template, request
app = Flask("")
@app.route('/pages/<path>')
def main(path):
    htmx_request = request.headers.get('HX-Request') is not None
    return render_template(path + '.html', fullpage=not htmx_request)
app.run()

What’s the standard way to output a full page (based on a parent template pagelayout.html):

{% extends "pagelayout.html" %}
{% block container %}
<button>Click me</button>
{% endblock %}

if fullpage is True, and just a HTML fragment:

<button>Click me</button>

if it is False?

Asked By: Basj

||

Answers:

This solution based on that we can use a dynamic variable when extending a base template. So depending on the type or the request, we use the full base template or a minimal base template that returns only our fragment’s content.

Lets call our base template for fragments base-fragments.html:

{% block container %}
{% endblock %}

It’s just returns the main block’s content, nothing else. At the view function we have a new template variable baselayout, that contains the name of the base template depending on the request’s type (originating from HTMX or not):

@app.route('/pages/<path>')
def main(path):
    htmx_request = request.headers.get('HX-Request') is not None
    baselayout = 'base-fragments.html' if htmx_request else 'pagelayout.html'

    return render_template(path + '.html', baselayout=baselayout)

And in the page template, we use this baselayout variable at the extends:

{% extends baselayout %}
{% block container %}
<button>Click me</button>
{% endblock %}
Answered By: Dauros

As pointed in the section Null-Default Fallback of Jinja documentation, the extends tag can actually come in an if statement:

Jinja supports dynamic inheritance and does not distinguish between parent and child template as long as no extends tag is visited. While this leads to the surprising behavior that everything before the first extends tag including whitespace is printed out instead of being ignored, it can be used for a neat trick.

Usually child templates extend from one template that adds a basic HTML skeleton. However it’s possible to put the extends tag into an if tag to only extend from the layout template if the standalone variable evaluates to false which it does per default if it’s not defined. Additionally a very basic skeleton is added to the file so that if it’s indeed rendered with standalone set to True a very basic HTML skeleton is added:

{% if not standalone %}{% extends 'default.html' %}{% endif -%}
<!DOCTYPE html>
<title>{% block title %}The Page Title{% endblock %}</title>
<link rel="stylesheet" href="style.css" type="text/css">
{% block body %}
  <p>This is the page body.</p>
{% endblock %}

Source: https://jinja.palletsprojects.com/en/3.0.x/tricks/#null-default-fallback

So, your requirement could be fulfilled doing:

{% if not fullpage %}{% extends 'pagelayout.html' %}{% endif -%}
{% block container -%}
  <button>Click me</button>
{%- endblock %}
Answered By: β.εηοιτ.βε

Use jinja2-fragments

from jinja2_fragments.flask import render_block

def conditional_render(template_name, block_name = None, **context):
    if "HX-Request" in request.headers:
        return render_block(template_name=template_name, block_name=block_name, **context)
    return render_template(template_name=template_name, **context)

@app.get("/foo")
def foo():
  return conditional_render(template_name="foo.html", block_name=request.headers.get("HX-Target"))

foo.html:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>...</title>    
</head>

<body>

  <div id="block1" hx-target="this">
    {% block block1 %}hey{% endblock %}
  </div>

  <div id="block2" hx-target="this">
    {% block block2 %}there{% endblock %}
  </div>

  <script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script>
</body>
</html>

Then if HX-Target is "block1" only block1 will be returned, and so on

Answered By: Neil McGuigan
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.