Repository: joshuadavidthomas/django-bird

Issue:
Allow for dynamic component name
> [@josh](https://social.joshthomas.dev/@josh) pretty cool, can you use variables for component names? Then the choice of component could come from the database (for example)

via https://mastodon.social/@hvdklauw/113271880969606494

Discussion:
joshuadavidthomas: > An example of using this for the docs would be handy.

@carltongibson Yup! It's why I haven't merged it yet. Thanks for the quick review ☺️

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3daf03..7f24636 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
 
 🚨 This release contains a breaking change. See the Changed section for more information. 🚨
 
+### Added
+
+- Added support for dynamic component names in `{% bird %}` tag. Component names can now be variables, e.g. `{% bird component_type %}` where `component_type` is a variable in the template context.
+
 ### Changed
 
 - Reversed template resolution order to prefer component-specific templates over generic ones.
diff --git a/docs/index.md b/docs/index.md
index c8f5e45..ffda223 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -11,6 +11,7 @@
 ```{toctree}
 :hidden:
 :maxdepth: 3
+Naming <naming>
 attrs
 slots
 Organization <organization>
diff --git a/docs/naming.md b/docs/naming.md
new file mode 100644
index 0000000..1d5ed69
--- /dev/null
+++ b/docs/naming.md
@@ -0,0 +1,74 @@
+# Naming Components
+
+Component names in django-bird are derived from their file locations and names within your templates directory. The naming system is flexible and supports both simple and complex component structures.
+
+For detailed organization patterns and real-world examples, see the [Organizing Components](organization.md) documentation.
+
+## Basic Naming
+
+The simplest way to name a component is through its filename in the `bird` directory:
+
+```bash
+templates/
+└── bird/
+    └── button.html
+```
+
+This creates a component that can be used as:
+
+```htmldjango
+{% bird button %}
+    Click me!
+{% endbird %}
+```
+
+## Nested Names
+
+Component names can include dots to represent directory structure:
+
+```htmldjango
+{% bird icon.arrow-down / %}
+```
+
+This maps to either a file path with dots (`icon.arrow-down.html`):
+
+```bash
+templates/
+└── bird/
+    └── icon.arrow-down.html
+```
+
+Or a nested directory structure (`icon/arrow-down.html`):
+
+```bash
+templates/
+└── bird/
+    └── icon/
+        └── arrow-down.html
+```
+
+See [Organizing Components](organization.md) for detailed directory structure examples.
+
+## Dynamic Names
+
+Component names can be dynamic, using template variables:
+
+```htmldjango
+{% with component_name="icon.arrow-down" %}
+    {% bird component_name / %}
+{% endwith %}
+```
+
+This is particularly useful when the component choice needs to be determined at runtime in your Django view.
+
+## Template Resolution
+
+django-bird follows these rules when looking for component templates:
+
+1. Searches custom directories specified in `COMPONENT_DIRS` setting, then the default `bird` directory
+2. For a component in a directory (e.g., `accordion`), looks for:
+   - `accordion/accordion.html`
+   - `accordion/index.html`
+   - `accordion.html`
+
+This flexibility allows you to organize components according to your project's needs while maintaining consistent usage patterns.s
diff --git a/src/django_bird/templatetags/django_bird.py b/src/django_bird/templatetags/django_bird.py
index 7c60d91..d54ab53 100644
--- a/src/django_bird/templatetags/django_bird.py
+++ b/src/django_bird/templatetags/django_bird.py
@@ -53,11 +53,19 @@ class BirdNode(template.Node):
 
     @override
     def render(self, context: Context) -> SafeString:
+        component_name = self.get_component_name(context)
         component_context = self.get_component_context_data(context)
-        template_names = self.get_template_names()
+        template_names = self.get_template_names(component_name)
         template = select_template(template_names)
         return template.render(component_context)
 
+    def get_component_name(self, context: Context) -> str:
+        try:
+            name = template.Variable(self.name).resolve(context)
+        except template.VariableDoesNotExist:
+            name = self.name
+        return name
+
     def get_component_context_data(self, context: Context) -> dict[str, Any]:
         rendered_slots = self.render_slots(context)
         flat_attrs = self.flatten_attrs(context)
@@ -113,7 +121,7 @@ class BirdNode(template.Node):
             for key, value in attrs.items()
         )
 
-    def get_template_names(self):
+    def get_template_names(self, name: str) -> list[str]:
         """
         Generate a list of potential template names for a component.
 
@@ -151,7 +159,7 @@ class BirdNode(template.Node):
         template_names = []
         component_dirs = list(dict.fromkeys([*app_settings.COMPONENT_DIRS, "bird"]))
 
-        name_parts = self.name.split(".")
+        name_parts = name.split(".")
         path_name = "/".join(name_parts)
 
         for component_dir in component_dirs:


Test patch:
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
index a404106..f693ccb 100644
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -60,31 +60,52 @@ class TestBirdTemplateTag:
         assert node.attrs == expected_attrs
 
     @pytest.mark.parametrize(
-        "component,template,expected",
+        "component,template,context,expected",
         [
             (
                 "<button>Click me</button>",
                 "{% bird button %}Click me{% endbird %}",
+                {},
                 "<button>Click me</button>",
             ),
             (
                 "<button>Click me</button>",
                 "{% bird 'button' %}Click me{% endbird %}",
+                {},
                 "<button>Click me</button>",
             ),
             (
                 "<button>Click me</button>",
                 '{% bird "button" %}Click me{% endbird %}',
+                {},
+                "<button>Click me</button>",
+            ),
+            (
+                "<button>Click me</button>",
+                "{% with dynamic_name='button' %}{% bird dynamic_name %}Click me{% endbird %}{% endwith %}",
+                {},
+                "<button>Click me</button>",
+            ),
+            (
+                "<button>Click me</button>",
+                "{% bird dynamic-name %}Click me{% endbird %}",
+                {"dynamic-name": "button"},
                 "<button>Click me</button>",
             ),
         ],
     )
     def test_rendered_name(
-        self, component, template, expected, create_bird_template, normalize_whitespace
+        self,
+        component,
+        template,
+        context,
+        expected,
+        create_bird_template,
+        normalize_whitespace,
     ):
         create_bird_template("button", component)
         t = Template(template)
-        rendered = t.render(context=Context({}))
+        rendered = t.render(context=Context(context))
         assert normalize_whitespace(rendered) == expected
 
     @pytest.mark.parametrize(
@@ -236,6 +257,29 @@ class TestBirdTemplateTagFutureFeatures:
 
 
 class TestBirdNode:
+    @pytest.mark.parametrize(
+        "name,context,expected",
+        [
+            (
+                "button",
+                {},
+                "button",
+            ),
+            (
+                "dynamic-name",
+                {"dynamic-name": "button"},
+                "button",
+            ),
+        ],
+    )
+    def test_get_component_name(self, name, context, expected, create_bird_template):
+        create_bird_template(name=name, content="<button>Click me</button>")
+        node = BirdNode(name=name, attrs=[], nodelist=None)
+
+        component_name = node.get_component_name(context=Context(context))
+
+        assert component_name == expected
+
     @pytest.mark.parametrize(
         "name,component_dirs,expected",
         [
@@ -279,27 +323,28 @@ class TestBirdNode:
         node = BirdNode(name=name, attrs=[], nodelist=None)
 
         with override_settings(DJANGO_BIRD={"COMPONENT_DIRS": component_dirs}):
-            template_names = node.get_template_names()
+            template_names = node.get_template_names(node.name)
 
         assert template_names == expected
 
     def test_get_template_names_invalid(self):
         node = BirdNode(name="input.label", attrs=[], nodelist=None)
 
-        template_names = node.get_template_names()
+        template_names = node.get_template_names(node.name)
 
         assert "bird/input/label/invalid.html" not in template_names
 
     def test_get_template_names_duplicates(self):
         with override_settings(DJANGO_BIRD={"COMPONENT_DIRS": ["bird"]}):
             node = BirdNode(name="button", attrs=[], nodelist=None)
-            template_names = node.get_template_names()
+
+            template_names = node.get_template_names(node.name)
 
             template_counts = {}
             for template in template_names:
                 template_counts[template] = template_counts.get(template, 0) + 1
 
-            for template, count in template_counts.items():
+            for _, count in template_counts.items():
                 assert count == 1