Skip to content

Commit 1c67809

Browse files
add component id as data attribute to component attrs (#124)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 76b2939 commit 1c67809

File tree

9 files changed

+110
-43
lines changed

9 files changed

+110
-43
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- Added `ENABLE_BIRD_ID_ATTR` setting (default: `True`) to control whether components receive an automatic `data-bird-id` attribute. This is to help with component identification in the DOM and for a future planned feature around JS/CSS asset scoping.
24+
2125
## [0.10.3]
2226

2327
### Fixed

docs/configuration.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ from pathlib import Path
1212
DJANGO_BIRD = {
1313
"COMPONENT_DIRS": list[Path | str] = [],
1414
"ENABLE_AUTO_CONFIG": bool = True,
15+
"ENABLE_BIRD_ID_ATTR": bool = True,
1516
}
1617
```
1718

@@ -110,3 +111,9 @@ TEMPLATES = [
110111
```
111112

112113
This configuration ensures that django-bird's templatetags are available globally and that its loader is used to compile bird component templates before the standard Django loaders.
114+
115+
## `ENABLE_BIRD_ID_ATTR`
116+
117+
Controls whether components automatically receive a `data-bird-id` attribute containing a unique identifier. Defaults to `True`.
118+
119+
See [Component ID Attribute](params.md#component-id-attribute) for more details on how this works.

docs/params.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,46 @@ This will render as:
8585
</button>
8686
```
8787

88+
### Component ID Attribute
89+
90+
When the [`ENABLE_BIRD_ID_ATTR` setting](configuration.md#enable_bird_id_attr) is enabled (the default), django-bird automatically adds a `data-bird-id` attribute to your components, available via the `{{ attrs }}` context variable. This attribute contains a unique identifier that helps identify specific component instances in the DOM.
91+
92+
For example, for a component template like this:
93+
94+
```htmldjango
95+
<button {{ attrs }}>
96+
{{ slot }}
97+
</button>
98+
```
99+
100+
And used like this:
101+
102+
```htmldjango
103+
{% bird button class="btn" %}
104+
Click me
105+
{% endbird %}
106+
```
107+
108+
It will be rendered as:
109+
110+
```html
111+
<button data-bird-id="abc1234" class="btn">
112+
Click me
113+
</button>
114+
```
115+
116+
The ID is automatically generated from a hash of the component's name and template content.
117+
118+
You can disable this feature globally by setting `ENABLE_BIRD_ID_ATTR = False` in your Django settings:
119+
120+
```python
121+
DJANGO_BIRD = {
122+
"ENABLE_BIRD_ID_ATTR": False,
123+
}
124+
```
125+
126+
When disabled, no `data-bird-id` attribute will be added to your components.
127+
88128
## Properties
89129

90130
Properties (i.e. `props`) allow you to define parameters that your component expects, with optional default values. Unlike attributes which are provided as a flattened string via `{{ attrs }}`, props are processed by the component and made available as individual values (e.g. `{{ props.variant }}`) that can be used to control rendering logic.

pyproject.toml

Lines changed: 12 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ types = [
3636
]
3737

3838
[project]
39-
authors = [
40-
{name = "Josh Thomas", email = "[email protected]"}
41-
]
39+
authors = [{name = "Josh Thomas", email = "[email protected]"}]
4240
classifiers = [
4341
"Development Status :: 3 - Alpha",
4442
"Framework :: Django",
@@ -56,10 +54,7 @@ classifiers = [
5654
"Programming Language :: Python :: 3.13",
5755
"Programming Language :: Python :: Implementation :: CPython"
5856
]
59-
dependencies = [
60-
"cachetools>=5.5.0",
61-
"django>=4.2"
62-
]
57+
dependencies = ["cachetools>=5.5.0", "django>=4.2"]
6358
description = "High-flying components for perfectionists with deadlines"
6459
dynamic = ["version"]
6560
keywords = []
@@ -74,9 +69,7 @@ Issues = "https://github.com/joshuadavidthomas/django-bird/issues"
7469
Source = "https://github.com/joshuadavidthomas/django-bird"
7570

7671
[tool.basedpyright]
77-
exclude = [
78-
"**/__pycache__"
79-
]
72+
exclude = ["**/__pycache__"]
8073
include = ["src"]
8174

8275
[[tool.basedpyright.executionEnvironments]]
@@ -102,15 +95,9 @@ tag = false
10295
version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]"
10396

10497
[tool.bumpver.file_patterns]
105-
".copier/package.yml" = [
106-
'current_version: {version}'
107-
]
108-
"src/django_bird/__init__.py" = [
109-
'__version__ = "{version}"'
110-
]
111-
"tests/test_version.py" = [
112-
'assert __version__ == "{version}"'
113-
]
98+
".copier/package.yml" = ['current_version: {version}']
99+
"src/django_bird/__init__.py" = ['__version__ = "{version}"']
100+
"tests/test_version.py" = ['assert __version__ == "{version}"']
114101

115102
[tool.coverage.paths]
116103
source = ["src"]
@@ -127,11 +114,7 @@ exclude_lines = [
127114
fail_under = 98
128115

129116
[tool.coverage.run]
130-
omit = [
131-
"src/django_bird/migrations/*",
132-
"src/django_bird/_typing.py",
133-
"tests/*"
134-
]
117+
omit = ["src/django_bird/migrations/*", "src/django_bird/_typing.py", "tests/*"]
135118
source = ["src/django_bird"]
136119

137120
[tool.django-stubs]
@@ -147,10 +130,7 @@ indent = 2
147130
profile = "django"
148131

149132
[tool.hatch.build]
150-
exclude = [
151-
".*",
152-
"Justfile"
153-
]
133+
exclude = [".*", "Justfile"]
154134

155135
[tool.hatch.build.targets.wheel]
156136
packages = ["src/django_bird"]
@@ -160,37 +140,26 @@ path = "src/django_bird/__init__.py"
160140

161141
[tool.mypy]
162142
check_untyped_defs = true
163-
exclude = [
164-
"docs",
165-
"tests",
166-
"migrations",
167-
"venv",
168-
".venv"
169-
]
143+
exclude = ["docs", "tests", "migrations", "venv", ".venv"]
170144
mypy_path = "src/"
171145
no_implicit_optional = true
172-
plugins = [
173-
"mypy_django_plugin.main"
174-
]
146+
plugins = ["mypy_django_plugin.main"]
175147
warn_redundant_casts = true
176148
warn_unused_configs = true
177149
warn_unused_ignores = true
178150

179151
[[tool.mypy.overrides]]
180152
ignore_errors = true
181153
ignore_missing_imports = true
182-
module = [
183-
"*.migrations.*",
184-
"docs.*",
185-
"tests.*"
186-
]
154+
module = ["*.migrations.*", "docs.*", "tests.*"]
187155

188156
[tool.mypy_django_plugin]
189157
ignore_missing_model_attributes = true
190158

191159
[tool.pytest.ini_options]
192160
addopts = "--create-db -n auto --dist loadfile --doctest-modules"
193161
django_find_project = false
162+
markers = ["default_app_settings"]
194163
norecursedirs = ".* bin build dist *.egg htmlcov logs node_modules templates venv"
195164
python_files = "tests.py test_*.py *_tests.py"
196165
pythonpath = "src"

src/django_bird/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
class AppSettings:
1919
COMPONENT_DIRS: list[Path | str] = field(default_factory=list)
2020
ENABLE_AUTO_CONFIG: bool = True
21+
ENABLE_BIRD_ID_ATTR: bool = True
2122
_template_configurator: TemplateConfigurator = field(init=False)
2223

2324
def __post_init__(self):

src/django_bird/templatetags/tags/bird.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
from django_bird._typing import override
1414
from django_bird.components import Component
1515
from django_bird.components import components
16+
from django_bird.conf import app_settings
1617
from django_bird.params import Param
1718
from django_bird.params import Params
19+
from django_bird.params import Value
1820
from django_bird.slots import DEFAULT_SLOT
1921
from django_bird.slots import Slots
2022

@@ -27,6 +29,7 @@ def do_bird(parser: Parser, token: Token) -> BirdNode:
2729
if len(bits) == 1:
2830
msg = f"{TAG} tag requires at least one argument"
2931
raise template.TemplateSyntaxError(msg)
32+
3033
name = bits[1]
3134
attrs = []
3235
only = False
@@ -87,10 +90,14 @@ def get_component_context_data(
8790
self, component: Component, context: Context
8891
) -> dict[str, Any]:
8992
context_data: dict[str, Any] = {}
93+
9094
if not self.only:
9195
flattened = context.flatten()
9296
context_data = {str(k): v for k, v in flattened.items()}
9397

98+
if app_settings.ENABLE_BIRD_ID_ATTR:
99+
self.attrs.append(Param("data_bird_id", Value(component.id, True)))
100+
94101
params = Params.with_attrs(self.attrs)
95102
props = params.render_props(component.nodelist, context)
96103
attrs = params.render_attrs(context)

tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ def _override_app_settings(**kwargs):
107107
return _override_app_settings
108108

109109

110+
@pytest.fixture(autouse=True)
111+
def data_bird_attr_app_setting(override_app_settings, request):
112+
from django_bird.conf import app_settings
113+
114+
enable = (
115+
app_settings.ENABLE_BIRD_ID_ATTR
116+
if "default_app_settings" in request.keywords
117+
else False
118+
)
119+
120+
with override_app_settings(ENABLE_BIRD_ID_ATTR=enable):
121+
yield
122+
123+
110124
@pytest.fixture
111125
def create_template():
112126
def _create_template(template_file: Path) -> DjangoTemplate:

tests/templatetags/test_bird.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from django.template.exceptions import TemplateDoesNotExist
1313
from django.template.exceptions import TemplateSyntaxError
1414

15+
from django_bird.components import Component
1516
from django_bird.params import Param
1617
from django_bird.params import Value
1718
from django_bird.templatetags.tags.bird import END_TAG
@@ -633,6 +634,27 @@ def test_error_handling(self, test_case, templates_dir, normalize_whitespace):
633634

634635
assert normalize_whitespace(rendered) == test_case.expected
635636

637+
def test_data_bird_id(
638+
self, override_app_settings, templates_dir, normalize_whitespace
639+
):
640+
button = TestComponent(
641+
name="button",
642+
content="""
643+
<button {{ attrs }}>
644+
{{ slot }}
645+
</button>
646+
""",
647+
).create(templates_dir)
648+
649+
template = Template("{% bird 'button' %}Click me{% endbird %}")
650+
651+
with override_app_settings(ENABLE_BIRD_ID_ATTR=True):
652+
rendered = template.render(Context({}))
653+
654+
comp = Component.from_name(button.name)
655+
656+
assert comp.id in rendered
657+
636658

637659
class TestProperties:
638660
@pytest.mark.parametrize(

tests/test_conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
from django_bird.conf import app_settings
1010

1111

12+
@pytest.mark.default_app_settings
1213
def test_app_settings():
14+
assert app_settings.COMPONENT_DIRS == []
1315
assert app_settings.ENABLE_AUTO_CONFIG is True
16+
assert app_settings.ENABLE_BIRD_ID_ATTR is True
1417

1518

1619
@override_settings(

0 commit comments

Comments
 (0)