After talking about how to define DTOs in the last article, this article focuses on how we can go even further and use those DTOs to render Django components.
Keep data layer separated from template layer
The first approach is the more explicit one that promotes "loose coupling" of the data and template layers: We populate a DTO (based on the DTO article, we go with the Article
and Author
) in the get_context_data
method of a Django view and only use it as a datastore for an included template.
Let's say in our views.py
we define BlogView
that should be able to render Article
DTOs:
from datetime import datetime
from django.views.generic import TemplateView
from blog.dtos import Article, Author
def BlogView(TemplateView):
template_name = 'blog/blog.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
author = Author(
name='Nico',
email='nico@example.blog',
)
context['articles'] = [Article(
title='From DTOs to UI Components',
content='After talking about how to define DTOs in the last article...',
author=author,
published_at=datetime.now()
)]
return context
We could have a special template file blog/article.html
on how an article should be rendered:
<article class="article">
<h1 class="article__title">{{ title }}</h1>
<section class="article__content">{{ content }}</section>
<time class="article__published">Published at: {{ published_at }}</time>
<div class="author article__author">
<span class="author__name">{{ author.name }}</span>
<span class="author__email">({{ author.email }})</span>
</div>
</article>
Our main blog/blog.html
template file would look like this then:
<main class="blog">
{% for article in articles %}
<div class="blog__article">
{% include 'blog/article.html' with title=article.title content=article.content author=article.author published_at=article.published_at %}
</div>
{% endif %}
</main>
We could simplify the include tag a little by just passing in the whole DTO instead of it's properties separately: {% include 'blog/article.html' with dto=article %}
And in the article template file we would need to write {{ dto.title }}
instead of {{ title }}
then.
Tightly coupled components
The second approach breaks a little with our strict separation between layers: This time we create a base Component
class that lets us define a template for each DTO which transforms this into a UI Component rather than only a data container. There will be a middle way presented at the end of this article though.
The Component
class would look similar to this:
from django.templates import render_to_string
from django.utils.functional import cached_property
class Component:
template_name = None
def __init__(self, request=None, **kwargs):
super().__init__(**kwargs)
self.request = request
def __str__(self):
return self.render()
def __repr__(self):
class_name = self.__class__.__name__
kwargs = ', '.join([f'{k}={v}' for k, v in self.__dict__.items()])
return f'{class_name}({kwargs})'
@cached_property
def render(self):
return render_to_string(
self.template_name,
self.__dict__,
request=self.request
)
We could then change our Article
DTO to inherit from this base class then:
class Article(Component):
template_name = 'blog/article.html'
// ...
And our blog/blog.html
file would use it like this:
<main class="blog">
{% for article in articles %}
<div class="blog__article">
{{ article }}
</div>
{% endif %}
</main>
This works because the classes __str__
will get called which implicitly renders the DTO's template with all the stored properties and optionally with a given request.
Component helper class
The third way would be instead of sub-classing Component
we could just use it as a helper class. Therefore we would change it to look like this:
class Component:
def __init__(self, template_name, dto, request=None):
self.template_name = template_name
self.dto = dto
self.request = request
def __str__(self):
return self.render()
def __repr__(self):
class_name = self.dto.__class__.__name__
kwargs = ', '.join([f'{k}={v}' for k, v in self.dto.__dict__.items()])
return f'Component<{class_name}>({kwargs})'
@cached_property
def render(self):
return render_to_string(
self.template_name,
self.dto.__dict__,
request=self.request
)
In the get_context_data
method it would be used like this then:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
author = Author(
name='Nico',
email='nico@example.blog',
)
article = Article(
title='From DTOs to UI Components',
content='After talking about how to define DTOs in the last article...',
author=author,
published_at=datetime.now()
)
context['articles'] = [
Component('blog/article.html', article, request=self.request)
]
return context
The blog/blog.html
would look the same as in the second approach.
Whichever way you prefer - it will be better than directly passing model instances to the template file and following its foreign keys without any pre-loading or optimizations.