refactor: rename template_name to template_file (#878)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2025-01-01 17:06:14 +01:00 committed by GitHub
parent b99e32e9d5
commit d94a459c8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 251 additions and 138 deletions

View file

@ -20,6 +20,12 @@
#### Refactor
- The canonical way to define a template file was changed from `template_name` to `template_file`,
to align with the rest of the API.
`template_name` remains for backwards compatibility. When you get / set `template_name`,
internally this is proxied to `template_file`.
- The undocumented `Component.component_id` was removed. Instead, use `Component.id`. Changes:
- While `component_id` was unique every time you instantiated `Component`, the new `id` is unique

View file

@ -18,7 +18,7 @@ A component in django-components can be as simple as a Django template and Pytho
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
```
Or a combination of Django template, Python, CSS, and Javascript:
@ -46,7 +46,7 @@ document.querySelector(".calendar").onclick = function () {
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
```
@ -77,7 +77,6 @@ class Calendar(Component):
"""
```
## Features
1. 🧩 **Reusability:** Allows creation of self-contained, reusable UI elements.
@ -97,13 +96,14 @@ Django-components can be particularly useful for larger Django projects that req
## Quickstart
django-components lets you create reusable blocks of code needed to generate the front end code you need for a modern app.
django-components lets you create reusable blocks of code needed to generate the front end code you need for a modern app.
Define a component in `components/calendar/calendar.py` like this:
```python
@register("calendar")
class Calendar(Component):
template_name = "template.html"
template_file = "template.html"
def get_context_data(self, date):
return {"date": date}

View file

@ -14,7 +14,7 @@ from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_name = "template.html"
template_file = "template.html"
# This component takes one parameter, a date string to show in the template
def get_context_data(self, date):
@ -127,6 +127,7 @@ NOTE: The Library instance can be accessed under `library` attribute of `Compone
When you are creating an instance of `ComponentRegistry`, you can define the components' behavior within the template.
The registry accepts these settings:
- `context_behavior`
- `tag_formatter`

View file

@ -10,7 +10,7 @@ HTML / JS / CSS with a component:
[`Component.css`](../../reference/api.md#django_components.Component.css) and
[`Component.js`](../../reference/api.md#django_components.Component.js) to define the main HTML / CSS / JS for a component
as inlined code.
- You can set [`Component.template_name`](../../reference/api.md#django_components.Component.template_name),
- You can set [`Component.template_file`](../../reference/api.md#django_components.Component.template_file),
[`Component.css_file`](../../reference/api.md#django_components.Component.css_file) and
[`Component.js_file`](../../reference/api.md#django_components.Component.js_file) to define the main HTML / CSS / JS
for a component in separate files.
@ -22,7 +22,7 @@ HTML / JS / CSS with a component:
You **cannot** use both inlined code **and** separate file for a single language type:
- You can only either set `Component.template` or `Component.template_name`
- You can only either set `Component.template` or `Component.template_file`
- You can only either set `Component.css` or `Component.css_file`
- You can only either set `Component.js` or `Component.js_file`
@ -51,7 +51,7 @@ HTML / JS / CSS with a component:
As seen in the [getting started example](../getting_started/your_first_component.md), to associate HTML / JS / CSS
files with a component, you can set them as
[`Component.template_name`](../../reference/api.md#django_components.Component.template_name),
[`Component.template_file`](../../reference/api.md#django_components.Component.template_file),
[`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
and
[`Component.css_file`](../../reference/api.md#django_components.Component.css_file) respectively:
@ -61,7 +61,7 @@ from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_name = "template.html"
template_file = "template.html"
css_file = "style.css"
js_file = "script.js"
```
@ -70,7 +70,7 @@ In the example above, we defined the files relative to the directory where the c
Alternatively, you can specify the file paths relative to the directories set in
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
or
or
[`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs).
If you specify the paths relative to component's directory, django-componenents does the conversion automatically
@ -85,7 +85,7 @@ from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_name = "calendar/template.html"
template_file = "calendar/template.html"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
```
@ -94,59 +94,59 @@ class Calendar(Component):
**File path resolution in-depth**
At component class creation, django-components checks all file paths defined on the component (e.g. `Component.template_name`).
At component class creation, django-components checks all file paths defined on the component (e.g. `Component.template_file`).
For each file path, it checks if the file path is relative to the component's directory.
And such file exists, the component's file path is re-written to be defined relative to a first matching directory
in [`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
or
or
[`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs).
**Example:**
```py title="[root]/components/mytable/mytable.py"
class MyTable(Component):
template_name = "mytable.html"
template_file = "mytable.html"
```
1. Component `MyTable` is defined in file `[root]/components/mytable/mytable.py`.
2. The component's directory is thus `[root]/components/mytable/`.
3. Because `MyTable.template_name` is `mytable.html`, django-components tries to
3. Because `MyTable.template_file` is `mytable.html`, django-components tries to
resolve it as `[root]/components/mytable/mytable.html`.
4. django-components checks the filesystem. If there's no such file, nothing happens.
5. If there IS such file, django-components tries to rewrite the path.
6. django-components searches `COMPONENTS.dirs` and `COMPONENTS.app_dirs` for a first
directory that contains `[root]/components/mytable/mytable.html`.
7. It comes across `[root]/components/`, which DOES contain the path to `mytable.html`.
8. Thus, it rewrites `template_name` from `mytable.html` to `mytable/mytable.html`.
8. Thus, it rewrites `template_file` from `mytable.html` to `mytable/mytable.html`.
NOTE: In case of ambiguity, the preference goes to resolving the files relative to the component's directory.
## Defining additional JS and CSS files
Each component can have only a single template, and single main JS and CSS. However, you can define additional JS or CSS
Each component can have only a single template, and single main JS and CSS. However, you can define additional JS or CSS
using the nested [`Component.Media` class](../../../reference/api#django_components.Component.Media).
This `Media` class behaves similarly to
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition):
- Paths are generally handled as static file paths, and resolved URLs are rendered to HTML with
`media_class.render_js()` or `media_class.render_css()`.
`media_class.render_js()` or `media_class.render_css()`.
- A path that starts with `http`, `https`, or `/` is considered a URL, skipping the static file resolution.
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
- A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
However, there's a few differences from Django's Media class:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list,
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
3. Our Media class does NOT support
[Django's `extend` keyword](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend)
[Django's `extend` keyword](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend)
```py
class MyTable(Component):
@ -268,7 +268,7 @@ class ModuleJsPath:
@register("calendar")
class Calendar(Component):
template_name = "calendar/template.html"
template_file = "calendar/template.html"
def get_context_data(self, date):
return {
@ -314,7 +314,7 @@ class MyMedia(Media):
@register("calendar")
class Calendar(Component):
template_name = "calendar/template.html"
template_file = "calendar/template.html"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
@ -331,27 +331,28 @@ class Calendar(Component):
Component's HTML / CSS / JS is resolved and loaded lazily.
This means that, when you specify any of
[`template_name`](../../reference/api.md#django_components.Component.template_name),
[`template_file`](../../reference/api.md#django_components.Component.template_file),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css_file`](../../reference/api.md#django_components.Component.css_file),
or [`Media.js/css`](../../reference/api.md#django_components.Component.Media),
these file paths will be resolved only once you either:
1. Access any of the following attributes on the component:
- [`media`](../../reference/api.md#django_components.Component.media),
[`template`](../../reference/api.md#django_components.Component.template),
[`template_name`](../../reference/api.md#django_components.Component.template_name),
[`js`](../../reference/api.md#django_components.Component.js),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css`](../../reference/api.md#django_components.Component.css),
[`css_file`](../../reference/api.md#django_components.Component.css_file)
- [`media`](../../reference/api.md#django_components.Component.media),
[`template`](../../reference/api.md#django_components.Component.template),
[`template_file`](../../reference/api.md#django_components.Component.template_file),
[`js`](../../reference/api.md#django_components.Component.js),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css`](../../reference/api.md#django_components.Component.css),
[`css_file`](../../reference/api.md#django_components.Component.css_file)
2. Render the component.
Once the component's media files have been loaded once, they will remain in-memory
on the Component class:
- HTML from [`Component.template_name`](../../reference/api.md#django_components.Component.template_name)
- HTML from [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
will be available under [`Component.template`](../../reference/api.md#django_components.Component.template)
- CSS from [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
will be available under [`Component.css`](../../reference/api.md#django_components.Component.css)
@ -359,7 +360,7 @@ on the Component class:
will be available under [`Component.js`](../../reference/api.md#django_components.Component.js)
Thus, whether you define HTML via
[`Component.template_name`](../../reference/api.md#django_components.Component.template_name)
[`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
or [`Component.template`](../../reference/api.md#django_components.Component.template),
you can always access the HTML content under [`Component.template`](../../reference/api.md#django_components.Component.template).
And the same applies for JS and CSS.
@ -371,7 +372,7 @@ And the same applies for JS and CSS.
# are not yet loaded!
@register("calendar")
class Calendar(Component):
template_name = "calendar/template.html"
template_file = "calendar/template.html"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
@ -395,7 +396,7 @@ print(Calendar.css)
django-components assumes that the component's media files like `js_file` or `Media.js/css` are static.
If you need to dynamically change these media files, consider instead defining multiple Components.
Modifying these files AFTER the component has been loaded at best does nothing. However, this is
an untested behavior.

View file

@ -3,10 +3,13 @@ title: Single-file components
weight: 1
---
Components can be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_name` and `Media`. For example, here's the calendar component from above, defined in a single file:
Components can be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_file`, `js_file`, and `css_file`.
For example, here's the calendar component from
the [Getting started](../getting_started/your_first_component.md) tutorial,
defined in a single file:
```python title="[project root]/components/calendar.py"
# In a file called [project root]/components/calendar.py
from django_components import Component, register, types
@register("calendar")
@ -17,18 +20,27 @@ class Calendar(Component):
}
template: types.django_html = """
<div class="calendar-component">Today's date is <span>{{ date }}</span></div>
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
"""
css: types.css = """
.calendar-component { width: 200px; background: pink; }
.calendar-component span { font-weight: bold; }
.calendar {
width: 200px;
background: pink;
}
.calendar span {
font-weight: bold;
}
"""
js: types.js = """
(function(){
if (document.querySelector(".calendar-component")) {
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
if (document.querySelector(".calendar")) {
document.querySelector(".calendar").onclick = () => {
alert("Clicked calendar!");
};
}
})()
"""

View file

@ -49,6 +49,7 @@ Inside `calendar.css`, write:
Be sure to prefix your rules with unique CSS class like `calendar`, so the CSS doesn't clash with other rules.
<!-- TODO: UPDATE AFTER SCOPED CSS ADDED -->
!!! note
Soon, django-components will automatically scope your CSS by default, so you won't have to worry
@ -99,6 +100,7 @@ an [anonymous self-invoking function](https://developer.mozilla.org/en-US/docs/G
This makes all variables defined only be defined inside this component and not affect other components.
<!-- TODO: UPDATE AFTER FUNCTIONS WRAPPED -->
!!! note
Soon, django-components will automatically wrap your JS in a self-invoking function by default
@ -172,7 +174,6 @@ So in your HTML, you may see something like this:
</html>
```
### 4. Link JS and CSS to a component
Finally, we return to our Python component in `calendar.py` to tie this together.
@ -184,7 +185,7 @@ and [`css_file`](../../../reference/api#django_components.Component.css_file) at
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
js_file = "calendar.js" # <--- new
css_file = "calendar.css" # <--- new
@ -208,7 +209,6 @@ automatically embed the associated JS and CSS.
(e.g. `[your apps]/components` dir and `[project root]/components`)
3. Relative to any of the directories defined by `STATICFILES_DIRS`.
<!-- TODO: UPDATE AFTER AT LEAST ONE IMPLEMENTED
!!! info
@ -243,7 +243,7 @@ with a few differences:
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
@ -273,7 +273,6 @@ class Calendar(Component):
and/or [`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `[your apps]/components` dir and `[project root]/components`)
!!! info
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/).
@ -312,7 +311,7 @@ Additionally to `Media.js` applies that:
1. JS in `Media.js` is executed **before** the component's primary JS.
2. JS in `Media.js` is executed **in the same order** as it was defined.
3. If there is multiple components that specify the same JS path or URL in `Media.js`,
3. If there is multiple components that specify the same JS path or URL in `Media.js`,
this JS will be still loaded and executed only once.
Putting all of this together, our `Calendar` component above would render HTML like so:
@ -322,8 +321,12 @@ Putting all of this together, our `Calendar` component above would render HTML l
<head>
...
<!-- CSS from Media.css -->
<link href="/static/path/to/shared.css" media="all" rel="stylesheet">
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" media="all" rel="stylesheet">
<link href="/static/path/to/shared.css" media="all" rel="stylesheet" />
<link
href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"
media="all"
rel="stylesheet"
/>
<!-- CSS from Component.css_file -->
<style>
.calendar {

View file

@ -8,7 +8,7 @@ our colleague - The calendar date needs to be shown on 3 different pages:
1. On one page, it needs to be shown as is
2. On the second, the date needs to be **bold**
3. On the third, the date needs to be in *italics*
3. On the third, the date needs to be in _italics_
As a reminder, this is what the component's template looks like:
@ -43,7 +43,7 @@ In the example below we introduce two tags that work hand in hand to make this w
- `{% slot <name> %}`/`{% endslot %}`: Declares a new slot in the component template.
- `{% fill <name> %}`/`{% endfill %}`: (Used inside a [`{% component %}`](../../reference/template_tags.md#component)
tag pair.) Fills a declared slot with the specified content.
tag pair.) Fills a declared slot with the specified content.
### 2. Add a slot tag
@ -122,7 +122,7 @@ Which will render as:
{% endcomponent %}
```
2. Implicitly as the [default slot](../fundamentals/slots.md#default-slot) (Omitting the
2. Implicitly as the [default slot](../fundamentals/slots.md#default-slot) (Omitting the
[`{% fill %}`](../../reference/template_tags.md#fill) tag)
```htmldjango
{% component "calendar" date="2024-12-13" %}
@ -177,7 +177,7 @@ def to_workweek_date(d: date):
@register("calendar")
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
...
def get_context_data(self, date: date, extra_class: str | None = None):
workweek_date = to_workweek_date(date)
@ -187,7 +187,7 @@ class Calendar(Component):
}
```
And the issue is that in our template, we used the `date` value that we used *as input*,
And the issue is that in our template, we used the `date` value that we used _as input_,
which is NOT the same as the `date` variable used inside Calendar's template.
### 5. Adding data to slots

View file

@ -31,7 +31,7 @@ from django_components import Component, register # <--- new
@register("calendar") # <--- new
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"

View file

@ -23,7 +23,7 @@ from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
...
def get_context_data(self):
return {
@ -71,7 +71,7 @@ from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
...
def get_context_data(self, date: date, extra_class: str | None = None):
return {
@ -135,7 +135,7 @@ We need to group the list items by size into following buckets by population:
- 0-10,000,000
- 10,000,001-20,000,000
- 20,000,001-30,000,000
- 20,000,001-30,000,000
- +30,000,001
So we want to end up with following data:
@ -179,7 +179,7 @@ def group_by_pop(data):
@register("population_table")
class PopulationTable(Component):
template_name = "population_table.html"
template_file = "population_table.html"
def get_context_data(self, data):
return {
@ -200,7 +200,7 @@ def to_workweek_date(d: date):
@register("calendar")
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
...
def get_context_data(self, date: date, extra_class: str | None = None):
workweek_date = to_workweek_date(date) # <--- new

View file

@ -15,7 +15,7 @@ A component in django-components can be as simple as a Django template and Pytho
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
```
Or a combination of Django template, Python, CSS, and Javascript:
@ -43,7 +43,7 @@ document.querySelector(".calendar").onclick = function () {
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
```
@ -80,7 +80,6 @@ class Calendar(Component):
[syntax highlighting](../../guides/setup/syntax_highlight.md) for better experience.
However, autocompletion / intellisense does not work with syntax highlighting.
We'll start by creating a component that defines only a Django template:
### 1. Create project structure
@ -124,14 +123,14 @@ when creating an instance of this component.
In `calendar.py`, create a subclass of [Component](../../../reference/api#django_components.Component)
to create a new component.
To link the HTML template with our component, set [`template_name`](../../../reference/api#django_components.Component.template_name)
To link the HTML template with our component, set [`template_file`](../../../reference/api#django_components.Component.template_file)
to the name of the HTML file.
```python title="[project root]/components/calendar/calendar.py"
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
```
!!! note
@ -156,7 +155,7 @@ will become available within the template as variables, e.g. as `{{ date }}`.
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
def get_context_data(self):
return {

View file

@ -65,7 +65,7 @@
```py
@register("my_comp")
class MyComp(Component):
template_name = "abc.html"
template_file = "abc.html"
```
Then:
@ -112,7 +112,7 @@
````py
@register("my_comp")
class MyComp(Component):
template_name = """
template_file = """
{% extends "abc.html" %}
{% block inner %}
@ -129,7 +129,7 @@
```py
@register("my_comp")
class MyComp(Component):
template_name = """
template_file = """
{% extends "abc.html" %}
{% load component_tags %}

View file

@ -2,11 +2,11 @@
title: Welcome to Django Components
weight: 1
---
<img src="https://raw.githubusercontent.com/EmilStenstrom/django-components/master/logo/logo-black-on-white.svg" alt="django-components" style="max-width: 100%; background: white; color: black;">
[![PyPI - Version](https://img.shields.io/pypi/v/django-components)](https://pypi.org/project/django-components/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-components)](https://pypi.org/project/django-components/) [![PyPI - License](https://img.shields.io/pypi/l/django-components)](https://github.com/EmilStenstrom/django-components/blob/master/LICENSE/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/django-components)](https://pypistats.org/packages/django-components) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/EmilStenstrom/django-components/tests.yml)](https://github.com/EmilStenstrom/django-components/actions/workflows/tests.yml)
django-components introduces component-based architecture to Django's server-side rendering.
It combines Django's templating system with the modularity seen in modern frontend frameworks like Vue or React.
@ -22,7 +22,7 @@ A component in django-components can be as simple as a Django template and Pytho
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
```
Or a combination of Django template, Python, CSS, and Javascript:
@ -50,7 +50,7 @@ document.querySelector(".calendar").onclick = function () {
from django_components import Component
class Calendar(Component):
template_name = "calendar.html"
template_file = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
```
@ -100,13 +100,14 @@ Django-components can be particularly useful for larger Django projects that req
## Quickstart
django-components lets you create reusable blocks of code needed to generate the front end code you need for a modern app.
django-components lets you create reusable blocks of code needed to generate the front end code you need for a modern app.
Define a component in `components/calendar/calendar.py` like this:
```python
@register("calendar")
class Calendar(Component):
template_name = "template.html"
template_file = "template.html"
def get_context_data(self, date):
return {"date": date}

View file

@ -6,8 +6,8 @@ class Calendar(Component):
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
# will be automatically found.
#
# `template_name` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
template_name = "calendar/calendar.html"
# `template_file` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
template_file = "calendar/calendar.html"
css_file = "calendar/calendar.css"
js_file = "calendar/calendar.js"
@ -31,8 +31,8 @@ class CalendarRelative(Component):
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
# will be automatically found.
#
# `template_name` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
template_name = "calendar.html"
# `template_file` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
template_file = "calendar.html"
css_file = "calendar.css"
js_file = "calendar.js"

View file

@ -6,8 +6,8 @@ class CalendarNested(Component):
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
# will be automatically found.
#
# `template_name` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
template_name = "calendar.html"
# `template_file` can be relative to dir where `calendar.py` is, or relative to COMPONENTS.dirs
template_file = "calendar.html"
css_file = "calendar.css"
js_file = "calendar.js"

View file

@ -5,4 +5,4 @@ from django_components import Component, register
class Todo(Component):
# Templates inside `[your apps]/components` dir and `[project root]/components` dir
# will be automatically found.
template_name = "todo/todo.html"
template_file = "todo/todo.html"

View file

@ -158,14 +158,32 @@ class ComponentVars(NamedTuple):
"""
# Descriptor to pass getting/setting of `template_name` onto `template_file`
class ComponentTemplateNameDescriptor:
def __get__(self, instance: Optional["Component"], cls: Type["Component"]) -> Any:
obj = instance if instance is not None else cls
return obj.template_file # type: ignore[attr-defined]
def __set__(self, instance_or_cls: Union["Component", Type["Component"]], value: Any) -> None:
cls = instance_or_cls if isinstance(instance_or_cls, type) else instance_or_cls.__class__
cls.template_file = value
class ComponentMeta(ComponentMediaMeta):
pass
def __new__(mcs, name: Any, bases: Tuple, attrs: Dict) -> Any:
# If user set `template_name` on the class, we instead set it to `template_file`,
# because we want `template_name` to be the descriptor that proxies to `template_file`.
if "template_name" in attrs:
attrs["template_file"] = attrs.pop("template_name")
attrs["template_name"] = ComponentTemplateNameDescriptor()
return super().__new__(mcs, name, bases, attrs)
# NOTE: We use metaclass to automatically define the HTTP methods as defined
# in `View.http_method_names`.
class ComponentViewMeta(type):
def __new__(cls, name: str, bases: Any, dct: Dict) -> Any:
def __new__(mcs, name: str, bases: Any, dct: Dict) -> Any:
# Default implementation shared by all HTTP methods
def create_handler(method: str) -> Callable:
def handler(self, request: HttpRequest, *args: Any, **kwargs: Any): # type: ignore[no-untyped-def]
@ -179,7 +197,7 @@ class ComponentViewMeta(type):
if method_name not in dct:
dct[method_name] = create_handler(method_name)
return super().__new__(cls, name, bases, dct)
return super().__new__(mcs, name, bases, dct)
class ComponentView(View, metaclass=ComponentViewMeta):
@ -205,14 +223,14 @@ class Component(
# PUBLIC API (Configurable by users)
# #####################################
template_name: Optional[str] = None
template_file: Optional[str] = None
"""
Filepath to the Django template associated with this component.
The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`.
Only one of [`template_name`](../api#django_components.Component.template_name),
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -221,13 +239,27 @@ class Component(
```py
class MyComponent(Component):
template_name = "path/to/template.html"
template_file = "path/to/template.html"
def get_context_data(self):
return {"name": "World"}
```
"""
# NOTE: This attribute is managed by `ComponentTemplateNameDescriptor` that's set in the metaclass.
# But we still define it here for documenting and type hinting.
template_name: Optional[str]
"""
Alias for [`template_file`](../api#django_components.Component.template_file).
For historical reasons, django-components used `template_name` to align with Django's
[TemplateView](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.TemplateView).
`template_file` was introduced to align with `js/js_file` and `css/css_file`.
Setting and accessing this attribute is proxied to `template_file`.
"""
def get_template_name(self, context: Context) -> Optional[str]:
"""
Filepath to the Django template associated with this component.
@ -235,7 +267,7 @@ class Component(
The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`.
Only one of [`template_name`](../api#django_components.Component.template_name),
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -246,7 +278,7 @@ class Component(
"""
Inlined Django template associated with this component. Can be a plain string or a Template instance.
Only one of [`template_name`](../api#django_components.Component.template_name),
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -266,7 +298,7 @@ class Component(
"""
Inlined Django template associated with this component. Can be a plain string or a Template instance.
Only one of [`template_name`](../api#django_components.Component.template_name),
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -596,21 +628,21 @@ class Component(
return ctx.is_filled
# NOTE: When the template is taken from a file (AKA specified via `template_name`),
# NOTE: When the template is taken from a file (AKA specified via `template_file`),
# then we leverage Django's template caching. This means that the same instance
# of Template is reused. This is important to keep in mind, because the implication
# is that we should treat Templates AND their nodelists as IMMUTABLE.
def _get_template(self, context: Context) -> Template:
# Resolve template name
template_name = self.template_name
if self.template_name is not None:
template_file = self.template_file
if self.template_file is not None:
if self.get_template_name(context) is not None:
raise ImproperlyConfigured(
"Received non-null value from both 'template_name' and 'get_template_name' in"
"Received non-null value from both 'template_file' and 'get_template_name' in"
f" Component {type(self).__name__}. Only one of the two must be set."
)
else:
template_name = self.get_template_name(context)
template_file = self.get_template_name(context)
# Resolve template str
template_input = self.template
@ -625,14 +657,14 @@ class Component(
template_getter = getattr(self, "get_template_string", self.get_template)
template_input = template_getter(context)
if template_name is not None and template_input is not None:
if template_file is not None and template_input is not None:
raise ImproperlyConfigured(
f"Received both 'template_name' and 'template' in Component {type(self).__name__}."
f"Received both 'template_file' and 'template' in Component {type(self).__name__}."
" Only one of the two must be set."
)
if template_name is not None:
return get_template(template_name).template
if template_file is not None:
return get_template(template_file).template
elif template_input is not None:
# We got template string, so we convert it to Template
@ -644,7 +676,7 @@ class Component(
return template
raise ImproperlyConfigured(
f"Either 'template_name' or 'template' must be set for Component {type(self).__name__}."
f"Either 'template_file' or 'template' must be set for Component {type(self).__name__}."
)
def inject(self, key: str, default: Optional[Any] = None) -> Any:

View file

@ -18,7 +18,7 @@ if TYPE_CHECKING:
# These are all the attributes that are handled by ComponentMedia and lazily-resolved
COMP_MEDIA_LAZY_ATTRS = ("media", "template", "template_name", "js", "js_file", "css", "css_file")
COMP_MEDIA_LAZY_ATTRS = ("media", "template", "template_file", "js", "js_file", "css", "css_file")
ComponentMediaInputPath = Union[
@ -176,7 +176,7 @@ class ComponentMedia:
media: Optional[MediaCls] = None
media_class: Type[MediaCls] = MediaCls
template: Optional[str] = None
template_name: Optional[str] = None
template_file: Optional[str] = None
js: Optional[str] = None
js_file: Optional[str] = None
css: Optional[str] = None
@ -185,9 +185,9 @@ class ComponentMedia:
# This metaclass is all about one thing - lazily resolving the media files.
#
# All the CSS/JS/HTML associated with a component - e.g. the `js`, `js_file`, `template_name` or `Media` class,
# All the CSS/JS/HTML associated with a component - e.g. the `js`, `js_file`, `template_file` or `Media` class,
# are all class attributes. And some of these attributes need to be resolved, e.g. to find the files
# that `js_file`, `css_file` and `template_name` point to.
# that `js_file`, `css_file` and `template_file` point to.
#
# Some of the resolutions we need to do is:
# - Component's HTML/JS/CSS files can be defined as relative to the component class file. So for each file,
@ -240,7 +240,14 @@ class ComponentMediaMeta(type):
" already resolved. This may lead to unexpected behavior."
)
super().__setattr__(name, value)
# NOTE: When a metaclass specifies a `__setattr__` method, this overrides the normal behavior of
# setting an attribute on the class with Descriptors. So we need to call the normal behavior explicitly.
# NOTE 2: `__dict__` is used to access the class attributes directly, without triggering the descriptors.
desc = cls.__dict__.get(name, None)
if hasattr(desc, "__set__"):
desc.__set__(cls, value)
else:
super().__setattr__(name, value)
# This sets up the lazy resolution of the media attributes.
@ -254,7 +261,7 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
media=attrs.get("media", None),
media_class=attrs.get("media_class", None),
template=attrs.get("template", None),
template_name=attrs.get("template_name", None),
template_file=attrs.get("template_file", None),
js=attrs.get("js", None),
js_file=attrs.get("js_file", None),
css=attrs.get("css", None),
@ -292,8 +299,8 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
continue
else:
return value
if attr in ("template", "template_name"):
if check_pair_empty("template", "template_name"):
if attr in ("template", "template_file"):
if check_pair_empty("template", "template_file"):
continue
else:
return value
@ -544,7 +551,7 @@ def _resolve_component_relative_files(
# HTML/JS/CSS files, just skip.
will_resolve_files = False
if (
getattr(comp_media, "template_name", None)
getattr(comp_media, "template_file", None)
or getattr(comp_media, "js_file", None)
or getattr(comp_media, "css_file", None)
):
@ -609,8 +616,8 @@ def _resolve_component_relative_files(
return filepath
# Check if template name is a local file or not
if getattr(comp_media, "template_name", None):
comp_media.template_name = resolve_media_file(comp_media.template_name)
if getattr(comp_media, "template_file", None):
comp_media.template_file = resolve_media_file(comp_media.template_file)
if getattr(comp_media, "js_file", None):
comp_media.js_file = resolve_media_file(comp_media.js_file)
if getattr(comp_media, "css_file", None):

View file

@ -197,7 +197,7 @@ class Command(BaseCommand):
@register("{name}")
class {name.capitalize()}(Component):
template_name = "{name}/{template_filename}"
template_file = "{name}/{template_filename}"
def get_context_data(self, value):
return {{

View file

@ -7,7 +7,7 @@ from django_components import Component, register
@register("multi_file_component")
class MultFileComponent(Component):
template_name = "multi_file/multi_file.html"
template_file = "multi_file/multi_file.html"
def post(self, request, *args, **kwargs) -> HttpResponse:
variable = request.POST.get("variable")

View file

@ -7,7 +7,7 @@ from django_components import Component, register
@register("relative_file_component")
class RelativeFileComponent(Component):
template_name = "relative_file.html"
template_file = "relative_file.html"
class Media:
js = "relative_file.js"

View file

@ -26,7 +26,7 @@ class PathObj:
@register("relative_file_pathobj_component")
class RelativeFileWithPathObjComponent(Component):
template_name = "relative_file_pathobj.html"
template_file = "relative_file_pathobj.html"
class Media:
js = PathObj("relative_file_pathobj.js")

View file

@ -6,7 +6,7 @@ from django_components import Component, register
# Used for testing the staticfiles finder in `test_staticfiles.py`
@register("staticfiles_component")
class RelativeFileWithPathObjComponent(Component):
template_name = "staticfiles.html"
template_file = "staticfiles.html"
class Media:
js = "staticfiles.js"

View file

@ -6,7 +6,7 @@ from django_components import Component, register
# Used for testing the template_loader
@register("app_lvl_comp")
class AppLvlCompComponent(Component):
template_name: Optional[str] = "app_lvl_comp.html"
template_file: Optional[str] = "app_lvl_comp.html"
js_file = "app_lvl_comp.js"
css_file = "app_lvl_comp.css"

View file

@ -6,7 +6,7 @@ from django_components import Component, register
# Used for testing the template_loader
@register("custom_app_lvl_comp")
class AppLvlCompComponent(Component):
template_name = "app_lvl_comp.html"
template_file = "app_lvl_comp.html"
class Media:
js = "app_lvl_comp.js"

View file

@ -206,9 +206,9 @@ class ComponentTest(BaseTestCase):
)
@parametrize_context_behavior(["django", "isolated"])
def test_template_name_static(self):
def test_template_file_static(self):
class SimpleComponent(Component):
template_name = "simple_template.html"
template_file = "simple_template.html"
def get_context_data(self, variable=None):
return {
@ -228,7 +228,58 @@ class ComponentTest(BaseTestCase):
)
@parametrize_context_behavior(["django", "isolated"])
def test_template_name_dynamic(self):
def test_template_file_static__compat(self):
class SimpleComponent(Component):
template_name = "simple_template.html"
def get_context_data(self, variable=None):
return {
"variable": variable,
}
class Media:
css = "style.css"
js = "script.js"
self.assertEqual(SimpleComponent.template_name, "simple_template.html")
self.assertEqual(SimpleComponent.template_file, "simple_template.html")
SimpleComponent.template_name = "other_template.html"
self.assertEqual(SimpleComponent.template_name, "other_template.html")
self.assertEqual(SimpleComponent.template_file, "other_template.html")
SimpleComponent.template_name = "simple_template.html"
rendered = SimpleComponent.render(kwargs={"variable": "test"})
self.assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3e>test</strong>
""",
)
comp = SimpleComponent()
self.assertEqual(comp.template_name, "simple_template.html")
self.assertEqual(comp.template_file, "simple_template.html")
# NOTE: Setting `template_file` on INSTANCE is not supported, as users should work
# with classes and not instances. This is tested for completeness.
comp.template_name = "other_template_2.html"
self.assertEqual(comp.template_name, "other_template_2.html")
self.assertEqual(comp.template_file, "other_template_2.html")
self.assertEqual(SimpleComponent.template_name, "other_template_2.html")
self.assertEqual(SimpleComponent.template_file, "other_template_2.html")
SimpleComponent.template_name = "simple_template.html"
rendered = comp.render(kwargs={"variable": "test"})
self.assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3f>test</strong>
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_template_file_dynamic(self):
class SvgComponent(Component):
def get_context_data(self, name, css_class="", title="", **attrs):
return {

View file

@ -31,7 +31,7 @@ class MainMediaTest(BaseTestCase):
def test_html_filepath(self):
class Test(Component):
template_name = "simple_template.html"
template_file = "simple_template.html"
rendered = Test.render(context={"variable": "test"})
@ -86,7 +86,7 @@ class MainMediaTest(BaseTestCase):
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
class TestComponent(AppLvlCompComponent):
template_name = None
template_file = None
template = """
{% load component_tags %}
{% component_js_dependencies %}
@ -167,7 +167,7 @@ class MainMediaTest(BaseTestCase):
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
class TestComponent(AppLvlCompComponent):
template_name = None
template_file = None
template = """
{% load component_tags %}
{% component_js_dependencies %}

View file

@ -13,7 +13,7 @@ setup_test_config({"autodiscover": False})
class SlottedComponent(Component):
template_name = "slotted_template.html"
template_file = "slotted_template.html"
#######################
@ -42,7 +42,7 @@ class TemplateInstrumentationTest(BaseTestCase):
@register("inner_component")
class SimpleComponent(Component):
template_name = "simple_template.html"
template_file = "simple_template.html"
def get_context_data(self, variable, variable2="default"):
return {

View file

@ -9,7 +9,7 @@ setup_test_config({"autodiscover": False})
class SlottedComponent(Component):
template_name = "slotted_template.html"
template_file = "slotted_template.html"
class SlottedComponentWithContext(Component):

View file

@ -11,11 +11,11 @@ setup_test_config({"autodiscover": False})
class SlottedComponent(Component):
template_name = "slotted_template.html"
template_file = "slotted_template.html"
class BlockedAndSlottedComponent(Component):
template_name = "blocked_and_slotted_template.html"
template_file = "blocked_and_slotted_template.html"
#######################
@ -418,7 +418,7 @@ class ExtendsCompatTests(BaseTestCase):
@register("extended_component")
class _ExtendedComponent(Component):
template_name = "included.html"
template_file = "included.html"
template: types.django_html = """
{% extends 'block.html' %}
@ -587,7 +587,7 @@ class ExtendsCompatTests(BaseTestCase):
@register("block_in_component_parent")
class BlockInCompParent(Component):
template_name = "block_in_component_parent.html"
template_file = "block_in_component_parent.html"
template: types.django_html = """
{% load component_tags %}
@ -620,7 +620,7 @@ class ExtendsCompatTests(BaseTestCase):
@register("block_inside_slot_v1")
class BlockInSlotInComponent(Component):
template_name = "block_in_slot_in_component.html"
template_file = "block_in_slot_in_component.html"
template: types.django_html = """
{% load component_tags %}