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

Viewed 132

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?

2 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 %}

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 %}
Related