From DTOs to UI Components

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.