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 #### 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: - 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 - 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 from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
``` ```
Or a combination of Django template, Python, CSS, and Javascript: Or a combination of Django template, Python, CSS, and Javascript:
@ -46,7 +46,7 @@ document.querySelector(".calendar").onclick = function () {
from django_components import Component from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
js_file = "calendar.js" js_file = "calendar.js"
css_file = "calendar.css" css_file = "calendar.css"
``` ```
@ -77,7 +77,6 @@ class Calendar(Component):
""" """
``` ```
## Features ## Features
1. 🧩 **Reusability:** Allows creation of self-contained, reusable UI elements. 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 ## 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: Define a component in `components/calendar/calendar.py` like this:
```python ```python
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "template.html" template_file = "template.html"
def get_context_data(self, date): def get_context_data(self, date):
return {"date": date} return {"date": date}

View file

@ -14,7 +14,7 @@ from django_components import Component, register
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "template.html" template_file = "template.html"
# This component takes one parameter, a date string to show in the template # This component takes one parameter, a date string to show in the template
def get_context_data(self, date): 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. When you are creating an instance of `ComponentRegistry`, you can define the components' behavior within the template.
The registry accepts these settings: The registry accepts these settings:
- `context_behavior` - `context_behavior`
- `tag_formatter` - `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.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 [`Component.js`](../../reference/api.md#django_components.Component.js) to define the main HTML / CSS / JS for a component
as inlined code. 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.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 [`Component.js_file`](../../reference/api.md#django_components.Component.js_file) to define the main HTML / CSS / JS
for a component in separate files. 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 **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.css` or `Component.css_file`
- You can only either set `Component.js` or `Component.js_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 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 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) [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
and and
[`Component.css_file`](../../reference/api.md#django_components.Component.css_file) respectively: [`Component.css_file`](../../reference/api.md#django_components.Component.css_file) respectively:
@ -61,7 +61,7 @@ from django_components import Component, register
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "template.html" template_file = "template.html"
css_file = "style.css" css_file = "style.css"
js_file = "script.js" 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 Alternatively, you can specify the file paths relative to the directories set in
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs) [`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). [`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 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") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "calendar/template.html" template_file = "calendar/template.html"
css_file = "calendar/style.css" css_file = "calendar/style.css"
js_file = "calendar/script.js" js_file = "calendar/script.js"
``` ```
@ -94,59 +94,59 @@ class Calendar(Component):
**File path resolution in-depth** **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. 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 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) 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). [`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs).
**Example:** **Example:**
```py title="[root]/components/mytable/mytable.py" ```py title="[root]/components/mytable/mytable.py"
class MyTable(Component): class MyTable(Component):
template_name = "mytable.html" template_file = "mytable.html"
``` ```
1. Component `MyTable` is defined in file `[root]/components/mytable/mytable.py`. 1. Component `MyTable` is defined in file `[root]/components/mytable/mytable.py`.
2. The component's directory is thus `[root]/components/mytable/`. 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`. resolve it as `[root]/components/mytable/mytable.html`.
4. django-components checks the filesystem. If there's no such file, nothing happens. 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. 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 6. django-components searches `COMPONENTS.dirs` and `COMPONENTS.app_dirs` for a first
directory that contains `[root]/components/mytable/mytable.html`. directory that contains `[root]/components/mytable/mytable.html`.
7. It comes across `[root]/components/`, which DOES contain the path to `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. NOTE: In case of ambiguity, the preference goes to resolving the files relative to the component's directory.
## Defining additional JS and CSS files ## 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). using the nested [`Component.Media` class](../../../reference/api#django_components.Component.Media).
This `Media` class behaves similarly to 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): [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 - 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. - 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), - 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 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()`. resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
However, there's a few differences from Django's Media class: 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, 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`, 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 [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)). (See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
3. Our Media class does NOT support 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 ```py
class MyTable(Component): class MyTable(Component):
@ -268,7 +268,7 @@ class ModuleJsPath:
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "calendar/template.html" template_file = "calendar/template.html"
def get_context_data(self, date): def get_context_data(self, date):
return { return {
@ -314,7 +314,7 @@ class MyMedia(Media):
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "calendar/template.html" template_file = "calendar/template.html"
css_file = "calendar/style.css" css_file = "calendar/style.css"
js_file = "calendar/script.js" js_file = "calendar/script.js"
@ -331,27 +331,28 @@ class Calendar(Component):
Component's HTML / CSS / JS is resolved and loaded lazily. Component's HTML / CSS / JS is resolved and loaded lazily.
This means that, when you specify any of 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), [`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css_file`](../../reference/api.md#django_components.Component.css_file), [`css_file`](../../reference/api.md#django_components.Component.css_file),
or [`Media.js/css`](../../reference/api.md#django_components.Component.Media), or [`Media.js/css`](../../reference/api.md#django_components.Component.Media),
these file paths will be resolved only once you either: these file paths will be resolved only once you either:
1. Access any of the following attributes on the component: 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), - [`media`](../../reference/api.md#django_components.Component.media),
[`template_name`](../../reference/api.md#django_components.Component.template_name), [`template`](../../reference/api.md#django_components.Component.template),
[`js`](../../reference/api.md#django_components.Component.js), [`template_file`](../../reference/api.md#django_components.Component.template_file),
[`js_file`](../../reference/api.md#django_components.Component.js_file), [`js`](../../reference/api.md#django_components.Component.js),
[`css`](../../reference/api.md#django_components.Component.css), [`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css_file`](../../reference/api.md#django_components.Component.css_file) [`css`](../../reference/api.md#django_components.Component.css),
[`css_file`](../../reference/api.md#django_components.Component.css_file)
2. Render the component. 2. Render the component.
Once the component's media files have been loaded once, they will remain in-memory Once the component's media files have been loaded once, they will remain in-memory
on the Component class: 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) 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) - 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) 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) will be available under [`Component.js`](../../reference/api.md#django_components.Component.js)
Thus, whether you define HTML via 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), 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). 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. And the same applies for JS and CSS.
@ -371,7 +372,7 @@ And the same applies for JS and CSS.
# are not yet loaded! # are not yet loaded!
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "calendar/template.html" template_file = "calendar/template.html"
css_file = "calendar/style.css" css_file = "calendar/style.css"
js_file = "calendar/script.js" 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. 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. 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 Modifying these files AFTER the component has been loaded at best does nothing. However, this is
an untested behavior. an untested behavior.

View file

@ -3,10 +3,13 @@ title: Single-file components
weight: 1 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" ```python title="[project root]/components/calendar.py"
# In a file called [project root]/components/calendar.py
from django_components import Component, register, types from django_components import Component, register, types
@register("calendar") @register("calendar")
@ -17,18 +20,27 @@ class Calendar(Component):
} }
template: types.django_html = """ 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 = """ css: types.css = """
.calendar-component { width: 200px; background: pink; } .calendar {
.calendar-component span { font-weight: bold; } width: 200px;
background: pink;
}
.calendar span {
font-weight: bold;
}
""" """
js: types.js = """ js: types.js = """
(function(){ (function(){
if (document.querySelector(".calendar-component")) { if (document.querySelector(".calendar")) {
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked 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. 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 --> <!-- TODO: UPDATE AFTER SCOPED CSS ADDED -->
!!! note !!! note
Soon, django-components will automatically scope your CSS by default, so you won't have to worry 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. This makes all variables defined only be defined inside this component and not affect other components.
<!-- TODO: UPDATE AFTER FUNCTIONS WRAPPED --> <!-- TODO: UPDATE AFTER FUNCTIONS WRAPPED -->
!!! note !!! note
Soon, django-components will automatically wrap your JS in a self-invoking function by default 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> </html>
``` ```
### 4. Link JS and CSS to a component ### 4. Link JS and CSS to a component
Finally, we return to our Python component in `calendar.py` to tie this together. 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 from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
js_file = "calendar.js" # <--- new js_file = "calendar.js" # <--- new
css_file = "calendar.css" # <--- 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`) (e.g. `[your apps]/components` dir and `[project root]/components`)
3. Relative to any of the directories defined by `STATICFILES_DIRS`. 3. Relative to any of the directories defined by `STATICFILES_DIRS`.
<!-- TODO: UPDATE AFTER AT LEAST ONE IMPLEMENTED <!-- TODO: UPDATE AFTER AT LEAST ONE IMPLEMENTED
!!! info !!! info
@ -243,7 +243,7 @@ with a few differences:
from django_components import Component from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
js_file = "calendar.js" js_file = "calendar.js"
css_file = "calendar.css" 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) 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`) (e.g. `[your apps]/components` dir and `[project root]/components`)
!!! info !!! info
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/). 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. 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. 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. this JS will be still loaded and executed only once.
Putting all of this together, our `Calendar` component above would render HTML like so: 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> <head>
... ...
<!-- CSS from Media.css --> <!-- CSS from Media.css -->
<link href="/static/path/to/shared.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"> <link
href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css"
media="all"
rel="stylesheet"
/>
<!-- CSS from Component.css_file --> <!-- CSS from Component.css_file -->
<style> <style>
.calendar { .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 1. On one page, it needs to be shown as is
2. On the second, the date needs to be **bold** 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: 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. - `{% slot <name> %}`/`{% endslot %}`: Declares a new slot in the component template.
- `{% fill <name> %}`/`{% endfill %}`: (Used inside a [`{% component %}`](../../reference/template_tags.md#component) - `{% 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 ### 2. Add a slot tag
@ -122,7 +122,7 @@ Which will render as:
{% endcomponent %} {% 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) [`{% fill %}`](../../reference/template_tags.md#fill) tag)
```htmldjango ```htmldjango
{% component "calendar" date="2024-12-13" %} {% component "calendar" date="2024-12-13" %}
@ -177,7 +177,7 @@ def to_workweek_date(d: date):
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
... ...
def get_context_data(self, date: date, extra_class: str | None = None): def get_context_data(self, date: date, extra_class: str | None = None):
workweek_date = to_workweek_date(date) 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. which is NOT the same as the `date` variable used inside Calendar's template.
### 5. Adding data to slots ### 5. Adding data to slots

View file

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

View file

@ -23,7 +23,7 @@ from django_components import Component, register
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
... ...
def get_context_data(self): def get_context_data(self):
return { return {
@ -71,7 +71,7 @@ from django_components import Component, register
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
... ...
def get_context_data(self, date: date, extra_class: str | None = None): def get_context_data(self, date: date, extra_class: str | None = None):
return { return {
@ -135,7 +135,7 @@ We need to group the list items by size into following buckets by population:
- 0-10,000,000 - 0-10,000,000
- 10,000,001-20,000,000 - 10,000,001-20,000,000
- 20,000,001-30,000,000 - 20,000,001-30,000,000
- +30,000,001 - +30,000,001
So we want to end up with following data: So we want to end up with following data:
@ -179,7 +179,7 @@ def group_by_pop(data):
@register("population_table") @register("population_table")
class PopulationTable(Component): class PopulationTable(Component):
template_name = "population_table.html" template_file = "population_table.html"
def get_context_data(self, data): def get_context_data(self, data):
return { return {
@ -200,7 +200,7 @@ def to_workweek_date(d: date):
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
... ...
def get_context_data(self, date: date, extra_class: str | None = None): def get_context_data(self, date: date, extra_class: str | None = None):
workweek_date = to_workweek_date(date) # <--- new 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 from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
``` ```
Or a combination of Django template, Python, CSS, and Javascript: Or a combination of Django template, Python, CSS, and Javascript:
@ -43,7 +43,7 @@ document.querySelector(".calendar").onclick = function () {
from django_components import Component from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
js_file = "calendar.js" js_file = "calendar.js"
css_file = "calendar.css" css_file = "calendar.css"
``` ```
@ -80,7 +80,6 @@ class Calendar(Component):
[syntax highlighting](../../guides/setup/syntax_highlight.md) for better experience. [syntax highlighting](../../guides/setup/syntax_highlight.md) for better experience.
However, autocompletion / intellisense does not work with syntax highlighting. However, autocompletion / intellisense does not work with syntax highlighting.
We'll start by creating a component that defines only a Django template: We'll start by creating a component that defines only a Django template:
### 1. Create project structure ### 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) In `calendar.py`, create a subclass of [Component](../../../reference/api#django_components.Component)
to create a new 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. to the name of the HTML file.
```python title="[project root]/components/calendar/calendar.py" ```python title="[project root]/components/calendar/calendar.py"
from django_components import Component from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
``` ```
!!! note !!! note
@ -156,7 +155,7 @@ will become available within the template as variables, e.g. as `{{ date }}`.
from django_components import Component from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
def get_context_data(self): def get_context_data(self):
return { return {

View file

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

View file

@ -2,11 +2,11 @@
title: Welcome to Django Components title: Welcome to Django Components
weight: 1 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;"> <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) [![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. 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. 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 from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
``` ```
Or a combination of Django template, Python, CSS, and Javascript: Or a combination of Django template, Python, CSS, and Javascript:
@ -50,7 +50,7 @@ document.querySelector(".calendar").onclick = function () {
from django_components import Component from django_components import Component
class Calendar(Component): class Calendar(Component):
template_name = "calendar.html" template_file = "calendar.html"
js_file = "calendar.js" js_file = "calendar.js"
css_file = "calendar.css" css_file = "calendar.css"
``` ```
@ -100,13 +100,14 @@ Django-components can be particularly useful for larger Django projects that req
## Quickstart ## 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: Define a component in `components/calendar/calendar.py` like this:
```python ```python
@register("calendar") @register("calendar")
class Calendar(Component): class Calendar(Component):
template_name = "template.html" template_file = "template.html"
def get_context_data(self, date): def get_context_data(self, date):
return {"date": date} return {"date": date}

View file

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

View file

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

View file

@ -5,4 +5,4 @@ from django_components import Component, register
class Todo(Component): class Todo(Component):
# Templates inside `[your apps]/components` dir and `[project root]/components` dir # Templates inside `[your apps]/components` dir and `[project root]/components` dir
# will be automatically found. # 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): 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 # NOTE: We use metaclass to automatically define the HTTP methods as defined
# in `View.http_method_names`. # in `View.http_method_names`.
class ComponentViewMeta(type): 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 # Default implementation shared by all HTTP methods
def create_handler(method: str) -> Callable: def create_handler(method: str) -> Callable:
def handler(self, request: HttpRequest, *args: Any, **kwargs: Any): # type: ignore[no-untyped-def] 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: if method_name not in dct:
dct[method_name] = create_handler(method_name) 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): class ComponentView(View, metaclass=ComponentViewMeta):
@ -205,14 +223,14 @@ class Component(
# PUBLIC API (Configurable by users) # PUBLIC API (Configurable by users)
# ##################################### # #####################################
template_name: Optional[str] = None template_file: Optional[str] = None
""" """
Filepath to the Django template associated with this component. Filepath to the Django template associated with this component.
The filepath must be relative to either the file where the component class was defined, The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`. 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), [`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template) [`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined. or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -221,13 +239,27 @@ class Component(
```py ```py
class MyComponent(Component): class MyComponent(Component):
template_name = "path/to/template.html" template_file = "path/to/template.html"
def get_context_data(self): def get_context_data(self):
return {"name": "World"} 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]: def get_template_name(self, context: Context) -> Optional[str]:
""" """
Filepath to the Django template associated with this component. 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, The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`. 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), [`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template) [`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined. 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. 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), [`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template) [`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined. 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. 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), [`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template) [`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined. or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -596,21 +628,21 @@ class Component(
return ctx.is_filled 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 # 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 # 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. # is that we should treat Templates AND their nodelists as IMMUTABLE.
def _get_template(self, context: Context) -> Template: def _get_template(self, context: Context) -> Template:
# Resolve template name # Resolve template name
template_name = self.template_name template_file = self.template_file
if self.template_name is not None: if self.template_file is not None:
if self.get_template_name(context) is not None: if self.get_template_name(context) is not None:
raise ImproperlyConfigured( 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." f" Component {type(self).__name__}. Only one of the two must be set."
) )
else: else:
template_name = self.get_template_name(context) template_file = self.get_template_name(context)
# Resolve template str # Resolve template str
template_input = self.template template_input = self.template
@ -625,14 +657,14 @@ class Component(
template_getter = getattr(self, "get_template_string", self.get_template) template_getter = getattr(self, "get_template_string", self.get_template)
template_input = template_getter(context) 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( 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." " Only one of the two must be set."
) )
if template_name is not None: if template_file is not None:
return get_template(template_name).template return get_template(template_file).template
elif template_input is not None: elif template_input is not None:
# We got template string, so we convert it to Template # We got template string, so we convert it to Template
@ -644,7 +676,7 @@ class Component(
return template return template
raise ImproperlyConfigured( 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: 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 # 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[ ComponentMediaInputPath = Union[
@ -176,7 +176,7 @@ class ComponentMedia:
media: Optional[MediaCls] = None media: Optional[MediaCls] = None
media_class: Type[MediaCls] = MediaCls media_class: Type[MediaCls] = MediaCls
template: Optional[str] = None template: Optional[str] = None
template_name: Optional[str] = None template_file: Optional[str] = None
js: Optional[str] = None js: Optional[str] = None
js_file: Optional[str] = None js_file: Optional[str] = None
css: 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. # 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 # 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: # 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, # - 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." " 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. # 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=attrs.get("media", None),
media_class=attrs.get("media_class", None), media_class=attrs.get("media_class", None),
template=attrs.get("template", 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=attrs.get("js", None),
js_file=attrs.get("js_file", None), js_file=attrs.get("js_file", None),
css=attrs.get("css", None), css=attrs.get("css", None),
@ -292,8 +299,8 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
continue continue
else: else:
return value return value
if attr in ("template", "template_name"): if attr in ("template", "template_file"):
if check_pair_empty("template", "template_name"): if check_pair_empty("template", "template_file"):
continue continue
else: else:
return value return value
@ -544,7 +551,7 @@ def _resolve_component_relative_files(
# HTML/JS/CSS files, just skip. # HTML/JS/CSS files, just skip.
will_resolve_files = False will_resolve_files = False
if ( if (
getattr(comp_media, "template_name", None) getattr(comp_media, "template_file", None)
or getattr(comp_media, "js_file", None) or getattr(comp_media, "js_file", None)
or getattr(comp_media, "css_file", None) or getattr(comp_media, "css_file", None)
): ):
@ -609,8 +616,8 @@ def _resolve_component_relative_files(
return filepath return filepath
# Check if template name is a local file or not # Check if template name is a local file or not
if getattr(comp_media, "template_name", None): if getattr(comp_media, "template_file", None):
comp_media.template_name = resolve_media_file(comp_media.template_name) comp_media.template_file = resolve_media_file(comp_media.template_file)
if getattr(comp_media, "js_file", None): if getattr(comp_media, "js_file", None):
comp_media.js_file = resolve_media_file(comp_media.js_file) comp_media.js_file = resolve_media_file(comp_media.js_file)
if getattr(comp_media, "css_file", None): if getattr(comp_media, "css_file", None):

View file

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

View file

@ -7,7 +7,7 @@ from django_components import Component, register
@register("multi_file_component") @register("multi_file_component")
class MultFileComponent(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: def post(self, request, *args, **kwargs) -> HttpResponse:
variable = request.POST.get("variable") variable = request.POST.get("variable")

View file

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

View file

@ -26,7 +26,7 @@ class PathObj:
@register("relative_file_pathobj_component") @register("relative_file_pathobj_component")
class RelativeFileWithPathObjComponent(Component): class RelativeFileWithPathObjComponent(Component):
template_name = "relative_file_pathobj.html" template_file = "relative_file_pathobj.html"
class Media: class Media:
js = PathObj("relative_file_pathobj.js") 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` # Used for testing the staticfiles finder in `test_staticfiles.py`
@register("staticfiles_component") @register("staticfiles_component")
class RelativeFileWithPathObjComponent(Component): class RelativeFileWithPathObjComponent(Component):
template_name = "staticfiles.html" template_file = "staticfiles.html"
class Media: class Media:
js = "staticfiles.js" js = "staticfiles.js"

View file

@ -6,7 +6,7 @@ from django_components import Component, register
# Used for testing the template_loader # Used for testing the template_loader
@register("app_lvl_comp") @register("app_lvl_comp")
class AppLvlCompComponent(Component): 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" js_file = "app_lvl_comp.js"
css_file = "app_lvl_comp.css" css_file = "app_lvl_comp.css"

View file

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

View file

@ -206,9 +206,9 @@ class ComponentTest(BaseTestCase):
) )
@parametrize_context_behavior(["django", "isolated"]) @parametrize_context_behavior(["django", "isolated"])
def test_template_name_static(self): def test_template_file_static(self):
class SimpleComponent(Component): class SimpleComponent(Component):
template_name = "simple_template.html" template_file = "simple_template.html"
def get_context_data(self, variable=None): def get_context_data(self, variable=None):
return { return {
@ -228,7 +228,58 @@ class ComponentTest(BaseTestCase):
) )
@parametrize_context_behavior(["django", "isolated"]) @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): class SvgComponent(Component):
def get_context_data(self, name, css_class="", title="", **attrs): def get_context_data(self, name, css_class="", title="", **attrs):
return { return {

View file

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

View file

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

View file

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

View file

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