Repository: EmilStenstrom/django-components

Issue:
Unexpected behaviour of `ComponentDependencyMiddleware` in combination with `as_view` usage
Hi! First I want to thank you for the great library.

I run into an issue which appears in combination of different features inside library. Issue appears if one uses `ComponentDependencyMiddleware` + `RENDER_DEPENDENCIES` setting + `as_view` usage of the component. 

Error provided: *The component "ClassName" is not registered* at django_components/component_registry.py, line 245

## How to reproduce

1. Configure `django_components` with dependencies middleware following default documentation
2. Create a simple component (doesn't matter which one)
3. Configure `RENDER_DEPENDENCIES: True`
4. Create any location which will be using given component
5. Try to open location - you will receive an exception *The component "MagicComponent" is not registered*

I created a simple repository with demo application illustrating combination of settings: https://github.com/Indomian/django_component_as_view_error

## Analysis

I belive problem is located somewhere in the combination of logic that is located here: https://github.com/EmilStenstrom/django-components/blob/b7e913a86087aee6f67d15805aca4a4252680e37/src/django_components/component.py#L673.
In general when component is rendered from `as_view` call it doesn't have any provided name, so value of `self.name` would be equal to the class name and this will lead to rendering of placeholder with name of the component which was not registered before. See https://github.com/EmilStenstrom/django-components/blob/b7e913a86087aee6f67d15805aca4a4252680e37/src/django_components/middleware.py#L70, here components registry will throw an exception that component with given class name is not registered (which is correct, due to the fact that is was registered under another name).

## How to approach to fix

I tried to check if it is possible to provide registred name to the `as_view` call, but right now it doesn't pass through any arguments. The only solution I came up with is:

```python
from django.urls import path
from django_components import ComponentView

from components.test_component.component import MagicComponent

component = MagicComponent(
    registered_name="test_component"
)


urlpatterns = [
    path(
        'magic/',
        ComponentView.as_view(
            component=component
        ),
        name="magic_component_name"
    )
]
```

But currently I'm not sure about downsides of this solution :). Also, I think I can create separate class with a component view which I will use later, but it would be definitely confusing.

Discussion:
@JuroOravec: Hey @Indomian, thanks for the report, you're right with the analysis. The middleware will undergo a revamp in a week or two, which will refactor the placeholders logic. But the component-name-passing behavior will still be there, so we can fix it before then.

So right now we insert the component's registered name with the placeholder into the rendered HTML, so that the rendered output can be passed around, so we can then extract the information inside the middleware. 

As for the bug, currently we pass around the registered component name, and then look up the corresponding component class. 

I think that, instead of component's registered name, we could pass around components import path (e.g. `myapp.components.mycomp.MyComp`). That way, we would also make the component lookup logic work across different ComponentRegistry instances.@JuroOravec: Alright, the actual fix is a bit more involved, so it will be easier to merge it with the rest of the refactor.

In the meantime, as part of v0.100 release, there will be also [this refactor](https://github.com/EmilStenstrom/django-components/pull/664), which will pick up the registered name when you call `as_view()` on an component INSTANCE. So you will be able to do this:

```py
MagicComponent(
    registered_name="test_component"
).as_view()
```@Indomian: Nice and easy, thank you! It still overhead for development, but will do the job.@JuroOravec: @Indomian I believe this should be fix in [v0.110](https://github.com/EmilStenstrom/django-components/releases/tag/0.110) and later - In those versions we no longer use the component's name, but instead the components import path (e.g. `path.to.MyComponent`), which is independent of the component name.  v0.110 introduced some bugs, so I recommend to update at least to [v0.117](https://github.com/EmilStenstrom/django-components/releases/tag/0.117), and ideally to the latest release 🎉 Happy holidays :) 🎄 

Please reopen this issue if you find that there's still something wrong even after updating.

Solution patch:
diff --git a/README.md b/README.md
index b958f67d1..f79f72486 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
     - [See here how to migrate your settings.py](https://github.com/EmilStenstrom/django-components/blob/master/docs/migrating_from_safer_staticfiles.md)
 - Beside the top-level `/components` directory, you can now define also app-level components dirs, e.g. `[app]/components`
   (See [`COMPONENTS.app_dirs`](#app_dirs)).
+- When you call `as_view()` on a component instance, that instance will be passed to `View.as_view()`
 
 **Version 0.97**
 - Fixed template caching. You can now also manually create cached templates with [`cached_template()`](#template_cache_size---tune-the-template-cache)
diff --git a/src/django_components/component.py b/src/django_components/component.py
index 3986546d9..a2deb82f3 100644
--- a/src/django_components/component.py
+++ b/src/django_components/component.py
@@ -252,6 +252,7 @@ def __init__(
         # See https://stackoverflow.com/a/76706399/9788634
         self.render_to_response = types.MethodType(self.__class__.render_to_response.__func__, self)  # type: ignore
         self.render = types.MethodType(self.__class__.render.__func__, self)  # type: ignore
+        self.as_view = types.MethodType(self.__class__.as_view.__func__, self)  # type: ignore
 
         self.registered_name: Optional[str] = registered_name
         self.outer_context: Context = outer_context or Context()
@@ -439,9 +440,15 @@ def as_view(cls, **initkwargs: Any) -> ViewFn:
         """
         Shortcut for calling `Component.View.as_view` and passing component instance to it.
         """
+        # This method may be called as class method or as instance method.
+        # If called as class method, create a new instance.
+        if isinstance(cls, Component):
+            comp: Component = cls
+        else:
+            comp = cls()
+
         # Allow the View class to access this component via `self.component`
-        component = cls()
-        return component.View.as_view(**initkwargs, component=component)
+        return comp.View.as_view(**initkwargs, component=comp)
 
     @classmethod
     def render_to_response(


Test patch:
diff --git a/tests/test_component_as_view.py b/tests/test_component_as_view.py
index eea2ca942..3307543da 100644
--- a/tests/test_component_as_view.py
+++ b/tests/test_component_as_view.py
@@ -160,6 +160,28 @@ def post(self, request, *args, **kwargs) -> HttpResponse:
             response.content,
         )
 
+    def test_instantiate_component(self):
+        class MockComponentRequest(Component):
+            template = """
+                <form method="post">
+                    <input type="text" name="variable" value="{{ inner_var }}">
+                </form>
+                """
+
+            def get_context_data(self, variable):
+                return {"inner_var": variable}
+
+            def get(self, request, *args, **kwargs) -> HttpResponse:
+                return self.render_to_response(kwargs={"variable": self.name})
+
+        client = CustomClient(urlpatterns=[path("test/", MockComponentRequest("my_comp").as_view())])
+        response = client.get("/test/")
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            b'<input type="text" name="variable" value="my_comp">',
+            response.content,
+        )
+
     @parametrize_context_behavior(["django", "isolated"])
     def test_replace_slot_in_view(self):
         class MockComponentSlot(Component):