Skip to content

Commit 1ddaea9

Browse files
add {% bird:prop %} templatetag (#54)
* add `{% bird:prop %}` templatetag * fix types * add tests for prop name * update changelog * update docs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * tweak attrs distinction and add section about multiple props * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update params.py * add tests for render props method * adjust None test --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent a6370b2 commit 1ddaea9

File tree

12 files changed

+372
-65
lines changed

12 files changed

+372
-65
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+
- Created `{% bird:prop %}` tag for defining properties within components. These operate similarly to the `{{ attrs }}` template context variable, but allow for setting defaults. Any attributes passed to a component will override the prop's default value, and props defined in a component template are automatically removed from the component's `attrs`. Props are accessible in templates via the `props` context variable (e.g. `{{ props.id }}`)
24+
2125
## [0.2.0]
2226

2327
🚨 This release contains a breaking change. See the Changed section for more information. 🚨

docs/attrs.md

Lines changed: 0 additions & 52 deletions
This file was deleted.

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
:hidden:
1313
:maxdepth: 3
1414
Naming <naming>
15-
attrs
15+
Attributes/Properties <params>
1616
slots
1717
Organization <organization>
1818
configuration

docs/params.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Passing Parameters to Components
2+
3+
django-bird provides two ways to pass parameters to your components: attributes and properties. While attributes and properties may look similar when using a component, they serve different purposes.
4+
5+
Attributes are made available to your component template as a flattened string via the `{{ attrs }}` template context variable which can be used to apply HTML attributes to elements, while properties are accessible as individual values (e.g. `{{ props.variant }}`) that your component can use internally to control its rendering logic.
6+
7+
Note that these parameters are distinct from [Slots](slots.md) - they are used to configured how your component behaves or renders, while slots define where content should be inserted into your component's template.
8+
9+
For example, a button component might use properties to control its styling and attributes to set HTML attributes, while using slots to define its content:
10+
11+
```htmldjango
12+
{% bird button variant="primary" data-analytics="signup" %}
13+
Click here {# This content will go in the default slot #}
14+
{% endbird %}
15+
```
16+
17+
## Attributes
18+
19+
Attributes (i.e. `attrs`) let you pass additional HTML attributes to your components. This feature provides flexibility and customization without modifying the component template.
20+
21+
In your component template, the `{{ attrs }}` variable is a special variable that contains all the attributes passed to the component as a pre-rendered string. Unlike props which can be accessed individually, attributes are flattened into a single string ready to be inserted into an HTML element. The `{{ attrs }}` variable automatically handles both key-value attributes (like `class="btn"`) and boolean attributes (like `disabled`).
22+
23+
### Basic Usage
24+
25+
Here's a simple example of a button component that accepts attributes:
26+
27+
```{code-block} htmldjango
28+
:caption: templates/bird/button.html
29+
30+
<button {{ attrs }}>
31+
{{ slot }}
32+
</button>
33+
```
34+
35+
Use this component and pass attributes like this:
36+
37+
```htmldjango
38+
{% bird button class="btn" %}
39+
Click me!
40+
{% endbird %}
41+
```
42+
43+
It will render as:
44+
45+
```html
46+
<button class="btn">
47+
Click me!
48+
</button>
49+
50+
```
51+
52+
### Multiple Attributes
53+
54+
You can pass multiple attributes to a component:
55+
56+
```htmldjango
57+
{% bird button class="btn btn-primary" id="submit-btn" disabled %}
58+
Submit
59+
{% endbird %}
60+
```
61+
62+
This will render as:
63+
64+
```html
65+
<button class="btn btn-primary" id="submit-btn" disabled>
66+
Submit
67+
</button>
68+
```
69+
70+
## Properties
71+
72+
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.
73+
74+
In your component template, props are defined using the `{% bird:prop %}` tag and accessed via the `{{ props }}` context variable. You can define as many props as needed using separate `{% bird:prop %}` tags. When a prop is defined, any matching attribute passed to the component will be removed from `{{ attrs }}` and made available in `{{ props }}` instead.
75+
76+
### Basic Usage
77+
78+
Here's a simple example of a button component that uses props:
79+
80+
```{code-block} htmldjango
81+
:caption: templates/bird/button.html
82+
83+
{% bird:prop variant='primary' %}
84+
<button class="btn btn-{{ props.variant }}" {{ attrs }}>
85+
{{ slot }}
86+
</button>
87+
```
88+
89+
Use this component and override the default variant like this:
90+
91+
```htmldjango
92+
{% bird button variant="secondary" id="secondary-button" %}
93+
Click me!
94+
{% endbird %}
95+
```
96+
97+
It will render as:
98+
99+
```html
100+
<button class="btn btn-secondary" id="secondary-button">
101+
Click me!
102+
</button>
103+
```
104+
105+
Notice how this works:
106+
107+
- The `variant` attribute is removed from `attrs` because it matches a defined prop
108+
- Its value "secondary" is made available as `props.variant`
109+
- The `id` attribute remains in `attrs` since it's not defined as a prop
110+
- The final HTML only includes `variant`'s value as part of the class name, while `id` appears as a direct attribute
111+
112+
This separation allows you to use props to control your component's logic while still accepting arbitrary HTML attributes.
113+
114+
### Multiple Props
115+
116+
Components often need multiple props to control different aspects of their behavior. Each prop is defined with its own `{% bird:prop %}` tag:
117+
118+
```{code-block} htmldjango
119+
:caption: templates/bird/button.html
120+
121+
{% bird:prop variant='primary' %}
122+
{% bird:prop size='md' %}
123+
<button
124+
class="btn btn-{{ props.variant }} btn-{{ props.size }}"
125+
{{ attrs }}
126+
>
127+
{{ slot }}
128+
</button>
129+
```
130+
131+
Use the component by setting any combination of these props:
132+
133+
```htmldjango
134+
{% bird button variant="secondary" size="lg" disabled=True %}
135+
Click me!
136+
{% endbird %}
137+
```
138+
139+
It will render as:
140+
141+
```html
142+
<button class="btn btn-secondary btn-lg" disabled>
143+
Click me!
144+
</button>
145+
```
146+
147+
This approach of using separate tags for each prop makes it easier to expand the prop system in the future - for example, adding features like type validation or choice constraints while maintaining a clean syntax.
148+
149+
### Props with Defaults
150+
151+
Props can be defined with or without default values:
152+
153+
```htmldjango
154+
{% bird:prop id %} {# No default value #}
155+
{% bird:prop variant='primary' %} {# With default value #}
156+
<button
157+
id="{{ props.id }}"
158+
class="btn btn-{{ props.variant }}"
159+
{{ attrs }}
160+
>
161+
{{ slot }}
162+
</button>
163+
```
164+
165+
When used, props will take their value from either the passed attribute or fall back to their default:
166+
167+
```htmldjango
168+
{% bird button variant="secondary" %}
169+
Submit
170+
{% endbird %}
171+
```
172+
173+
This will render as:
174+
175+
```html
176+
<button id="" class="btn btn-secondary">
177+
Submit
178+
</button>
179+
```
180+
181+
```{note}
182+
Props defined without a default value will render as an empty string if no value is provided when using the component. This behavior may change in a future version to either require default values or handle undefined props differently.
183+
```

src/django_bird/components/params.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from dataclasses import field
45

56
from django import template
7+
from django.template.base import NodeList
68
from django.template.context import Context
79
from django.utils.safestring import SafeString
810
from django.utils.safestring import mark_safe
@@ -39,7 +41,35 @@ def from_bit(cls, bit: str):
3941

4042
@dataclass
4143
class Params:
42-
attrs: list[Param]
44+
attrs: list[Param] = field(default_factory=list)
45+
props: list[Param] = field(default_factory=list)
46+
47+
def render_props(self, nodelist: NodeList | None, context: Context):
48+
from django_bird.templatetags.tags.prop import PropNode
49+
50+
if nodelist is None:
51+
return
52+
53+
attrs_to_remove = set()
54+
55+
for node in nodelist:
56+
if not isinstance(node, PropNode):
57+
continue
58+
59+
value: str | bool | None = node.default
60+
61+
for idx, attr in enumerate(self.attrs):
62+
if node.name == attr.name:
63+
if attr.value:
64+
value = attr.value
65+
attrs_to_remove.add(idx)
66+
67+
self.props.append(Param(name=node.name, value=value))
68+
69+
for idx in sorted(attrs_to_remove, reverse=True):
70+
self.attrs.pop(idx)
71+
72+
return {prop.name: prop.value for prop in self.props}
4373

4474
def render_attrs(self, context: Context) -> SafeString:
4575
rendered = " ".join(attr.render(context) for attr in self.attrs)

src/django_bird/templatetags/django_bird.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from django import template
44

55
from .tags import bird
6+
from .tags import prop
67
from .tags import slot
78

89
register = template.Library()
910

1011

1112
register.tag(bird.TAG, bird.do_bird)
13+
register.tag(prop.TAG, prop.do_prop)
1214
register.tag(slot.TAG, slot.do_slot)
Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +0,0 @@
1-
from __future__ import annotations
2-
3-
from .bird import do_bird
4-
from .slot import do_slot
5-
6-
__all__ = [
7-
"do_bird",
8-
"do_slot",
9-
]

src/django_bird/templatetags/tags/bird.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django import template
77
from django.template.base import NodeList
88
from django.template.base import Parser
9+
from django.template.base import Template
910
from django.template.base import Token
1011
from django.template.context import Context
1112
from django.template.loader import select_template
@@ -61,9 +62,9 @@ def __init__(self, name: str, params: Params, nodelist: NodeList | None) -> None
6162
@override
6263
def render(self, context: Context) -> SafeString:
6364
component_name = self.get_component_name(context)
64-
component_context = self.get_component_context_data(context)
6565
template_names = get_template_names(component_name)
6666
template = select_template(template_names)
67+
component_context = self.get_component_context_data(template.template, context)
6768
return template.render(component_context)
6869

6970
def get_component_name(self, context: Context) -> str:
@@ -73,12 +74,16 @@ def get_component_name(self, context: Context) -> str:
7374
name = self.name
7475
return name
7576

76-
def get_component_context_data(self, context: Context) -> dict[str, Any]:
77+
def get_component_context_data(
78+
self, template: Template, context: Context
79+
) -> dict[str, Any]:
80+
props = self.params.render_props(template.nodelist, context)
7781
attrs = self.params.render_attrs(context)
7882
slots = Slots.collect(self.nodelist, context).render()
7983
default_slot = slots.get(DEFAULT_SLOT) or context.get("slot")
8084
return {
8185
"attrs": attrs,
86+
"props": props,
8287
"slot": default_slot,
8388
"slots": slots,
8489
}

0 commit comments

Comments
 (0)