Giter VIP home page Giter VIP logo

jinja2-fragments's Introduction

Jinja2 fragments

Jinja2 Fragments allows rendering individual blocks from Jinja2 templates. This library was created to enable the pattern of Template Fragments with Jinja2. It's a great pattern if you are using HTMX or some other library that leverages fetching partial HTML.

With jinja2, if you have a template block that you want to render by itself and as part of another page, you are forced to put that block on a separate file and then use the include tag (or Jinja Partials) on the wrapping template.

With Jinja2 Fragments, following the Locality of Behavior design principle, you have a single file for both cases. See below for examples.

Install

It's just pip install jinja2-fragments and you're all set. It's a pure Python package that only needs jinja2 (for obvious reasons!).

Usage

This is an example of how to use the library with vanilla Jinja2. Given the template page.html.jinja2:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>This is the title</title>
</head>
<body>
    <h1>This is a header</h1>
    {% block content %}
    <p>This is the magic number: {{ magic_number }}.</p>
    {% endblock %}
</body>
</html>

If you want to render only the content block, do:

from jinja2 import Environment, FileSystemLoader, select_autoescape
from jinja2_fragments import render_block

environment = Environment(
    loader=FileSystemLoader("my_templates"),
    autoescape=select_autoescape(("html", "jinja2"))
)
rendered_html = render_block(
    environment, "page.html.jinja2", "content", magic_number=42
)

And this will only render:

<p>This is the magic number: 42.</p>

Usage with Flask

If you want to use Jinja2 Fragments with Flask, assuming the same template as the example above, do:

from flask import Flask, render_template
from jinja2_fragments.flask import render_block

app = Flask(__name__)

@app.get("/full_page")
def full_page():
    return render_template("page.html.jinja2", magic_number=42)


@app.get("/only_content")
def only_content():
    return render_block("page.html.jinja2", "content", magic_number=42)

Usage with Quart

If you want to use Jinja2 Fragments with Quart, assuming the same template as the example above, do:

from quart import Quart, render_template
from jinja2_fragments.quart import render_block

app = Quart(__name__)

@app.get("/full_page")
async def full_page():
    return await render_template("page.html.jinja2", magic_number=42)


@app.get("/only_content")
async def only_content():
    return await render_block("page.html.jinja2", "content", magic_number=42)

Usage with FastAPI

You can also use Jinja2 Fragments with FastAPI. In this case, Jinja2 Fragments has a wrapper around the FastAPI Jinja2Templates object called Jinja2Blocks.

It functions exactly the same, but allows you to include an optional parameter to the TemplateResponse that includes the block_name you want to render.

Assuming the same template as the examples above:

from fastapi import FastAPI
from fastapi.requests import Request
from jinja2_fragments.fastapi import Jinja2Blocks

app = FastAPI()

templates = Jinja2Blocks(directory="path/to/templates")

@app.get("/full_page")
async def full_page(request: Request):
    return templates.TemplateResponse(
        "page.html.jinja2",
        {"request": request, "magic_number": 42}
    )

@app.get("/only_content")
async def only_content(request: Request):
    return templates.TemplateResponse(
        "page.html.jinja2",
        {"request": request, "magic_number": 42},
        block_name="content"
    )

Usage with Sanic

You can use jinja2-fragments's render() with Sanic as a drop-in replacement of the Sanic template extension's render(). Your request context and environment configuration will work the same as before. You must have sanic_ext and Jinja2 installed.

By default, the full page is rendered (block=None) unless you provide a block keyword argument.

from sanic import Sanic, Request
import sanic_ext
from jinja2_fragments.sanic import render

app = Sanic(__name__)
app.extend(config=sanic_ext.Config(templating_path_to_templates='path/to/templates'))

@app.get('/full_page')
async def full_page(request: Request):
    return await render(
        'page.html.jinja2', 
        context={"magic_number": 42}
    )

@app.get("/only_content")
async def only_content(request: Request):
    return await render(
        'page.html.jinja2',
        block='content',
        context={"magic_number": 42}
    )

Usage with Litestar

You can use Jinja2 Fragments with Litestar by using the LitestarHTMXTemplate class. This gives you access to the block_name parameter when rendering the template.

By default, the full page is rendered unless you provide a block_name keyword argument.

from litestar.contrib.htmx.request import HTMXRequest
from litestar import get, Litestar
from litestar.response import Template

from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.template.config import TemplateConfig
from jinja2_fragments.litestar import HTMXBlockTemplate


@get('/full_page')
def full_page(request: HTMXRequest) -> Template:
    return HTMXBlockTemplate(
        template_name='page.html.jinja2',
        context={"magic_number": 42}
    )

@get('/only_content')
def only_content(request: HTMXRequest) -> Template:
    return HTMXBlockTemplate(
        template_name='page.html.jinja2',
        block_name='content',
        context={"magic_number": 42}
    )

app = Litestar(
    route_handlers=[full_page, only_content],
    request_class=HTMXRequest,
    template_config=TemplateConfig(
        directory="path/to/templates",
        engine=JinjaTemplateEngine,
    )
)

How to collaborate

This project uses pre-commit hooks to run black, isort, pyupgrade and flake8 on each commit. To have that running automatically on your environment, install the project with:

pip install -e .[dev]

And then run once:

pre-commit install

From now on, every time you commit your files on this project, they will be automatically processed by the tools listed above.

How to run tests

You can install pytest and other required dependencies with:

pip install -e .[tests]

And then run the test suite with:

pytest

jinja2-fragments's People

Contributors

amjith avatar bo5o avatar chickenbellyfin avatar etiennepelletier avatar gconklin avatar kedod avatar rdpate avatar s0er3n avatar smsearcy avatar sponsfreixes avatar tataraba avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jinja2-fragments's Issues

'loop' is undefined

Hi,

I'm migrating a traditional Flask+Jinja2 app to one with HTMX. Similar to #23, I'm using a block inside a loop.

I'm also using the special loop.index variable, which is when I'm getting: jinja2.exceptions.UndefinedError: 'loop' is undefined.

I know I can get around that by simply wrapping my iterable into list(enumerate(...)), but it'd probably make a smoother experience if loop was available.

Not sure if that's even possible. If not, it should probably be documented as a caveat.

๐Ÿ™

I am working on a awesome-python-htmx, seeking your feedback

@tataraba and I at PyCon this year got together and brainstormed a new Web Stack that we are calling PyHAT (Python, htmx, ASGI, TailwindCSS). The first thing we set out to do is create awesome-python-htmx; a collection of active tools/libraries/projects in that space.

This project is an obvious thing to include, and indeed was part of the inspiration to do this, so I did so. I'd appreciate feedback if you have any on it's inclusion.

In addition to that, if you could also participate in PyHAT-stack/awesome-python-htmx#1 that would be greatly appreciated!

Related to #8

CSS selectors

I haven't used Flask and Jinja for a long time and just stumbled upon this project via the htmx website.
So maybe my suggestion doesn't make much sense. But here it is:

Have you considered using CSS selectors in a render_ function instead of (or in addition to) marking the blocks explicitly with {%block%} and `{%endblock%} in the template and giving them a label?

That would provide way more flexibility, but only work for html.
It would also now be unclear that this template is being depended on (and how),
but maybe this drawback is acceptable or could otherwise be mitigated.

AIOHTTP template render_block with HTMX

Django & HTMX - Template Fragments with django-render-block
https://www.youtube.com/watch?v=BsGak1t23QA

I tried to make the django example at this address to be aiohttp jinja2. I couldn't adapt it to aiohttp. Sample code is attached.

import jinja2
import aiohttp_jinja2
from aiohttp import web
from pathlib import Path
from models import Device
from tortoise.contrib.aiohttp import register_tortoise
from jinja2_fragments import render_block

here = Path(__file__).resolve().parent

##########################################################
@aiohttp_jinja2.template("home.html")
async def home(request):
    return {}

@aiohttp_jinja2.template("device_list.html")
async def device_list(request):
    objects = await Device.all().order_by('name')
    return {'objects': objects, 'title': 'DEVICE LIST HTMX'}

async def device_add(request: web.Request) -> web.Response:
    form = await request.post()
    code = form['code']
    name = form['name']

    await Device.create(code=code, name=name)
    headers={'HX-Trigger': "device_list_changed"}

    #html =  render_block("device_list.jinja2", "device_form_block", context)
    #response = web.Response(html)

    return web.Response(status=204, headers=headers)


async def device_all(request):
    objects = await Device.all().order_by('name')
    """
    headers={
                    'HX-Trigger': json.dumps({
                        "device_list_changed": None,
                        "showMessage": f"device added.",
                        "objects": objects
                    })
                    }


    """
    context = {'objects': objects}
    html =  render_block("device_list.jinja2", "device_list_block", context)
    return web.Response(html)
#################################################################
app = web.Application()
app.router.add_get('/', home, name="home")
app.router.add_get('/device/list', device_list, name="device-list")
app.router.add_post('/device/add', device_add, name='device-add')
[aiohttp_template_render_block.zip](https://github.com/sponsfreixes/jinja2-fragments/files/10138379/aiohttp_template_render_block.zip)

app.router.add_get('/device_all', device_all, name="device_all")
app.add_routes([web.static('/static', here / 'static' )])
register_tortoise(
    app,
    db_url='postgres://postgres:root@localhost:5433/myweb',
    modules={'models': ['models']},
    generate_schemas=True
)
aiohttp_jinja2.setup(
    app,
    loader=jinja2.FileSystemLoader('templates'),
    autoescape=jinja2.select_autoescape(("html", "jinja2"))
)
web.run_app(app, port=8000)
{% extends "base.html" %}

{% block content %}
    
	<div class="container">
		
		<div class="row">
			<h3>{{ title }}</h3>
		</div>
		
		
		{% block device_form_block %}
		<div class="row" id="add-form-container">
				<form class="" hx-post="/device/add" hx-target="#add-form-container" hx-swap="outerHTML">

							<div class="row mb-2">
								<label for="code" class="col-md-2 col-form-label">Code</label>
								<div class="col-md-10">
								  <input type="text" class="form-control" name="code" value="" placeholder="code.....">
								</div>
![error1](https://user-images.githubusercontent.com/51874093/205223392-76401124-dbb6-4d11-97b4-a5ccf6f559cc.png)

							</div>
							<div class="row mb-2">
								<label for="name" class="col-md-2 col-form-label">Name</label>
								<div class="col-md-10">
								  <input type="text" class="form-control" name="name" value="" placeholder="name....." required>
								</div>
							</div>
							
							<div class="row mb-2">
								<label for="" class="col-md2 col-form-label"></label>
								<div class="col-md-10">
								  <input type="submit" class="btn btn-success" value="Save">
								</div>
							</div>
						</form>
		</div>
		{% endblock %}
		
		{% block device_list_block %}
		<div class="row" id="add-form-container">
			<table class="table">
			  <thead>
				<tr>
				  <th scope="col">Code</th>
				  <th scope="col">Name</th>
				  
				</tr>
			  </the
![error1](https://user-images.githubusercontent.com/51874093/205223502-f673e368-5162-4f74-a6fa-08f26520c44c.png)
ad>
			  <tbody hx-trigger="device_list_changed from:body" hx-get="/device_all" hx-swap="outerHTML">
				{% for row in objects %}
					<tr>
					<td>{{ row.code }}</td>
					<td>{{ row.name }}</td>
					 
					 
					</tr>
				{% endfor %}
			  </tbody>
			</table>
		</div>
		{% endblock %}
	</div>
{% endblock %}

Getting New Lines

Using
fastapi[all]==0.103.1
jinja2-fragments==1.0.0

I'm getting lots of \n new lines all over the block when using the Jinja2Blocks

{% block test_items %}
        <div id="items">

            {% for item in items %}
                <li>{{ item }}</li>
            {% endfor %}
        </div>
{% endblock %}

Output and it is stringified with quotations

"\n
\n
\n\n \n
 Item1
\n \n
 Item2
\n \n
\n
\n"

but if I put this in a partial html file with just

<ul>
    {% for item in items %}
        <li>{{ item }}</li>
    {% endfor %}
</ul>

Then comes out perfect

  • Item1
  • Item2

Can you understand why it might be creating extra line items.

Using python3.11-bullseye docker container

imports are not considered when used in the fragment/block

If I am using macros (used as components) inside a block I want to render

render_block(templates.env, "index.html.j2", "tracks_body", **context)

it fails like this:

  File "templates/index.html.j2", line 72, in block 'tracks_body'
    {{ m.spotify_link(href=track.external_urls['spotify']) }}
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/xyz/.venv/lib/python3.11/site-packages/jinja2/environment.py", line 485, in getattr
    return getattr(obj, attribute)
           ^^^^^^^^^^^^^^^^^^^^^^^
jinja2.exceptions.UndefinedError: 'm' is undefined

when in index.html.j2 I have

{% import "_macros.j2" as m %}

...

{% block tracks_body %}
...
{{ m.spotify_link(href=track.external_urls['spotify']) }}
...
{% endblock %}

Can we have some type of heuristic that when an import is used then we pass that to the context as well?

Return HTMLReponse instead of str

Problem

In FastAPI Jinja2Templates.TemplateResponse returns a _TemplateResponse, a subclass from Response. When rendering a block with Jinja2Blocks, a str is returned.

There are two problems with this approach:

  1. The other arguments (status_code, headers (see #6), media_type, background) to TemplateReponse are ignored.
  2. The user needs to manually wrap the rendered block in an HTMLResponse or set the default_response_class to HTMLResponse in the app/router.

Proposal

Return an HTMLReponse from Jinja2Blocks.TemplateResponse whenblock_name is defined to be more inline with the default behavior of the method.

The change would look something like this:

class Jinja2Blocks(Jinja2Templates):
    def __init__(self, directory, **env_options):
        super().__init__(directory, **env_options)

    def TemplateResponse(
        self,
        name: str,
        context: dict,
        status_code: int = 200,
        headers: typing.Optional[typing.Mapping[str, str]] = None,
        media_type: typing.Optional[str] = None,
        background: typing.Optional[BackgroundTask] = None,
        *,
        block_name: typing.Optional[str] = None,
    ) -> typing.Union[_TemplateResponse, HTMLResponse]:
        if "request" not in context:
            raise ValueError('context must include a "request" key')
        template = self.get_template(name)

        if block_name:
            return HTMLResponse(
                render_block(
                    self.env,
                    name,
                    block_name,
                    context,
                ),
                status_code=status_code,
                headers=headers,
                media_type=media_type,
                background=background,
            )

        return _TemplateResponse(
            template,
            context,
            status_code=status_code,
            headers=headers,
            media_type=media_type,
            background=background,
        )

I would be happy to also make a PR with this proposal.

accessing dictionary.items() is undefined

my_dict = {"a": "apple", "b":  "bananna"}
return render_block(template, block, my_dict=my_dict)
{% block content %}
  {% for k, v in my_dict.items() %}
    {{ k }} = {{ v }}
  {% endfor %}
{% endblock %}

jinja2.exceptions.UndefinedError: 'my_dict' is undefined

The fix for #21 created this issue. Building the module when dictionary.items() is used raises an undefined exception because the variables were not present.

(likely a problem for any object.attribute access)

Please add type annotations and py.typed marker.

I love this library, it's very usefull!
I see it mosty supports type annotations, but there are some places where they're missing.
Unfortunately it doesn't contain py.typed marker file, so mypy is throwing an error.

Could you add this file and missing type annotations in future release?

Thanks in advance!

Parent macros are not available when rendering individual blocks

It would be a super cool feature if, in case of block-rendering, parent level macro imports were available for the blocks. For now, we either cannot use macros within blocks, which is very restricting, or need to import the macros in the blocks as well, which is uncomfortable and unintuitive. The worst part of it, is that one can never now for sure, without testing, if all block-rendering would work even if the complete template renders.

Desired syntax:

{% from 'macro.j2' import func %}

{{ func(global_variable) }}

{% block block_one %}
  {{ func(variable_1) }}
{% endblock %}

{% block block_two %}
  {{ func(variable_2) }}
{% endblock %}

Current workaround:

{% from 'macro.j2' import func %}

{{ func(global_variable) }}

{% block block_one %}
  {% from 'macro.j2' import func %}
  {{ func(variable_1) }}
{% endblock %}

{% block block_two %}
  {% from 'macro.j2' import func %}
  {{ func(variable_2) }}
{% endblock %}

Any way to work with `extends` / `super()`?

parent.html:

...
<title>{%block title%} - SiteName{%endblock%}</title>
...

child.html:

{%extends "parent.html"%}
{%block title%}Child{{super()}}{%endblock%}  <!-- full title is "Child - SiteName" -->

app.py:


@app.get("/child")
def get_child():
  return render_template("child.html") # WORKS
  # return render_block("child.html", "title") # FAILS

thanks!

Response headers are ignored when rendering a block

I am trying to send a response header when I return the response to render a block. But it looks like the code path doesn't pass through the response headers for rendering a block.

Can this be added? I'm happy to take a stab at a PR if you can give me a hint as to how to do this.

Using blocks inside loops?

Hi there, thanks a lot for this great library!

I'm trying to use it with htmx to manage a table, it all works well if I replace the entire table, but if I try to use a block for the single row, then jinja complains about the missing element in the context when rendering the entire page.

{% block projects_table %} <!-- this was used when replacing the entire table -->
<table id="projects_table">
  <thead>...</thead>
  <tbody>
    {% for project in projects %}
      {% block project_row %}  <!-- this i what I'm trying to use now -->
        <tr>
          <td><a href="/projects/{{ project.id }}">{{ project.code }}</a></td>
          <td>{{ project.name }}</td>
          <td>{{ project.created_at }}</td>
        </tr>
      {% endblock %}
    {% endfor %}
  </tbody>
</table>
{% endblock %}

The following is the htmx setup for the new project form;

<form method="post" hx-post="/projects" hx-target="#projects_table" hx-swap="beforeend">

The fastapi endpoints

@router.get("/projects")
async def list_projects(request: Request) -> Response:
    projects = [
        Project(id=i, code=f"Test {i}", name=f"Test Project {i}")
        for i in range(1, 10)
   ]
    return templates.TemplateResponse(
        "list_projects.html.jinja",
        {
            "request": request,
            "projects": projects,
        },
    )

@router.post("/projects")
async def create_project(request: Request) -> Response:
   project = Project(id=10, code="Test10", name="Test Project 10")
    return templates.TemplateResponse(
        "list_projects.html.jinja",
        {"request": request, "project": project},
        block_name="project_row",
    )

I got an error on the get /projects endpoint:

jinja2.exceptions.UndefinedError: 'project' is undefined

Am I using it wrong, or is this a bug?

Obviously, after writing all of this, I realized I can swap the loop and block and pass a single element list, but it feels like a workaround rather than the solution to this problem

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.