⚠️ disclaimer
I have no idea what is the correct way to do things. I probably do some things really wrong, and I will be glad to learn that. Here I'm just sharing my experience and where I'm now in using Flask for multiple small-to-medium projects (all basically CRUD apps). Your experience may be different, and I will gladly hear it.
✍️ writing Jinja as a proper Python developer
You probably heard about the DRY principle. I'm not going into the heated discussion if that's something you should code by. Actually, let's forget there's the DRY principle at all.
Let's just say that I'm a lazy person.
So, the idea of writing code I don't have to multiple times is, well, not appealing to me.
But it happens. Especially when the project is young and dynamic and you have no idea what code you will need, or how you will structure things.
One place I extremely hate writing code over and over again is HTML. There are so many <
s and >
s and other things. And soo much that's obvious and shouldn't be written.
you know - if you are writing the <a>
tag of course you will use href argument. Why is it even kwarg
? I would never be kwarg
if I was writing it in Python!
And as I'm programming Flask CRUD app, most of the time <a>
is pointing to some object show/edit page. Should I write
<a href="{{ url_for('UserView:show', id=user.id) }}> {{ user.name }} </a>"
every time? Absolutely not!
As I'm exposed to Ruby on Rails in my work, I've seen link_to
in templates a lot. And I thought: that's really nice! That's what I should be doing, right? Just write {{ link_to(user) }}
in my template, when I want a really simple link. Why can't I do that?
Let's look into what did I discover on my journey into great developer experience with Jinja templates.
The question was - how can I create some "helpers" like this in Jinja?
And the answer is...
There are many ways depending on context. Sigh.
Well, let's look into them.
partials
By partial, I mean a separate template that I include
in my "main" template. They are useful when there's some big part of a page (like a table of all users) that I use on multiple pages (index, show).
I can give them some variables using {% with users=... %}
, but if I try adding some complex logic, it's not really great.
macros
When you were reading this article, you probably thought "he's talking about macros, right?". Well, of course, macros are THE suggested way to handle a bit more generalized, repeated pieces of HTML.
It's mostly the only advice you get on StackOverflow (here and here just two examples). And macros are great! I used them happily for some time - having macro for rendering form fields, icons, and other repeated elements. I had macros that use other macros! But...
Jinja logic is somewhat.. ..more verbose than I'm used to from Python. Not a great code. And getting some more complex logic in, it's not really what you want.
Another thing with macros is that you have to import them into your templates. Normally it's just a matter of adding everything to base.html.j2
template, but as I use Turbo (another article on that), sometimes I render just a partial, so I need to import explicitly there too. I know that Zen of Python says "Explicit is better than implicit.", but in this case, I don't know. Having link_to
accessible globally seems like a sensible thing to me (and another zen says "Although practicality beats purity.").
So, how do we do that?
We will have to come back from the dark, messy realms of HTML to the sweet embrace of Python.
helper functions
I did know that I can register custom Jinja filters and test (you know, because I read Jinja API documentation more times than I would like to).
But for a long time one great method was hiding from me: application.add_template_global()
.
What is it? Simple, just a method to register custom functions to use in Jinja. They are used in code like macro, but they are your normal Python functions.
Nice, let's use that!
I created a new directory: app/components
and there created this file:
# app/components/links.py
from flask import Markup
def link_to(obj):
# (do some logic)
return Markup(some_html_code)
and then, to make things nice, I added:
# app/components/__init__.py
from .links import link_to
def register_all_components(application):
application.add_template_global(link_to)
and finally, in my app/__init__.py
(simplified):
# app/__init__.py
def create_app():
application = Flask()
(...)
from app.components import register_all_components
register_all_components(application)
(...)
Now I can use {{ link_to(something) }}
anywhere in my templates. Smooth!
You can probably guess what some logic
in link_to
does, and I don't want to include full code here as it's but sprawled, but basic functionality goes like this:
- I can pass either object or string to
link_to
- if it's a string
- it either includes
:
, so it's recognized as Flask-Classfull reference to route (eg. "UserView:index"), so it's converted tourl_for(something)
- or it's just pure URL, then I just pass it as it is
- it either includes
- or it is object
- then I get generated route - so, from
user
object I can expectUserView
, add:show
andid=user.id
. This logic is handled by UserPresenter which gives ususer.path_to_show
- then I get generated route - so, from
I was happy with this. But I wasn't happy for long. Because soon I got repeating code again, and it was just not beautiful enough. So my journey to perfect developer experience continued and I found...
kittens!
No, sadly kittens are of no direct help with HTML. They are way too cute for that.
We need to continue our search...
TemplateComponents
Have you never heard of TemplateComponents? That's understandable, they don't exist.
Inspiration for this pattern comes from Ruby on Rails, and specifically, gem(package) created and used by Github - ViewComponent.
As views
in Rails and Django are templates
in Flask (I don't know why I would be happy with MVC instead of MTV), let's call it TemplateComponents!
The idea is to move the core of these reusable pieces of HTML into Python objects, getting all the benefits of OOP.
How would that look?
Here's an example:
1️⃣ Components are classes, in my case located in app/components/
.
2️⃣ Component itself does some logic and then renders jinja template (with minimum logic from app/templates/components/
3️⃣ They are used by helper functions (defined in the same place), these helpers are registered by flask to be globally accessible, eg.(application.add_template_global(icon)
)
Here's an example how it can be set up:
# app/__init__.py
def create_app():
(...)
from app.components import register_all_components
register_all_components(application)
(...)
Nothing new here...
# app/components/__init__.py
from .links import link_to, link_to_edit
def register_all_components(application):
application.add_template_global(link_to)
application.add_template_global(link_to_edit)
Yeah, we know this part.
# app/components/links.py
from app.components import Component
# core
class Link(Component):
def __init__(self, path, value, **kwargs):
super(Link, self).__init__(**kwargs)
self.path = path
self.value = value
# helpers
def link_to(obj, **kwargs):
path = obj.path_to_show()
value = kwargs.pop("value", obj.default_value_link)
return Link(path=path, value=value, **kwargs).render()
def link_to_edit(obj, **kwargs):
from app.components import icon
path = obj.path_to_edit()
value = kwargs.pop("value", icon("edit"))
return Link(path=path, value=value, **kwargs).render()
Ok, here's something new. But Link itself doesn't have a lot of logic, so how..
# app/components/base.py
from flask import render_template as render
from flask import Markup
class Component:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.kwargs["data"] = self.kwargs.pop("data", {})
def render(self):
return Markup(render(self.template, **self.attributes))
@property
def template(self):
folder = getattr(self, "folder", f"{self.__class__.__name__.lower()}s")
file = getattr(self, "file", f"{self.__class__.__name__.lower()}")
return f"components/{folder}/{file}.html.j2"
@property
def attributes(self):
# All public variables of the view are passed to the template
class_attributes = self.__class__.__dict__
view_attributes = self.__dict__
all_attributes = class_attributes | view_attributes
public_attributes = {
k: all_attributes[k] for k in all_attributes if not k.startswith("_")
}
# kwargs has higher priority, therefore rewrites public attributes
merged_values = {**public_attributes, **self.kwargs}
return merged_values
Oh, right! Because some logic will probably be used in any component, so let's move that to Component class!
And here's a template it renders:
# app/templates/components/links/link.html.j2
<a
href="{{ path }}"
{% if class %} class="{{ class }}" {% endif %}
>{{ value }}</a>
Mind you, this is the first version of my Component system. I haven't even moved all my macros there, so there will be probably some changes and improvements. But the basic idea is here.
For now, I'm not pushing component classes to jinja context - so I cannot do {{ Link(something) }}
.
It would be possible, but for now, it makes sense to me to use them through helper functions such as link_to
and link_to_edit
.
Why am I happy (for now) with this approach?
- I can handle complex logic easily (as I'm using Python, not Jinja)
- there's a clear way to split logic for
- components in general (handled by Component class) - getting template name, rendering it
- kinds of components (handled by Link class for example) - possibly setting some defaults - style,...
- and specific helpers (link_to, link_to_edit) - setting specific values - path and defaults.
- it's easily testable!
- I can test class or helper. not doing it now as I only have simple use cases, but in the future, it will make sense - so I can check parts of my HTML in a fast unit test instead of a complicated slow FE test.
As always, I'm not saying this is the best approach. I was trying to show all the styles I used for this part of creating a web app. All may be useful, it depends on what you need.
Question for this post:
- how do you handle your templates?
- how important is "Explicit is better than implicit" zen important to you?
Hope to hear your ideas, experiences, and critique!
Top comments (0)