Repository: joshuadavidthomas/django-bird

Issue:
Components cannot take outside template context variables
Was playing around with moving some of my base templates to bird components, one of which set's the CSRF Token in the body tag like so:

```htmldjango
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
   ...
</body>
```

But when rendered, it comes out:

```html
<body hx-headers='{"X-CSRFToken": ""}'>
   ...
</body>
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc506d2..8c3c252 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,14 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
 
 ## [Unreleased]
 
+### Added
+
+- Added `only` keyword to `{% bird %}` tag for isolating component context that, when used, components cannot access their parent template's context, e.g., `{% bird button only %}`.
+
+### Changed
+
+- Changed handling of self-closing indicator (`/`) in `{% bird %}` tag to always treat it as a syntax marker rather adding to the component's template context.
+
 ## [0.8.2]
 
 ### Fixed
diff --git a/docs/params.md b/docs/params.md
index 92229ad..f724a2a 100644
--- a/docs/params.md
+++ b/docs/params.md
@@ -281,3 +281,50 @@ Renders as:
 ```html
 <button class="themed-button" disabled>Click me</button>
 ```
+
+## Context Isolation
+
+By default, components have access to their parent template's context. This means variables defined in the parent template are available inside the component.
+
+You can use the `only` keyword to prevent a component from accessing its parent context:
+
+```htmldjango
+{% bird button only %}
+    Click me
+{% endbird %}
+```
+
+When `only` is used:
+
+- The component cannot access variables from the parent context
+- Props, slots, and other component-specific context still work normally
+- Default values in the component template will be used when parent context variables are not available
+
+### Examples
+
+Without `only`, components can access parent context:
+
+```htmldjango
+{# Parent template with user in context #}
+{% bird button %}
+    {{ user.name }}  {# Will render "John" #}
+{% endbird %}
+```
+
+With `only`, parent context is isolated:
+
+```htmldjango
+{# Parent template with user in context #}
+{% bird button only %}
+    {{ user.name|default:"Anonymous" }}  {# Will render "Anonymous" #}
+{% endbird %}
+```
+
+Props and slots still work with `only`:
+
+```htmldjango
+{% bird button variant="primary" only %}
+    {% bird:slot prefix %}→{% endbird:slot %}
+    Submit
+{% endbird %}
+```
diff --git a/src/django_bird/templatetags/tags/bird.py b/src/django_bird/templatetags/tags/bird.py
index 54e975e..ab2d9ad 100644
--- a/src/django_bird/templatetags/tags/bird.py
+++ b/src/django_bird/templatetags/tags/bird.py
@@ -26,11 +26,20 @@ def do_bird(parser: Parser, token: Token) -> BirdNode:
     bits = token.split_contents()
     name = parse_bird_name(bits)
     attrs = []
+    only = False
+
     for bit in bits[2:]:
-        param = Param.from_bit(bit)
-        attrs.append(param)
+        match bit:
+            case "only":
+                only = True
+            case "/":
+                continue
+            case _:
+                param = Param.from_bit(bit)
+                attrs.append(param)
+
     nodelist = parse_nodelist(bits, parser)
-    return BirdNode(name, attrs, nodelist)
+    return BirdNode(name, attrs, nodelist, only)
 
 
 def parse_bird_name(bits: TagBits) -> str:
@@ -57,11 +66,16 @@ def parse_nodelist(bits: TagBits, parser: Parser) -> NodeList | None:
 
 class BirdNode(template.Node):
     def __init__(
-        self, name: str, attrs: list[Param], nodelist: NodeList | None
+        self,
+        name: str,
+        attrs: list[Param],
+        nodelist: NodeList | None,
+        only: bool = False,
     ) -> None:
         self.name = name
         self.attrs = attrs
         self.nodelist = nodelist
+        self.only = only
 
     @override
     def render(self, context: Context) -> str:
@@ -80,14 +94,24 @@ class BirdNode(template.Node):
     def get_component_context_data(
         self, component: Component, context: Context
     ) -> dict[str, Any]:
+        context_data: dict[str, Any] = {}
+        if not self.only:
+            flattened = context.flatten()
+            context_data = {str(k): v for k, v in flattened.items()}
+
         params = Params.with_attrs(self.attrs)
         props = params.render_props(component.nodelist, context)
         attrs = params.render_attrs(context)
         slots = Slots.collect(self.nodelist, context).render()
         default_slot = slots.get(DEFAULT_SLOT) or context.get("slot")
-        return {
-            "attrs": attrs,
-            "props": props,
-            "slot": default_slot,
-            "slots": slots,
-        }
+
+        context_data.update(
+            {
+                "attrs": attrs,
+                "props": props,
+                "slot": default_slot,
+                "slots": slots,
+            }
+        )
+
+        return context_data


Test patch:
diff --git a/tests/templatetags/test_bird.py b/tests/templatetags/test_bird.py
index 5335609..2d27211 100644
--- a/tests/templatetags/test_bird.py
+++ b/tests/templatetags/test_bird.py
@@ -1145,6 +1145,188 @@ def test_nested_components_with_loops(templates_dir, normalize_whitespace):
     """)
 
 
+@pytest.mark.parametrize(
+    "test_case",
+    [
+        TestComponentCase(
+            description="Access parent context variable",
+            component=TestComponent(
+                name="button",
+                content="""
+                    <button>
+                        {{ user.name }}
+                    </button>
+                """,
+            ),
+            template_content="""
+                {% bird button %}{% endbird %}
+            """,
+            template_context={"user": {"name": "John"}},
+            expected="<button>John</button>",
+        ),
+        TestComponentCase(
+            description="Access parent context in slot",
+            component=TestComponent(
+                name="button",
+                content="""
+                    <button>
+                        {{ slot }}
+                    </button>
+                """,
+            ),
+            template_content="""
+                {% bird button %}
+                    {{ user.name }}
+                {% endbird %}
+            """,
+            template_context={"user": {"name": "John"}},
+            expected="<button>John</button>",
+        ),
+        TestComponentCase(
+            description="Access parent context in named slot",
+            component=TestComponent(
+                name="button",
+                content="""
+                    <button>
+                        {% bird:slot prefix %}{% endbird:slot %}
+                        {{ slot }}
+                    </button>
+                """,
+            ),
+            template_content="""
+                {% bird button %}
+                    {% bird:slot prefix %}{{ user.role }}{% endbird:slot %}
+                    {{ user.name }}
+                {% endbird %}
+            """,
+            template_context={"user": {"name": "John", "role": "Admin"}},
+            expected="<button>Admin John</button>",
+        ),
+        TestComponentCase(
+            description="Component-specific context overrides parent context values",
+            component=TestComponent(
+                name="button",
+                content="""
+                    <button {{ attrs }}>
+                        {{ slot }}
+                    </button>
+                """,
+            ),
+            template_content="""
+                {% bird button id="foo" %}
+                    Component Content
+                {% endbird %}
+            """,
+            template_context={
+                "slot": "Parent Content",
+                "attrs": 'id="bar"',
+            },
+            expected='<button id="foo">Component Content</button>',
+        ),
+    ],
+    ids=lambda x: x.description,
+)
+def test_parent_context_access(test_case, templates_dir, normalize_whitespace):
+    test_case.component.create(templates_dir)
+
+    template = Template(test_case.template_content)
+    rendered = template.render(Context(test_case.template_context))
+
+    assert normalize_whitespace(rendered) == test_case.expected
+
+
+@pytest.mark.parametrize(
+    "test_case",
+    [
+        TestComponentCase(
+            description="Only flag prevents access to parent context",
+            component=TestComponent(
+                name="button",
+                content="""
+                    <button>
+                        {{ user.name|default:"Anonymous" }}
+                    </button>
+                """,
+            ),
+            template_content="""
+                {% bird button only %}{% endbird %}
+            """,
+            template_context={"user": {"name": "John"}},
+            expected="<button>Anonymous</button>",
+        ),
+        TestComponentCase(
+            description="Only flag still allows props and slots",
+            component=TestComponent(
+                name="button",
+                content="""
+                    {% bird:prop label %}
+                    <button {{ attrs }}>
+                        {{ props.label }}
+                        {{ slot }}
+                        {{ user.name|default:"Anonymous" }}
+                    </button>
+                """,
+            ),
+            template_content="""
+                {% bird button id="foo" label="Click" only %}
+                    Content
+                {% endbird %}
+            """,
+            template_context={
+                "props": {
+                    "label": "Outside",
+                },
+                "user": {"name": "John"},
+            },
+            expected='<button id="foo">Click Content Anonymous</button>',
+        ),
+        TestComponentCase(
+            description="Only flag with named slots",
+            component=TestComponent(
+                name="button",
+                content="""
+                    <button>
+                        {% bird:slot prefix %}{% endbird:slot %}
+                        {{ user.name|default:"Anonymous" }}
+                    </button>
+                """,
+            ),
+            template_content="""
+                {% bird button only %}
+                    {% bird:slot prefix %}{{ user.role }}{% endbird:slot %}
+                {% endbird %}
+            """,
+            template_context={"user": {"name": "John", "role": "Admin"}},
+            expected="<button>Admin Anonymous</button>",
+        ),
+        TestComponentCase(
+            description="Only flag with self-closing tag",
+            component=TestComponent(
+                name="button",
+                content="""
+                    <button>
+                        {{ user.name|default:"Anonymous" }}
+                    </button>
+                """,
+            ),
+            template_content="""
+                {% bird button only / %}
+            """,
+            template_context={"user": {"name": "John"}},
+            expected="<button>Anonymous</button>",
+        ),
+    ],
+    ids=lambda x: x.description,
+)
+def test_only_flag(test_case, templates_dir, normalize_whitespace):
+    test_case.component.create(templates_dir)
+
+    template = Template(test_case.template_content)
+    rendered = template.render(Context(test_case.template_context))
+
+    assert normalize_whitespace(rendered) == test_case.expected
+
+
 class TestBirdNode:
     @pytest.mark.parametrize(
         "test_case",