mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 06:18:17 +00:00
Multi-line tag support, watch component files, and cleanup (#624)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
b26a201138
commit
ab059f362d
8 changed files with 217 additions and 50 deletions
92
README.md
92
README.md
|
@ -57,6 +57,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
- [Defining HTML/JS/CSS files](#defining-htmljscss-files)
|
||||
- [Rendering JS/CSS dependencies](#rendering-jscss-dependencies)
|
||||
- [Available settings](#available-settings)
|
||||
- [Running with development server](#running-with-development-server)
|
||||
- [Logging and debugging](#logging-and-debugging)
|
||||
- [Management Command](#management-command)
|
||||
- [Writing and sharing component libraries](#writing-and-sharing-component-libraries)
|
||||
|
@ -66,9 +67,13 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
|
||||
## Release notes
|
||||
|
||||
**Version 0.94**
|
||||
- django_components now automatically configures Django to support multi-line tags. (See [Multi-line tags](#multi-line-tags))
|
||||
- New setting `reload_on_template_change`. Set this to `True` to reload the dev server on changes to component template files. (See [Reload dev server on component file changes](#reload-dev-server-on-component-file-changes))
|
||||
|
||||
**Version 0.93**
|
||||
- Spread operator `...dict` inside template tags. See [Spread operator](#spread-operator))
|
||||
- Use template tags inside string literals in component inputs. See [Use template tags inside component inputs](#use-template-tags-inside-component-inputs))
|
||||
- Spread operator `...dict` inside template tags. (See [Spread operator](#spread-operator))
|
||||
- Use template tags inside string literals in component inputs. (See [Use template tags inside component inputs](#use-template-tags-inside-component-inputs))
|
||||
- Dynamic slots, fills and provides - The `name` argument for these can now be a variable, a template expression, or via spread operator
|
||||
- Component library authors can now configure `CONTEXT_BEHAVIOR` and `TAG_FORMATTER` settings independently from user settings.
|
||||
|
||||
|
@ -227,6 +232,7 @@ For a step-by-step guide on deploying production server with static files,
|
|||
4. Modify `TEMPLATES` section of settings.py as follows:
|
||||
|
||||
- _Remove `'APP_DIRS': True,`_
|
||||
- NOTE: Instead of APP_DIRS, for the same effect, we will use [`django.template.loaders.app_directories.Loader`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.loaders.app_directories.Loader)
|
||||
- Add `loaders` to `OPTIONS` list and set it to following value:
|
||||
|
||||
```python
|
||||
|
@ -239,8 +245,11 @@ For a step-by-step guide on deploying production server with static files,
|
|||
],
|
||||
'loaders':[(
|
||||
'django.template.loaders.cached.Loader', [
|
||||
# Default Django loader
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
# Inluding this is the same as APP_DIRS=True
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
# Components loader
|
||||
'django_components.template_loader.Loader',
|
||||
]
|
||||
)],
|
||||
|
@ -2209,6 +2218,34 @@ Sweet! Now all the relevant HTML is inside the template, and we can move it to a
|
|||
> {"attrs": {"my_key:two": 2}}
|
||||
> ```
|
||||
|
||||
### Multi-line tags
|
||||
|
||||
By default, Django expects a template tag to be defined on a single line.
|
||||
|
||||
However, this can become unwieldy if you have a component with a lot of inputs:
|
||||
|
||||
```django
|
||||
{% component "card" title="Joanne Arc" subtitle="Head of Kitty Relations" date_last_active="2024-09-03" ... %}
|
||||
```
|
||||
|
||||
Instead, when you install django_components, it automatically configures Django
|
||||
to suport multi-line tags.
|
||||
|
||||
So we can rewrite the above as:
|
||||
|
||||
```django
|
||||
{% component "card"
|
||||
title="Joanne Arc"
|
||||
subtitle="Head of Kitty Relations"
|
||||
date_last_active="2024-09-03"
|
||||
...
|
||||
%}
|
||||
```
|
||||
|
||||
Much better!
|
||||
|
||||
To disable this behavior, set [`COMPONENTS.multiline_tag`](#multiline_tags---enabledisable-multiline-support) to `False`
|
||||
|
||||
## Prop drilling and dependency injection (provide / inject)
|
||||
|
||||
_New in version 0.80_:
|
||||
|
@ -2749,6 +2786,20 @@ COMPONENTS = {
|
|||
|
||||
All library settings are handled from a global `COMPONENTS` variable that is read from `settings.py`. By default you don't need it set, there are resonable defaults.
|
||||
|
||||
Here's overview of all available settings and their defaults:
|
||||
|
||||
```py
|
||||
COMPONENTS = {
|
||||
"autodiscover": True,
|
||||
"context_behavior": "django", # "django" | "isolated"
|
||||
"libraries": [], # ["mysite.components.forms", ...]
|
||||
"multiline_tags": True,
|
||||
"reload_on_template_change": False,
|
||||
"tag_formatter": "django_components.component_formatter",
|
||||
"template_cache_size": 128,
|
||||
}
|
||||
```
|
||||
|
||||
### `libraries` - Load component modules
|
||||
|
||||
Configure the locations where components are loaded. To do this, add a `COMPONENTS` variable to you `settings.py` with a list of python paths to load. This allows you to build a structure of components that are independent from your apps.
|
||||
|
@ -2801,6 +2852,16 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
### `multiline_tags` - Enable/Disable multiline support
|
||||
|
||||
If `True`, template tags can span multiple lines. Default: `True`
|
||||
|
||||
```python
|
||||
COMPONENTS = {
|
||||
"multiline_tags": True,
|
||||
}
|
||||
```
|
||||
|
||||
### `template_cache_size` - Tune the template cache
|
||||
|
||||
Each time a template is rendered it is cached to a global in-memory cache (using Python's `lru_cache` decorator). This speeds up the next render of the component. As the same component is often used many times on the same page, these savings add up.
|
||||
|
@ -2918,6 +2979,13 @@ But since `"cheese"` is not defined there, it's empty.
|
|||
|
||||
Notice that the variables defined with the `{% with %}` tag are ignored inside the `{% fill %}` tag with the `"isolated"` mode.
|
||||
|
||||
### `reload_on_template_change` - Reload dev server on component file changes
|
||||
|
||||
If `True`, configures Django to reload on component files. See
|
||||
[Reload dev server on component file changes](#reload-dev-server-on-component-file-changes).
|
||||
|
||||
NOTE: This setting should be enabled only for the dev environment!
|
||||
|
||||
### `tag_formatter` - Change how components are used in templates
|
||||
Sets the [`TagFormatter`](#available-tagformatters) instance. See the section [Customizing component tags with TagFormatter](#customizing-component-tags-with-tagformatter).
|
||||
|
||||
|
@ -2939,6 +3007,26 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
## Running with development server
|
||||
|
||||
### Reload dev server on component file changes
|
||||
|
||||
This is relevant if you are using the project structure as shown in our examples, where
|
||||
HTML, JS, CSS and Python are separate and nested in a directory.
|
||||
|
||||
In this case you may notice that when you are running a development server,
|
||||
the server sometimes does not reload when you change comoponent files.
|
||||
|
||||
From relevant [StackOverflow thread](https://stackoverflow.com/a/76722393/9788634):
|
||||
|
||||
> TL;DR is that the server won't reload if it thinks the changed file is in a templates directory,
|
||||
> or in a nested sub directory of a templates directory. This is by design.
|
||||
|
||||
To make the dev server reload on all component files, set [`reload_on_template_change`](#reload_on_template_change---reload-dev-server-on-component-file-changes) to `True`.
|
||||
This configures Django to watch for component files too.
|
||||
|
||||
NOTE: This setting should be enabled only for the dev environment!
|
||||
|
||||
## Logging and debugging
|
||||
|
||||
Django components supports [logging with Django](https://docs.djangoproject.com/en/5.0/howto/logging/#logging-how-to). This can help with troubleshooting.
|
||||
|
|
|
@ -37,7 +37,22 @@ class SimpleComponent(Component):
|
|||
|
||||
|
||||
class BreadcrumbComponent(Component):
|
||||
template_name = "mdn_component_template.html"
|
||||
template: types.django_html = """
|
||||
<div class="breadcrumb-container">
|
||||
<nav class="breadcrumbs">
|
||||
<ol typeof="BreadcrumbList" vocab="https://schema.org/" aria-label="breadcrumbs">
|
||||
{% for label, url in links %}
|
||||
<li property="itemListElement" typeof="ListItem">
|
||||
<a class="breadcrumb-current-page" property="item" typeof="WebPage" href="{{ url }}">
|
||||
<span property="name">{{ label }}</span>
|
||||
</a>
|
||||
<meta property="position" content="4">
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
"""
|
||||
|
||||
LINKS = [
|
||||
(
|
||||
|
|
|
@ -102,6 +102,14 @@ class AppSettings:
|
|||
def LIBRARIES(self) -> List:
|
||||
return self.settings.get("libraries", [])
|
||||
|
||||
@property
|
||||
def MULTILINE_TAGS(self) -> bool:
|
||||
return self.settings.get("multiline_tags", True)
|
||||
|
||||
@property
|
||||
def RELOAD_ON_TEMPLATE_CHANGE(self) -> bool:
|
||||
return self.settings.get("reload_on_template_change", False)
|
||||
|
||||
@property
|
||||
def TEMPLATE_CACHE_SIZE(self) -> int:
|
||||
return self.settings.get("template_cache_size", 128)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import re
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
|
@ -8,10 +10,36 @@ class ComponentsConfig(AppConfig):
|
|||
# to Django's INSTALLED_APPS
|
||||
def ready(self) -> None:
|
||||
from django_components.app_settings import app_settings
|
||||
from django_components.autodiscover import autodiscover, import_libraries
|
||||
from django_components.autodiscover import autodiscover, get_dirs, import_libraries, search_dirs
|
||||
from django_components.utils import watch_files_for_autoreload
|
||||
|
||||
# Import modules set in `COMPONENTS.libraries` setting
|
||||
import_libraries()
|
||||
|
||||
if app_settings.AUTODISCOVER:
|
||||
autodiscover()
|
||||
|
||||
# Watch template files for changes, so Django dev server auto-reloads
|
||||
# See https://github.com/EmilStenstrom/django-components/discussions/567#discussioncomment-10273632
|
||||
# And https://stackoverflow.com/questions/42907285/66673186#66673186
|
||||
if app_settings.RELOAD_ON_TEMPLATE_CHANGE:
|
||||
dirs = get_dirs()
|
||||
component_filepaths = search_dirs(dirs, "**/*")
|
||||
watch_files_for_autoreload(component_filepaths)
|
||||
|
||||
if app_settings.MULTILINE_TAGS:
|
||||
# Allow tags to span multiple lines. This makes it easier to work with
|
||||
# components inside Django templates, allowing us syntax like:
|
||||
# ```html
|
||||
# {% component "icon"
|
||||
# icon='outline_chevron_down'
|
||||
# size=16
|
||||
# color="text-gray-400"
|
||||
# attrs:class="ml-2"
|
||||
# %}{% endcomponent %}
|
||||
# ```
|
||||
#
|
||||
# See https://stackoverflow.com/a/54206609/9788634
|
||||
from django.template import base
|
||||
|
||||
base.tag_re = re.compile(base.tag_re.pattern, re.DOTALL)
|
||||
|
|
|
@ -232,7 +232,8 @@ def component(parser: Parser, token: Token, registry: ComponentRegistry, tag_nam
|
|||
tag_name,
|
||||
parser,
|
||||
token,
|
||||
params=True, # Allow many args
|
||||
params=[],
|
||||
extra_params=True, # Allow many args
|
||||
flags=[COMP_ONLY_FLAG],
|
||||
keywordonly_kwargs=True,
|
||||
repeatable_kwargs=False,
|
||||
|
@ -360,7 +361,8 @@ def _parse_tag(
|
|||
tag: str,
|
||||
parser: Parser,
|
||||
token: Token,
|
||||
params: Union[List[str], bool] = False,
|
||||
params: Optional[List[str]] = None,
|
||||
extra_params: bool = False,
|
||||
flags: Optional[List[str]] = None,
|
||||
end_tag: Optional[str] = None,
|
||||
optional_params: Optional[List[str]] = None,
|
||||
|
@ -455,37 +457,36 @@ def _parse_tag(
|
|||
# modify it much to maintain some sort of compatibility with Django's version of
|
||||
# `parse_bits`.
|
||||
# Ideally, Django's parser would be expanded to support our use cases.
|
||||
if params != True: # noqa F712
|
||||
params_to_sort = [param for param in params if param not in seen_kwargs]
|
||||
new_args = []
|
||||
new_params = []
|
||||
new_kwargs = []
|
||||
for index, bit in enumerate(bits):
|
||||
if is_kwarg(bit) or not len(params_to_sort):
|
||||
# Pass all remaining bits (including current one) as kwargs
|
||||
new_kwargs.extend(bits[index:])
|
||||
break
|
||||
params_to_sort = [param for param in params if param not in seen_kwargs]
|
||||
new_args = []
|
||||
new_params = []
|
||||
new_kwargs = []
|
||||
for index, bit in enumerate(bits):
|
||||
if is_kwarg(bit) or not len(params_to_sort):
|
||||
# Pass all remaining bits (including current one) as kwargs
|
||||
new_kwargs.extend(bits[index:])
|
||||
break
|
||||
|
||||
param = params_to_sort.pop(0)
|
||||
if optional_params and param in optional_params:
|
||||
mark_kwarg_key(param, False)
|
||||
new_kwargs.append(f"{param}={bit}")
|
||||
continue
|
||||
new_args.append(bit)
|
||||
new_params.append(param)
|
||||
param = params_to_sort.pop(0)
|
||||
if optional_params and param in optional_params:
|
||||
mark_kwarg_key(param, False)
|
||||
new_kwargs.append(f"{param}={bit}")
|
||||
continue
|
||||
new_args.append(bit)
|
||||
new_params.append(param)
|
||||
|
||||
bits = [*new_args, *new_kwargs]
|
||||
params = [*new_params, *params_to_sort]
|
||||
bits = [*new_args, *new_kwargs]
|
||||
params = [*new_params, *params_to_sort]
|
||||
|
||||
# Remove any remaining optional positional args if they were not given
|
||||
if optional_params:
|
||||
params = [param for param in params_to_sort if param not in optional_params]
|
||||
# Remove any remaining optional positional args if they were not given
|
||||
if optional_params:
|
||||
params = [param for param in params_to_sort if param not in optional_params]
|
||||
|
||||
# Parse args/kwargs that will be passed to the fill
|
||||
raw_args, raw_kwarg_pairs = parse_bits(
|
||||
parser=parser,
|
||||
bits=bits,
|
||||
params=[] if isinstance(params, bool) else params,
|
||||
params=[] if extra_params else params,
|
||||
name=tag_name,
|
||||
)
|
||||
|
||||
|
@ -516,14 +517,11 @@ def _parse_tag(
|
|||
kwarg_pairs.append((key, val))
|
||||
|
||||
# Allow only as many positional args as given
|
||||
if params != True and len(args) > len(params): # noqa F712
|
||||
if not extra_params and len(args) > len(params): # noqa F712
|
||||
raise TemplateSyntaxError(f"Tag '{tag_name}' received unexpected positional arguments: {args[len(params):]}")
|
||||
|
||||
# For convenience, allow to access named args by their name instead of index
|
||||
if params != True: # noqa F712
|
||||
named_args = {param: args[index] for index, param in enumerate(params)}
|
||||
else:
|
||||
named_args = {}
|
||||
named_args = {param: args[index] for index, param in enumerate(params)}
|
||||
|
||||
# Validate kwargs
|
||||
kwargs: RuntimeKwargsInput = {}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
from typing import Any, Callable, List
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, List, Sequence, Union
|
||||
|
||||
from django.utils.autoreload import autoreload_started
|
||||
|
||||
# Global counter to ensure that all IDs generated by `gen_id` WILL be unique
|
||||
_id = 0
|
||||
|
@ -23,3 +26,13 @@ def find_last_index(lst: List, predicate: Callable[[Any], bool]) -> Any:
|
|||
|
||||
def is_str_wrapped_in_quotes(s: str) -> bool:
|
||||
return s.startswith(('"', "'")) and s[0] == s[-1] and len(s) >= 2
|
||||
|
||||
|
||||
# See https://github.com/EmilStenstrom/django-components/issues/586#issue-2472678136
|
||||
def watch_files_for_autoreload(watch_list: Sequence[Union[str, Path]]) -> None:
|
||||
def autoreload_hook(sender: Any, *args: Any, **kwargs: Any) -> None:
|
||||
watch = sender.extra_files.add
|
||||
for file in watch_list:
|
||||
watch(Path(file))
|
||||
|
||||
autoreload_started.connect(autoreload_hook)
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<div class="breadcrumb-container">
|
||||
<nav class="breadcrumbs">
|
||||
<ol typeof="BreadcrumbList" vocab="https://schema.org/" aria-label="breadcrumbs">
|
||||
{% for label, url in links %}
|
||||
<li property="itemListElement" typeof="ListItem">
|
||||
<a class="breadcrumb-current-page" property="item" typeof="WebPage" href="{{ url }}">
|
||||
<span property="name">{{ label }}</span>
|
||||
</a>
|
||||
<meta property="position" content="4">
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
|
@ -9,7 +9,7 @@ from django_components import Component, register, registry, types
|
|||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
setup_test_config()
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
class SlottedComponent(Component):
|
||||
|
@ -480,3 +480,34 @@ class BlockCompatTests(BaseTestCase):
|
|||
</html>
|
||||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
|
||||
class MultilineTagsTests(BaseTestCase):
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_multiline_tags(self):
|
||||
@register("test_component")
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
Variable: <strong>{{ variable }}</strong>
|
||||
"""
|
||||
|
||||
def get_context_data(self, variable, variable2="default"):
|
||||
return {
|
||||
"variable": variable,
|
||||
"variable2": variable2,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component
|
||||
"test_component"
|
||||
123
|
||||
variable2="abc"
|
||||
%}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
rendered = Template(template).render(Context())
|
||||
expected = """
|
||||
Variable: <strong>123</strong>
|
||||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue