Skip to content

Commit 8519dfa

Browse files
add new {% bird:var %} templatetag (#170)
* add new `{% bird:var %}` templatetag * Add tests for custom template variable tag * Escape regex special characters in pytest raises matcher for template tag tests * Add tests for variable context isolation in template components * Add tests for var context and nested component behavior * Remove unused end var tag and simplify var tag implementation * Add documentation for project variables * Add documentation for component variables with `bird:var` tag * Add documentation for explicit variable cleanup with `endbird:var` * Add support for explicit variable cleanup in Django Bird template tags * Update documentation for django-bird variable tag syntax and examples * Update "Basic Usage" section in documentation with improved examples and clarity * Add test for resetting template variable to None * update changelog * tweak * add to sidebar * Update documentation for django-bird variables with clearer examples and explanations * tweak * rweak * uv
1 parent eb0398e commit 8519dfa

File tree

7 files changed

+527
-0
lines changed

7 files changed

+527
-0
lines changed

CHANGELOG.md

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

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- Added the `{% bird:var %}` templatetag for managing local context variables within components, including support for appending, overwriting, and resetting values. Variables are scoped to components and automatically are cleaned up when the component finishes rendering, with optional explicit cleanup via `{% endbird:var %}`.
24+
2125
## [0.13.2]
2226

2327
### Fixed
28+
2429
- Fixed static file URL generation to use correct relative paths when serving component assets through Django's static file storage system.
2530

2631
## [0.13.1]

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
:maxdepth: 3
1414
Naming <naming>
1515
Attributes/Properties <params>
16+
Variables <vars>
1617
slots
1718
Assets <assets>
1819
Organization <organization>

docs/vars.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Variables in Components
2+
3+
django-bird provides a way to manage local variables within components using the `{% bird:var %}` template tag. Similar to Django's built-in `{% with %}` tag, it allows you to create temporary variables, but with some key advantages:
4+
5+
- No closing tag required (unlike `{% with %}` which needs `{% endwith %}`)
6+
- Variables are automatically cleaned up when the component finishes rendering
7+
- Variables are properly scoped to each component instance
8+
- Supports appending to existing values
9+
10+
## Basic Usage
11+
12+
The `{% bird:var %}` tag allows you to create and modify variables that are scoped to the current component. These variables are accessible through the `vars` context dictionary.
13+
14+
### Creating Variables
15+
16+
To create a new variable, use the assignment syntax:
17+
18+
```htmldjango
19+
{% bird:var name='value' %}
20+
21+
{{ vars.name }} {# Outputs: value #}
22+
```
23+
24+
### Overwriting and Clearing Variables
25+
26+
You can overwrite an existing variable by assigning a new value:
27+
28+
```htmldjango
29+
{% bird:var counter='1' %}
30+
31+
{{ vars.counter }} {# Outputs: 1 #}
32+
33+
{% bird:var counter='2' %}
34+
35+
{{ vars.counter }} {# Outputs: 2 #}
36+
```
37+
38+
To reset/clear a variable, set it to None:
39+
40+
```htmldjango
41+
{% bird:var message='hello' %}
42+
43+
{{ vars.message }} {# Outputs: hello #}
44+
45+
{% bird:var message=None %}
46+
47+
{{ vars.message }} {# Variable is cleared #}
48+
```
49+
50+
Alternatively, you can use explicit cleanup with `{% endbird:var %}` - see [Explicit Variable Cleanup](#explicit-variable-cleanup) for details.
51+
52+
### Appending to Variables
53+
54+
The `+=` operator lets you append to existing variables:
55+
56+
```htmldjango
57+
{% bird:var greeting='Hello' %}
58+
{% bird:var greeting+=' World' %}
59+
60+
{{ vars.greeting }} {# Outputs: Hello World #}
61+
```
62+
63+
If you append to a non-existent variable, it will be created:
64+
65+
```htmldjango
66+
{% bird:var message+='World' %}
67+
68+
{{ vars.message }} {# Outputs: World #}
69+
```
70+
71+
## Variable Scope
72+
73+
Variables created with `{% bird:var %}` are:
74+
75+
- Local to the component where they are defined
76+
- Isolated between different instances of the same component
77+
- Not accessible outside the component
78+
- Reset between renders
79+
80+
```{code-block} htmldjango
81+
:caption: templates/bird/button.html
82+
83+
{% bird:var count='1' %}
84+
85+
Count: {{ vars.count }}
86+
```
87+
88+
```{code-block} htmldjango
89+
:caption: template.html
90+
91+
{% bird button %}{% endbird %} {# Count: 1 #}
92+
{% bird button %}{% endbird %} {# Count: 1 #}
93+
94+
Outside: {{ vars.count }} {# vars.count is not accessible here #}
95+
```
96+
97+
Each instance of the button component will have its own isolated `count` variable.
98+
99+
### Explicit Variable Cleanup
100+
101+
While variables are automatically cleaned up when a component finishes rendering, you can explicitly clean up variables using the `{% endbird:var %}` tag:
102+
103+
```htmldjango
104+
{% bird:var message='Hello' %}
105+
106+
{{ vars.message }} {# Outputs: Hello #}
107+
108+
{% endbird:var message %}
109+
110+
{{ vars.message }} {# Variable is now cleaned up #}
111+
```
112+
113+
This can be useful when you want to ensure a variable is cleaned up at a specific point in your template, rather than waiting for the component to finish rendering.
114+
115+
You can clean up multiple variables independently:
116+
117+
```htmldjango
118+
{% bird:var x='hello' %}
119+
{% bird:var y='world' %}
120+
121+
{{ vars.x }} {{ vars.y }} {# Outputs: hello world #}
122+
123+
{% endbird:var x %}
124+
125+
{{ vars.x }} {{ vars.y }} {# Outputs: world (x is cleaned up) #}
126+
127+
{% endbird:var y %}
128+
129+
{{ vars.x }} {{ vars.y }} {# Both variables are now cleaned up #}
130+
```
131+
132+
## Working with Template Variables
133+
134+
You can use template variables when setting values:
135+
136+
```htmldjango
137+
{% bird:var greeting='Hello ' %}
138+
{% bird:var greeting+=user.name %}
139+
140+
{{ vars.greeting }} {# Outputs: Hello John #}
141+
```
142+
143+
## Nested Components
144+
145+
Variables are properly scoped in nested components:
146+
147+
```{code-block} htmldjango
148+
:caption: templates/bird/outer.html
149+
150+
{% bird:var message='Outer' %}
151+
152+
{{ vars.message }}
153+
154+
{% bird inner %}{% endbird %}
155+
156+
{{ vars.message }} {# vars.message still contains 'Outer' here #}
157+
```
158+
159+
```{code-block} htmldjango
160+
:caption: templates/bird/inner.html
161+
162+
{% bird:var message='Inner' %}
163+
164+
{{ vars.message }} {# Contains 'Inner', doesn't affect outer component #}
165+
```

src/django_bird/components.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def render(self, context: Context):
151151
"props": props,
152152
"slot": slots.get(DEFAULT_SLOT),
153153
"slots": slots,
154+
"vars": {},
154155
}
155156
):
156157
return self.component.template.template.render(context)

src/django_bird/templatetags/django_bird.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .tags import bird
77
from .tags import prop
88
from .tags import slot
9+
from .tags import var
910

1011
register = template.Library()
1112

@@ -15,3 +16,5 @@
1516
register.tag(bird.TAG, bird.do_bird)
1617
register.tag(prop.TAG, prop.do_prop)
1718
register.tag(slot.TAG, slot.do_slot)
19+
register.tag(var.TAG, var.do_var)
20+
register.tag(var.END_TAG, var.do_end_var)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from typing import Any
5+
from typing import final
6+
7+
from django import template
8+
from django.template.base import Parser
9+
from django.template.base import Token
10+
from django.template.context import Context
11+
12+
from django_bird._typing import override
13+
14+
register = template.Library()
15+
16+
TAG = "bird:var"
17+
END_TAG = "endbird:var"
18+
19+
OPERATOR_PATTERN = re.compile(r"(\w+)\s*(\+=|=)\s*(.+)")
20+
21+
22+
@register.tag(name=TAG)
23+
def do_var(parser: Parser, token: Token):
24+
_tag, *bits = token.split_contents()
25+
if not bits:
26+
msg = f"'{TAG}' tag requires an assignment"
27+
raise template.TemplateSyntaxError(msg)
28+
29+
var_assignment = bits.pop(0)
30+
match = re.match(OPERATOR_PATTERN, var_assignment)
31+
if not match:
32+
msg = (
33+
f"Invalid assignment in '{TAG}' tag: {var_assignment}. "
34+
f"Expected format: {TAG} variable='value' or {TAG} variable+='value'."
35+
)
36+
raise template.TemplateSyntaxError(msg)
37+
38+
var_name, operator, var_value = match.groups()
39+
var_value = var_value.strip()
40+
value = parser.compile_filter(var_value)
41+
42+
return VarNode(var_name, operator, value)
43+
44+
45+
@register.tag(name=END_TAG)
46+
def do_end_var(_parser: Parser, token: Token):
47+
_tag, *bits = token.split_contents()
48+
if not bits:
49+
msg = f"{token.contents.split()[0]} tag requires a variable name"
50+
raise template.TemplateSyntaxError(msg)
51+
52+
var_name = bits.pop(0)
53+
54+
return EndVarNode(var_name)
55+
56+
57+
@final
58+
class VarNode(template.Node):
59+
def __init__(self, name: str, operator: str, value: Any):
60+
self.name = name
61+
self.operator = operator
62+
self.value = value
63+
64+
@override
65+
def render(self, context: Context) -> str:
66+
if "vars" not in context:
67+
context["vars"] = {}
68+
69+
value = self.value.resolve(context)
70+
71+
if self.operator == "+=":
72+
previous = context["vars"].get(self.name, "")
73+
value = f"{previous}{value}"
74+
75+
context["vars"][self.name] = value
76+
return ""
77+
78+
79+
@final
80+
class EndVarNode(template.Node):
81+
def __init__(self, name: str):
82+
self.name = name
83+
84+
@override
85+
def render(self, context: Context) -> str:
86+
if "vars" in context and self.name in context["vars"]:
87+
del context["vars"][self.name]
88+
return ""

0 commit comments

Comments
 (0)