django / django/new-features

Split `django.template.base.Template` class into `Renderer` and `Template`

Open
#66 5 comments 5 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
188
Forks
7
PR merge metrics
No merged PRs in 30d

Description

### Code of Conduct

- [x] I agree to follow Django's Code of Conduct

### Feature Description

Split `django.template.base.Template` class into `Renderer` and `Template`

### Problem

## Background

Over at [django-components (DJC)](https://github.com/django-components/django-components), one of our key features is smart management of dependencies (JS/CSS files).

The idea is that:
1. You just define the JS/CSS files on the components themselves
```py
class Table(Component):
# Component's own JS/CSS
js_file = "table.js"
css_file = "table.css"

# Put any extra or 3rd party JS/CSS here
class Media:
js = [
"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4",
"https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js",
]
```
2. You render the component either with `Component.render()` or inside another template with `{% component %}` tag:
```py
html = Table.render()
```
```django
{% component "Table" / %}
```
3. And this HTML that gets served in the browser will automatically have all the JS/CSS from the components already included.

```html

...

...




```

There is more. We support HTML fragments/partials - we dedupe JS/CSS, and ensure that a component's JS/CSS is loaded in the browser only ever once. Even if you render that component as a fragment and insert it into the DOM multiple times. (See more on [Rendering JS/CSS](https://django-components.github.io/django-components/latest/concepts/advanced/rendering_js_css/#dependencies-strategies)).

However, we need the user to tell us whether we should render the component's JS/CSS as:
- A fragment to be inserted into the DOM via AJAX (ensuring the JS/CSS doesn't get loaded again if it was already)
- As part of the HTML document when the page is loaded the first time (document mode)
- Other

There are some differences, e.g. when the page is loaded the first time (AKA "document" mode), we include all the components' CSS, because omitting CSS would lead to [Flash of unstyled content](https://en.wikipedia.org/wiki/Flash_of_unstyled_content).

When rendering Components directly from Python, we have an input for that called `deps_strategy`:

```py
html = Table.render(
deps_strategy="fragment",
)
```

However, when the component is nested inside a template with `{% component %}` tag, there's no way to directly pass that information.

There are alternative ways to achieve this:
- in older versions of django-components (before v0.140), we had a Django middleware to address that. But it still wasn't great.
- Our user (web developer) can pass the result of the rendered template into our `render_dependencies()`, which post-processes the JS/CSS.

In v0.140 and later, we've taken following approach:
1. We know that all Django templates and DJC components go through Django's `Template.render()` in order to be rended.
2. So in DJC we monkeypatched Django's `Template.render()`, and we've moved the logic from our middleware `Template.render()`.
3. This way our users don't need to install the middleware.

And if they need to configure which "strategy" to use (fragment or document), they can also pass this info through the Context object:

```py
from django.shortcuts import render

context = {"DJC_DEPS_STRATEGY": fragment}
html = render(request, template, context)
```

## Problem

So as said above, in django-components we monkeymatch `Template.render()` so that we can smartly manage JS/CSS dependencies even when the components are nested in templates with the `{% component %}` tag.

[Today](https://github.com/django-components/django-components/issues/1323#issuecomment-3160412838) we came across an incompatibility with django-template-partial, because their partials don't use `Template.render()`, but instead they define their own `TemplateProxy` class, which *looks* like Template, but it doesn't share the same `Template.render()` method. It has both `.render()` and `_render()` methods, but they are copy-pasted.

The reason why `TemplateProxy` doesn't inherit from `Template` is that `Template` does a lot more, but `TemplateProxy needed just the render logic.

## Proposal

So as I mentioned in [that comment](https://github.com/django-components/django-components/issues/1323#issuecomment-3160412838), this incompatibility could be resolved if in Django we split the `django.template.base.Template` class into two:
- `Renderer` - Mixin that defines just `render()` and `_render()`
- `Template` - Inherits from `Renderer` and defines the rest:

```py
class Renderer:
def _render(self, context):
return self.nodelist.render(context)

def render(self, context):
"Display stage -- can be called many times"
with context.render_context.push_state(self):
if context.template is None:
with context.bind_template(self):
context.template_name = self.name
return self._render(context)
else:
return self._render(context)

class Template(Renderer):
...
```

With this, we could resolve the issue as:

- `TemplateProxy` of django-template-partials could inherit from `Renderer` instead of re-defining the logic from `Template.render()`
- They would avoid potential drift by not having to re-define the logic.
- django-components could monkeypatch this `Renderer` instead of `Template`, thus ensuring that it works also with django-template-partials
- django-components could be more permissive in integrating with other django libraries - Instead of needing others to use `Template`, it would be reduced to only need others to use classes that inherit from `Renderer`.

### Request or proposal

proposal

### Additional Details

_No response_

### Implementation Suggestions

_No response_

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.