askama
                                
                                 askama copied to clipboard
                                
                                    askama copied to clipboard
                            
                            
                            
                        Add block-based partials
Adds support for rendering partial templates from existing templates without requiring that the template be split into multiple, included files. This feature is enabled using the new block sub-attribute on the template() attribute, which causes the generated template to only include the named block (and its contents). This works especially well with client-side frameworks like htmx that encourage returning only changed subsections of a previous HTML file.
Prior art that I reviewed when implementing this feature:
- htmx tweet proposing a feature like this in server-side templating languages: https://twitter.com/htmx_org/status/1419657468301348866
- DjPj uses the blockandendblocktags to select partials.
- Django Mako Plus also provides support for partials using its version of the blocktags.
I tried to keep this change as simple as possible and implemented the feature by modifying the generator to select only the named block's nodes when the partial feature is in use. I believe that this ensures that the entire rest of the generator/handler functions see only those nodes, just as if they were the only contents in the template source.
I chose to name the new template() attribute block since that is what is being targeted by the generator, but I think that the name partial might also be a good choice.
I am open to any and all suggestions to improve this feature and welcome any comments that you might have. Thank you for taking the time to review this feature/feature request!
This broadly makes sense to me, and I think I prefer the block name over partial. @vallentin @Kijewski what do you think? Does this fit with Askama's design? Will it not cause to many problems with other features in the future?
For some reason I misinterpreted this as "conditional blocks", and was about to suggest {% elseblock %}.
I didn't read through the code yet, but will do later when I have time. So what I'm about to say might already be addressed.
So given block = "contents", then it finds the block with the name contents, and only generate rendering code for that block, and nothing else. Right?
If so, then I could imagine issues, if the block uses variables defined outside of the block.
Additionally, if the block extends the parent block, i.e. you extend a template, and then replace a block from the parent template with e.g. {% block contents %} {{ super() }} {% endblock %}. Does the implementation handle that correct, additionally, if the parent block uses variables defined outside that block's scope, does that also still work?
Thank you @vallentin for the insightful questions!
So given
block = "contents", then it finds theblockwith the namecontents, and only generate rendering code for thatblock, and nothing else. Right?
That is correct. The code acts as if the source template only contains the contents of the named block. No other content from the original template is included.
If so, then I could imagine issues, if the
blockuses variables defined outside of theblock.
Also correct, the template will fail to compile because the let binding is not part of the block that was extracted.
Additionally, if the
blockextends the parentblock, i.e. you extend a template, and then replace ablockfrom the parent template with e.g.{% block contents %} {{ super() }} {% endblock %}. Does the implementation handle that correct,
Calling super() does not work; Askama does not think that you are in a block and so it fails with a "cannot call 'super()' outside block" error.
additionally, if the parent
blockuses variables defined outside thatblock's scope, does that also still work?
No, that doesn't work either, because the code doesn't even process the parent block, it only renders what it sees in the child.
These questions have exposed an interesting issue with using the block construct for this feature: this change is really just using the blocks as a naming construct, and breaks all of the template inheritance features that blocks were designed for. I based this implementation on what I saw in the Python versions of this feature, and I don't use template inheritance (yet?), so none of these issues came up in my prototyping.
I am not sure how to best resolve this issue. I could create a new {% partial %} .. {% endpartial %} construct and use that to select the partial, but I think it would have all of the same downsides (external let variables not working inside the partial). I don't think it would be possible to filter in the output phase without extensive changes to the generator to output (for example) let bindings, but not anything that could possibly render text.
So far I am fine with the limitations of the current approach, but I agree that it could create confusion with the normal use of the block feature. Let me know how you think I should proceed. This feature has cleaned up my code quite a bit, but admittedly my sample project is small and I might run into these issues you called out as the project grows in size.
I think I'd also be fine with these limitations as long as they cause compile-time errors; then it's sort of at the author's peril. I don't think there is a sensible way that Askama could solve these issues anyway?
On the other hand, this seems like a really nichey feature since we already have includes and macros...
I think I'd also be fine with these limitations as long as they cause compile-time errors; then it's sort of at the author's peril.
I agree, these are not silent errors, and as long as the author understands the intent of the feature then the limitations should not be surprising. On the other hand, it could be confusing for the author if it turns out that there are legitimate use cases for out-of-block variables or super-callable templates in partials that I haven't seen yet.
On the other hand, this seems like a really nichey feature since we already have includes and macros...
That is certainly true. Technologies like htmx are new and not widely known in the Rust community. I do think that Rust is a good fit for this pattern and I hope others discover it as well, but we're not there yet. Ultimately you and the other Askama maintainers need to decide if you want a feature like this in the core of the library. Thankfully Cargo has Git dependencies, which means that I can point at my fork and keep using this feature as long as I want, so I am fine with any decision that you make.
That is certainly true. Technologies like htmx are new and not widely known in the Rust community.
I mean, this is essentially a way of doing functional reactive programming with Askama templates, right? I've long wanted to work on something like that but then it's still unclear if this is the right way to do that. Zooming out, I'd definitely expect that the entire template context is available (including variables declared outside the block that you're rendering). So maybe the PR should change to run the entire template but inhibit emitting rendered text outside of the selected block?
I'm not sure if this adds to the conversation at all but I recently noticed minijinja just added this sort of functionality: https://twitter.com/mitsuhiko/status/1664939991158456323  (relevant github parts: https://github.com/mitsuhiko/minijinja/issues/260 & https://github.com/mitsuhiko/minijinja/pull/262)
I'm not technically skilled enough to understand the differences between your approaches and if this helps show a path forward that alleviates your concerns or whatnot. I'm just a big fan of Askama and am starting to explore htmx in my app that uses Askama and don't want to switch if this becomes a blocker. Anyways, thought it might help out to show how others are approaching this.
Thanks for your work on this library!
Since it seems like this PR is dead, I made a prototype with the proposed changes, but had a few thoughts on if improving the API would be nice. I'll follow up with a draft PR.
Currently it has the same API as this PR does, but it also means you need to make a new struct for the block partial/fragment even if it has the same fields (which it probably should if you're rendering fragments like this). That might be fine, but I thought a nice improvement would be to either implement a render_block method on the struct, or introduce a trait + derive macro with it. Maybe like:
#[derive(Template, TemplateBlock)]
#[template(path = "...")]
#[blocks("block_1", "block_2")]
struct MyTemplate {};
That is probably a bit hairier, and would add a few branches at runtime if you want to support multiple blocks like that in one method. But if there's any other ideas on improving it, that would be great. Or if sticking with the current API would be best.