diff --git a/src/django_components/component.py b/src/django_components/component.py index 6e68bb81..c9de848b 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -228,6 +228,20 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass): self.component_id = component_id or gen_id() self._context: Optional[Context] = None + # When user first instantiates the component class before calling + # `render` or `render_to_response`, then we want to allow the render + # function to make use of the instantiated object. + # + # So while `MyComp.render()` creates a new instance of MyComp internally, + # if we do `MyComp(registered_name="abc").render()`, then we use the + # already-instantiated object. + # + # To achieve that, we want to re-assign the class methods as instance methods. + # For that we have to "unwrap" the class methods via __func__. + # 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 + def __init_subclass__(cls, **kwargs: Any) -> None: cls._class_hash = hash(inspect.getfile(cls) + cls.__name__) @@ -440,7 +454,13 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass): ) ``` """ - comp = cls() + # 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() + return comp._render(context, args, kwargs, slots, escape_slots_content) def _render( diff --git a/tests/test_component.py b/tests/test_component.py index a92ff656..be4429f3 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -477,3 +477,35 @@ class ComponentRenderTest(BaseTestCase): """, ) + + @parametrize_context_behavior(["django", "isolated"]) + def test_render_can_access_instance(self): + class TestComponent(component.Component): + template = "Variable: {{ id }}" + + def get_context_data(self, **attrs): + return { + "id": self.component_id, + } + + rendered = TestComponent(component_id="123").render() + self.assertHTMLEqual( + rendered, + "Variable: 123", + ) + + @parametrize_context_behavior(["django", "isolated"]) + def test_render_to_response_can_access_instance(self): + class TestComponent(component.Component): + template = "Variable: {{ id }}" + + def get_context_data(self, **attrs): + return { + "id": self.component_id, + } + + rendered_resp = TestComponent(component_id="123").render_to_response() + self.assertHTMLEqual( + rendered_resp.content.decode('utf-8'), + "Variable: 123", + )