From 2b7f60062d401af28705d6f146c692d2ecd9d6d8 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:29:02 -0700 Subject: [PATCH 01/21] Pre-register view_to_component URLs --- CHANGELOG.md | 9 ++++++++- src/django_idom/__init__.py | 2 +- src/django_idom/components.py | 35 +++++++++++++++++------------------ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 894a1390..c57df31a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ Using the following categories, list your changes in this order: - Nothing (Yet) +## [1.2.1] - 2022-09-21 + +### Fixed + +- URLs are now pre-registered when using `view_to_component` with `compatibility=True`. + ## [1.2.0] - 2022-09-19 ### Added @@ -130,7 +136,8 @@ Using the following categories, list your changes in this order: - Support for IDOM within the Django -[unreleased]: https://github.com/idom-team/django-idom/compare/1.2.0...HEAD +[unreleased]: https://github.com/idom-team/django-idom/compare/1.2.1...HEAD +[1.2.0]: https://github.com/idom-team/django-idom/compare/1.2.0...1.2.1 [1.2.0]: https://github.com/idom-team/django-idom/compare/1.1.0...1.2.0 [1.1.0]: https://github.com/idom-team/django-idom/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/idom-team/django-idom/compare/0.0.5...1.0.0 diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 998ab5a6..2a4fa2f4 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -3,7 +3,7 @@ from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH -__version__ = "1.2.0" +__version__ = "1.2.1" __all__ = [ "IDOM_WEBSOCKET_PATH", "IdomWebsocket", diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 34ce359c..081e2e59 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -47,11 +47,17 @@ def view_to_component( kwargs: The keyword arguments to pass to the view. """ kwargs = kwargs or {} - rendered_view, set_rendered_view = hooks.use_state(None) + converted_view, set_converted_view = hooks.use_state(None) request_obj = request if not request: request_obj = HttpRequest() request_obj.method = "GET" + if compatibility: + dotted_path = f"{view.__module__}.{view.__name__}" + dotted_path = dotted_path.replace("<", "").replace(">", "") + IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( + view, args, kwargs + ) # Render the view render within a hook @hooks.use_effect( @@ -64,14 +70,7 @@ async def async_renderer(): """Render the view in an async hook to avoid blocking the main thread.""" # Render Check 1: Compatibility mode if compatibility: - dotted_path = f"{view.__module__}.{view.__name__}" - dotted_path = dotted_path.replace("<", "").replace(">", "") - IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( - view, args, kwargs - ) - - # Signal that the view has been rendered - set_rendered_view( + set_converted_view( html.iframe( { "src": reverse("idom:view_to_component", args=[dotted_path]), @@ -83,40 +82,40 @@ async def async_renderer(): # Render Check 2: Async function view elif iscoroutinefunction(view): - render = await view(request_obj, *args, **kwargs) + view_html = await view(request_obj, *args, **kwargs) # Render Check 3: Async class view elif getattr(view, "view_is_async", False): view_or_template_view = await view.as_view()(request_obj, *args, **kwargs) if getattr(view_or_template_view, "render", None): # TemplateView - render = await view_or_template_view.render() + view_html = await view_or_template_view.render() else: # View - render = view_or_template_view + view_html = view_or_template_view # Render Check 4: Sync class view elif getattr(view, "as_view", None): async_cbv = database_sync_to_async(view.as_view()) view_or_template_view = await async_cbv(request_obj, *args, **kwargs) if getattr(view_or_template_view, "render", None): # TemplateView - render = await database_sync_to_async(view_or_template_view.render)() + view_html = await database_sync_to_async(view_or_template_view.render)() else: # View - render = view_or_template_view + view_html = view_or_template_view # Render Check 5: Sync function view else: - render = await database_sync_to_async(view)(request_obj, *args, **kwargs) + view_html = await database_sync_to_async(view)(request_obj, *args, **kwargs) # Signal that the view has been rendered - set_rendered_view( + set_converted_view( utils.html_to_vdom( - render.content.decode("utf-8").strip(), + view_html.content.decode("utf-8").strip(), *transforms, strict=strict_parsing, ) ) # Return the view if it's been rendered via the `async_renderer` hook - return rendered_view + return converted_view @component From 3d1779d5683d477aec3939823145500137d978ec Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 21 Sep 2022 18:02:55 -0700 Subject: [PATCH 02/21] top level `view_to_component` is now a regular function --- CHANGELOG.md | 1 + src/django_idom/components.py | 148 +++++++++++++++++++++------------- 2 files changed, 91 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c57df31a..d32214f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Using the following categories, list your changes in this order: ### Fixed - URLs are now pre-registered when using `view_to_component` with `compatibility=True`. +- `view_to_component` type hints will now display like normal utility functions. ## [1.2.0] - 2022-09-19 diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 081e2e59..14f836ab 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -19,7 +19,6 @@ # TODO: Might want to intercept href clicks and form submit events. # Form events will probably be accomplished through the upcoming DjangoForm. -@component def view_to_component( view: Callable | View, compatibility: bool = False, @@ -47,75 +46,104 @@ def view_to_component( kwargs: The keyword arguments to pass to the view. """ kwargs = kwargs or {} - converted_view, set_converted_view = hooks.use_state(None) request_obj = request if not request: request_obj = HttpRequest() request_obj.method = "GET" if compatibility: - dotted_path = f"{view.__module__}.{view.__name__}" + dotted_path = _generate_obj_name(view, raises=True) dotted_path = dotted_path.replace("<", "").replace(">", "") IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( view, args, kwargs ) - # Render the view render within a hook - @hooks.use_effect( - dependencies=[ - json.dumps(vars(request_obj), default=lambda x: _generate_obj_name(x)), - json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)), - ] - ) - async def async_renderer(): - """Render the view in an async hook to avoid blocking the main thread.""" - # Render Check 1: Compatibility mode - if compatibility: + @component + def new_component( + view: Callable | View, + compatibility: bool = False, + transforms: Iterable[Callable[[VdomDict], Any]] = (), + strict_parsing: bool = True, + request: HttpRequest | None = None, + args: Iterable = (), + kwargs: Dict | None = None, + ): + converted_view, set_converted_view = hooks.use_state(None) + + # Render the view render within a hook + @hooks.use_effect( + dependencies=[ + json.dumps(vars(request_obj), default=lambda x: _generate_obj_name(x)), + json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)), + ] + ) + async def async_renderer(): + """Render the view in an async hook to avoid blocking the main thread.""" + # Render Check 1: Compatibility mode + if compatibility: + set_converted_view( + html.iframe( + { + "src": reverse( + "idom:view_to_component", args=[dotted_path] + ), + "loading": "lazy", + } + ) + ) + return + + # Render Check 2: Async function view + elif iscoroutinefunction(view): + view_html = await view(request_obj, *args, **kwargs) + + # Render Check 3: Async class view + elif getattr(view, "view_is_async", False): + view_or_template_view = await view.as_view()( + request_obj, *args, **kwargs + ) + if getattr(view_or_template_view, "render", None): # TemplateView + view_html = await view_or_template_view.render() + else: # View + view_html = view_or_template_view + + # Render Check 4: Sync class view + elif getattr(view, "as_view", None): + async_cbv = database_sync_to_async(view.as_view()) + view_or_template_view = await async_cbv(request_obj, *args, **kwargs) + if getattr(view_or_template_view, "render", None): # TemplateView + view_html = await database_sync_to_async( + view_or_template_view.render + )() + else: # View + view_html = view_or_template_view + + # Render Check 5: Sync function view + else: + view_html = await database_sync_to_async(view)( + request_obj, *args, **kwargs + ) + + # Signal that the view has been rendered set_converted_view( - html.iframe( - { - "src": reverse("idom:view_to_component", args=[dotted_path]), - "loading": "lazy", - } + utils.html_to_vdom( + view_html.content.decode("utf-8").strip(), + *transforms, + strict=strict_parsing, ) ) - return - - # Render Check 2: Async function view - elif iscoroutinefunction(view): - view_html = await view(request_obj, *args, **kwargs) - - # Render Check 3: Async class view - elif getattr(view, "view_is_async", False): - view_or_template_view = await view.as_view()(request_obj, *args, **kwargs) - if getattr(view_or_template_view, "render", None): # TemplateView - view_html = await view_or_template_view.render() - else: # View - view_html = view_or_template_view - - # Render Check 4: Sync class view - elif getattr(view, "as_view", None): - async_cbv = database_sync_to_async(view.as_view()) - view_or_template_view = await async_cbv(request_obj, *args, **kwargs) - if getattr(view_or_template_view, "render", None): # TemplateView - view_html = await database_sync_to_async(view_or_template_view.render)() - else: # View - view_html = view_or_template_view - - # Render Check 5: Sync function view - else: - view_html = await database_sync_to_async(view)(request_obj, *args, **kwargs) - - # Signal that the view has been rendered - set_converted_view( - utils.html_to_vdom( - view_html.content.decode("utf-8").strip(), - *transforms, - strict=strict_parsing, - ) - ) - # Return the view if it's been rendered via the `async_renderer` hook - return converted_view + # Return the view if it's been rendered via the `async_renderer` hook + return converted_view + + return new_component( + view=view, + compatibility=compatibility, + transforms=transforms, + strict_parsing=strict_parsing, + request=request, + args=args, + kwargs=kwargs, + ) @component @@ -164,7 +192,7 @@ def _cached_static_contents(static_path: str): return file_contents -def _generate_obj_name(object: Any) -> str | None: +def _generate_obj_name(object: Any, raises: bool = False) -> str | None: """Makes a best effort to create a name for an object. Useful for JSON serialization of Python objects.""" if hasattr(object, "__module__"): @@ -172,4 +200,8 @@ def _generate_obj_name(object: Any) -> str | None: return f"{object.__module__}.{object.__name__}" if hasattr(object, "__class__"): return f"{object.__module__}.{object.__class__.__name__}" - return None + + if not raises: + return None + + raise (TypeError(f"Could not generate name for object {object}")) From cc39d942d671cc41d3bed8b96a50b3e7e5ad625a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 21 Sep 2022 18:08:17 -0700 Subject: [PATCH 03/21] mypy --- src/django_idom/components.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 14f836ab..8aff424d 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -51,7 +51,7 @@ def view_to_component( request_obj = HttpRequest() request_obj.method = "GET" if compatibility: - dotted_path = _generate_obj_name(view, raises=True) + dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore[union-attr] dotted_path = dotted_path.replace("<", "").replace(">", "") IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( view, args, kwargs @@ -192,7 +192,7 @@ def _cached_static_contents(static_path: str): return file_contents -def _generate_obj_name(object: Any, raises: bool = False) -> str | None: +def _generate_obj_name(object: Any) -> str | None: """Makes a best effort to create a name for an object. Useful for JSON serialization of Python objects.""" if hasattr(object, "__module__"): @@ -201,7 +201,4 @@ def _generate_obj_name(object: Any, raises: bool = False) -> str | None: if hasattr(object, "__class__"): return f"{object.__module__}.{object.__class__.__name__}" - if not raises: - return None - - raise (TypeError(f"Could not generate name for object {object}")) + return None From bea145bb055347b50f6e6eccb591d1555f5bc026 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 21 Sep 2022 18:40:03 -0700 Subject: [PATCH 04/21] mypy again --- src/django_idom/components.py | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 8aff424d..b1b71ce8 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -11,6 +11,7 @@ from django.urls import reverse from django.views import View from idom import component, hooks, html, utils +from idom.core.component import Component from idom.types import VdomDict from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES @@ -27,7 +28,7 @@ def view_to_component( request: HttpRequest | None = None, args: Iterable = (), kwargs: Dict | None = None, -) -> VdomDict | None: +) -> Component: """Converts a Django view to an IDOM component. Args: @@ -58,15 +59,7 @@ def view_to_component( ) @component - def new_component( - view: Callable | View, - compatibility: bool = False, - transforms: Iterable[Callable[[VdomDict], Any]] = (), - strict_parsing: bool = True, - request: HttpRequest | None = None, - args: Iterable = (), - kwargs: Dict | None = None, - ): + def new_component(): converted_view, set_converted_view = hooks.use_state(None) # Render the view render within a hook @@ -135,15 +128,7 @@ async def async_renderer(): # Return the view if it's been rendered via the `async_renderer` hook return converted_view - return new_component( - view=view, - compatibility=compatibility, - transforms=transforms, - strict_parsing=strict_parsing, - request=request, - args=args, - kwargs=kwargs, - ) + return new_component() @component From ba71dd46e415ac2d13bb3855d280965d93e470a9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 21 Sep 2022 19:00:21 -0700 Subject: [PATCH 05/21] add tests --- tests/test_app/tests/test_components.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index cf6b153e..75f8350a 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -5,6 +5,9 @@ from channels.testing import ChannelsLiveServerTestCase from playwright.sync_api import TimeoutError, sync_playwright +from django_idom.components import view_to_component +from django_idom.config import IDOM_VIEW_COMPONENT_IFRAMES + class TestIdomCapabilities(ChannelsLiveServerTestCase): @classmethod @@ -176,3 +179,9 @@ def test_view_to_component_template_view_class_compatibility(self): ).locator( "#ViewToComponentTemplateViewClassCompatibility[data-success=true]" ).wait_for() + + def test_view_to_component_iframe_registration(self): + view_to_component(lambda x: None, compatibility=True) + self.assertIn( + "test_app.tests.test_components.lambda", IDOM_VIEW_COMPONENT_IFRAMES + ) From 253652ac58c99eeb41cef74f37b5e505e83eb966 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 21 Sep 2022 19:01:27 -0700 Subject: [PATCH 06/21] fix changelog version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d32214f6..158c2c54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -138,7 +138,7 @@ Using the following categories, list your changes in this order: - Support for IDOM within the Django [unreleased]: https://github.com/idom-team/django-idom/compare/1.2.1...HEAD -[1.2.0]: https://github.com/idom-team/django-idom/compare/1.2.0...1.2.1 +[1.2.1]: https://github.com/idom-team/django-idom/compare/1.2.0...1.2.1 [1.2.0]: https://github.com/idom-team/django-idom/compare/1.1.0...1.2.0 [1.1.0]: https://github.com/idom-team/django-idom/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/idom-team/django-idom/compare/0.0.5...1.0.0 From 37372e425b56f66286f5b3e9ae55beac07a40303 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 23 Sep 2022 02:03:24 -0700 Subject: [PATCH 07/21] fix spacing --- src/django_idom/components.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index b1b71ce8..8658a1b4 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -185,5 +185,4 @@ def _generate_obj_name(object: Any) -> str | None: return f"{object.__module__}.{object.__name__}" if hasattr(object, "__class__"): return f"{object.__module__}.{object.__class__.__name__}" - return None From 293dc018a8b63fa2967cfd59680c8f62dbdea5ca Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 23 Sep 2022 02:04:02 -0700 Subject: [PATCH 08/21] clean up changelog verbiage --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 158c2c54..f39aef1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ Using the following categories, list your changes in this order: ### Fixed - URLs are now pre-registered when using `view_to_component` with `compatibility=True`. -- `view_to_component` type hints will now display like normal utility functions. +- `view_to_component` type hints will now display like normal functions. ## [1.2.0] - 2022-09-19 From 78372d276b3b3ce3ec5b20133ed94bb9cda985a4 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 23 Sep 2022 02:06:52 -0700 Subject: [PATCH 09/21] fix type hints for django_css and django_js --- CHANGELOG.md | 2 +- src/django_idom/components.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f39aef1a..29ae61c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ Using the following categories, list your changes in this order: ### Fixed - URLs are now pre-registered when using `view_to_component` with `compatibility=True`. -- `view_to_component` type hints will now display like normal functions. +- `view_to_component`, `django_css`, and `django_js` type hints will now display like normal functions. ## [1.2.0] - 2022-09-19 diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 8658a1b4..d1ce0380 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -131,7 +131,6 @@ async def async_renderer(): return new_component() -@component def django_css(static_path: str): """Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading. @@ -139,10 +138,14 @@ def django_css(static_path: str): static_path: The path to the static file. This path is identical to what you would use on a `static` template tag. """ - return html.style(_cached_static_contents(static_path)) + + @component + def new_component(): + return html.style(_cached_static_contents(static_path)) + + return new_component() -@component def django_js(static_path: str): """Fetches a JS static file for use within IDOM. This allows for deferred JS loading. @@ -150,7 +153,12 @@ def django_js(static_path: str): static_path: The path to the static file. This path is identical to what you would use on a `static` template tag. """ - return html.script(_cached_static_contents(static_path)) + + @component + def new_component(): + return html.script(_cached_static_contents(static_path)) + + return new_component() def _cached_static_contents(static_path: str): From 655a6153a8c7b2df7e554e7243abc8d81e594e0d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:55:01 -0700 Subject: [PATCH 10/21] consistent component identity --- src/django_idom/components.py | 173 ++++++++++++++++++---------------- 1 file changed, 94 insertions(+), 79 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index d1ce0380..e63e34f7 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -18,6 +18,78 @@ from django_idom.types import ViewComponentIframe +@component +def _view_to_component( + view: Callable | View, + compatibility: bool = False, + transforms: Iterable[Callable[[VdomDict], Any]] = (), + strict_parsing: bool = True, + request: HttpRequest | None = None, + args: Iterable = (), + kwargs: Dict | None = None, + dotted_path: str | None = None, +): + converted_view, set_converted_view = hooks.use_state(None) + + # Render the view render within a hook + @hooks.use_effect( + dependencies=[ + json.dumps(vars(request), default=lambda x: _generate_obj_name(x)), + json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)), + ] + ) + async def async_renderer(): + """Render the view in an async hook to avoid blocking the main thread.""" + # Render Check 1: Compatibility mode + if compatibility: + set_converted_view( + html.iframe( + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } + ) + ) + return + + # Render Check 2: Async function view + elif iscoroutinefunction(view): + view_html = await view(request, *args, **kwargs) + + # Render Check 3: Async class view + elif getattr(view, "view_is_async", False): + view_or_template_view = await view.as_view()(request, *args, **kwargs) + if getattr(view_or_template_view, "render", None): # TemplateView + view_html = await view_or_template_view.render() + else: # View + view_html = view_or_template_view + + # Render Check 4: Sync class view + elif getattr(view, "as_view", None): + async_cbv = database_sync_to_async(view.as_view()) + view_or_template_view = await async_cbv(request, *args, **kwargs) + if getattr(view_or_template_view, "render", None): # TemplateView + view_html = await database_sync_to_async(view_or_template_view.render)() + else: # View + view_html = view_or_template_view + + # Render Check 5: Sync function view + else: + view_html = await database_sync_to_async(view)(request, *args, **kwargs) + + # Signal that the view has been rendered + set_converted_view( + utils.html_to_vdom( + view_html.content.decode("utf-8").strip(), + *transforms, + strict=strict_parsing, + ) + ) + + # Return the view if it's been rendered via the `async_renderer` hook + return converted_view + + # TODO: Might want to intercept href clicks and form submit events. # Form events will probably be accomplished through the upcoming DjangoForm. def view_to_component( @@ -48,7 +120,7 @@ def view_to_component( """ kwargs = kwargs or {} request_obj = request - if not request: + if not request_obj: request_obj = HttpRequest() request_obj.method = "GET" if compatibility: @@ -57,78 +129,24 @@ def view_to_component( IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( view, args, kwargs ) + else: + dotted_path = None - @component - def new_component(): - converted_view, set_converted_view = hooks.use_state(None) - - # Render the view render within a hook - @hooks.use_effect( - dependencies=[ - json.dumps(vars(request_obj), default=lambda x: _generate_obj_name(x)), - json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)), - ] - ) - async def async_renderer(): - """Render the view in an async hook to avoid blocking the main thread.""" - # Render Check 1: Compatibility mode - if compatibility: - set_converted_view( - html.iframe( - { - "src": reverse( - "idom:view_to_component", args=[dotted_path] - ), - "loading": "lazy", - } - ) - ) - return - - # Render Check 2: Async function view - elif iscoroutinefunction(view): - view_html = await view(request_obj, *args, **kwargs) - - # Render Check 3: Async class view - elif getattr(view, "view_is_async", False): - view_or_template_view = await view.as_view()( - request_obj, *args, **kwargs - ) - if getattr(view_or_template_view, "render", None): # TemplateView - view_html = await view_or_template_view.render() - else: # View - view_html = view_or_template_view - - # Render Check 4: Sync class view - elif getattr(view, "as_view", None): - async_cbv = database_sync_to_async(view.as_view()) - view_or_template_view = await async_cbv(request_obj, *args, **kwargs) - if getattr(view_or_template_view, "render", None): # TemplateView - view_html = await database_sync_to_async( - view_or_template_view.render - )() - else: # View - view_html = view_or_template_view - - # Render Check 5: Sync function view - else: - view_html = await database_sync_to_async(view)( - request_obj, *args, **kwargs - ) - - # Signal that the view has been rendered - set_converted_view( - utils.html_to_vdom( - view_html.content.decode("utf-8").strip(), - *transforms, - strict=strict_parsing, - ) - ) + return _view_to_component( + view=view, + compatibility=compatibility, + transforms=transforms, + strict_parsing=strict_parsing, + request=request_obj, + args=args, + kwargs=kwargs, + dotted_path=dotted_path, + ) - # Return the view if it's been rendered via the `async_renderer` hook - return converted_view - return new_component() +@component +def _django_css(static_path: str): + return html.style(_cached_static_contents(static_path)) def django_css(static_path: str): @@ -139,11 +157,12 @@ def django_css(static_path: str): use on a `static` template tag. """ - @component - def new_component(): - return html.style(_cached_static_contents(static_path)) + return _django_css(static_path=static_path) + - return new_component() +@component +def _django_js(static_path: str): + return html.script(_cached_static_contents(static_path)) def django_js(static_path: str): @@ -154,11 +173,7 @@ def django_js(static_path: str): use on a `static` template tag. """ - @component - def new_component(): - return html.script(_cached_static_contents(static_path)) - - return new_component() + return _django_js(static_path=static_path) def _cached_static_contents(static_path: str): From 8719dd9d19fcb909281f1c2975c927dccafcbff5 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 2 Oct 2022 17:02:21 -0700 Subject: [PATCH 11/21] async_renderer -> async_render --- src/django_idom/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index e63e34f7..53d96659 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -38,7 +38,7 @@ def _view_to_component( json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)), ] ) - async def async_renderer(): + async def async_render(): """Render the view in an async hook to avoid blocking the main thread.""" # Render Check 1: Compatibility mode if compatibility: @@ -86,7 +86,7 @@ async def async_renderer(): ) ) - # Return the view if it's been rendered via the `async_renderer` hook + # Return the view if it's been rendered via the `async_render` hook return converted_view From 89f4033c342a1a489a98c67834fc44ca30b7a71c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 2 Oct 2022 18:09:47 -0700 Subject: [PATCH 12/21] add vtc limitations --- docs/src/features/components.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 8d95c062..91e5241a 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -181,6 +181,16 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible return HttpResponse("
Hello World!
") ``` +??? warning "Limitations" + + There are currently several limitations of using `view_to_component` that may be resolved in a future version of `django_idom`. + + Please note these limitations do not exist when using `compatibility` mode. + + - Requires manual intervention to change request methods beyond `GET`. + - Does not currently load any HTML contained with a `` tag + - Has no option to automatically intercept local anchor link (ex. `#!html `) click events + ## Django CSS Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). From 798357648cb5b9e98d2340b9b0e90ad20d6798d5 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 12 Oct 2022 01:09:58 -0700 Subject: [PATCH 13/21] revert version --- CHANGELOG.md | 12 +++++------- src/django_idom/__init__.py | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ae61c0..91777546 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,15 +22,14 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (Yet) - -## [1.2.1] - 2022-09-21 - ### Fixed -- URLs are now pre-registered when using `view_to_component` with `compatibility=True`. - `view_to_component`, `django_css`, and `django_js` type hints will now display like normal functions. +### Changed + +- `view_to_component` now returns a `Callable`, instead of directly returning a `Component`. Check the docs for new usage info. + ## [1.2.0] - 2022-09-19 ### Added @@ -137,8 +136,7 @@ Using the following categories, list your changes in this order: - Support for IDOM within the Django -[unreleased]: https://github.com/idom-team/django-idom/compare/1.2.1...HEAD -[1.2.1]: https://github.com/idom-team/django-idom/compare/1.2.0...1.2.1 +[unreleased]: https://github.com/idom-team/django-idom/compare/1.2.0...HEAD [1.2.0]: https://github.com/idom-team/django-idom/compare/1.1.0...1.2.0 [1.1.0]: https://github.com/idom-team/django-idom/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/idom-team/django-idom/compare/0.0.5...1.0.0 diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 2a4fa2f4..998ab5a6 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -3,7 +3,7 @@ from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH -__version__ = "1.2.1" +__version__ = "1.2.0" __all__ = [ "IDOM_WEBSOCKET_PATH", "IdomWebsocket", From 5cda1da3a9335b46b43c09988268fabf40040bd8 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 00:21:25 -0700 Subject: [PATCH 14/21] New `view_to_component`, that can be used as a decorator. --- src/django_idom/components.py | 104 +++++++++++++----------- tests/test_app/components.py | 99 +++++++++++----------- tests/test_app/templates/base.html | 2 + tests/test_app/tests/test_components.py | 8 ++ 4 files changed, 119 insertions(+), 94 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 53d96659..acffaccd 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -3,7 +3,7 @@ import json import os from inspect import iscoroutinefunction -from typing import Any, Callable, Dict, Iterable +from typing import Any, Callable, Dict, Sequence from channels.db import database_sync_to_async from django.contrib.staticfiles.finders import find @@ -21,20 +21,33 @@ @component def _view_to_component( view: Callable | View, - compatibility: bool = False, - transforms: Iterable[Callable[[VdomDict], Any]] = (), - strict_parsing: bool = True, - request: HttpRequest | None = None, - args: Iterable = (), - kwargs: Dict | None = None, - dotted_path: str | None = None, + compatibility: bool, + transforms: Sequence[Callable[[VdomDict], Any]], + strict_parsing: bool, + request: HttpRequest | None, + args: Sequence | None, + kwargs: Dict | None, ): converted_view, set_converted_view = hooks.use_state(None) + args = args or () + kwargs = kwargs or {} + request_obj = request + if not request_obj: + request_obj = HttpRequest() + request_obj.method = "GET" + if compatibility: + dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore[union-attr] + dotted_path = dotted_path.replace("<", "").replace(">", "") + IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( + view, args, kwargs + ) + else: + dotted_path = None # Render the view render within a hook @hooks.use_effect( dependencies=[ - json.dumps(vars(request), default=lambda x: _generate_obj_name(x)), + json.dumps(vars(request_obj), default=lambda x: _generate_obj_name(x)), json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)), ] ) @@ -54,11 +67,11 @@ async def async_render(): # Render Check 2: Async function view elif iscoroutinefunction(view): - view_html = await view(request, *args, **kwargs) + view_html = await view(request_obj, *args, **kwargs) # Render Check 3: Async class view elif getattr(view, "view_is_async", False): - view_or_template_view = await view.as_view()(request, *args, **kwargs) + view_or_template_view = await view.as_view()(request_obj, *args, **kwargs) if getattr(view_or_template_view, "render", None): # TemplateView view_html = await view_or_template_view.render() else: # View @@ -67,7 +80,7 @@ async def async_render(): # Render Check 4: Sync class view elif getattr(view, "as_view", None): async_cbv = database_sync_to_async(view.as_view()) - view_or_template_view = await async_cbv(request, *args, **kwargs) + view_or_template_view = await async_cbv(request_obj, *args, **kwargs) if getattr(view_or_template_view, "render", None): # TemplateView view_html = await database_sync_to_async(view_or_template_view.render)() else: # View @@ -75,7 +88,7 @@ async def async_render(): # Render Check 5: Sync function view else: - view_html = await database_sync_to_async(view)(request, *args, **kwargs) + view_html = await database_sync_to_async(view)(request_obj, *args, **kwargs) # Signal that the view has been rendered set_converted_view( @@ -93,20 +106,15 @@ async def async_render(): # TODO: Might want to intercept href clicks and form submit events. # Form events will probably be accomplished through the upcoming DjangoForm. def view_to_component( - view: Callable | View, + view: Callable | View = None, # type: ignore[assignment] compatibility: bool = False, - transforms: Iterable[Callable[[VdomDict], Any]] = (), + transforms: Sequence[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, - request: HttpRequest | None = None, - args: Iterable = (), - kwargs: Dict | None = None, -) -> Component: +) -> Callable[[HttpRequest | None, Sequence | None, Dict | None], Component]: """Converts a Django view to an IDOM component. - Args: - view: The view function or class to convert. - Keyword Args: + view: The view function or class to convert. compatibility: If True, the component will be rendered in an iframe. When using compatibility mode `tranforms`, `strict_parsing`, and `request` arguments will be ignored. @@ -114,34 +122,34 @@ def view_to_component( The functions will be called on each VDOM node. strict_parsing: If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. - request: Request object to provide to the view. - args: The positional arguments to pass to the view. - kwargs: The keyword arguments to pass to the view. + + Returns: + Callable: A function that takes a request, args, and kwargs and returns an IDOM + component. """ - kwargs = kwargs or {} - request_obj = request - if not request_obj: - request_obj = HttpRequest() - request_obj.method = "GET" - if compatibility: - dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore[union-attr] - dotted_path = dotted_path.replace("<", "").replace(">", "") - IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( - view, args, kwargs - ) - else: - dotted_path = None - return _view_to_component( - view=view, - compatibility=compatibility, - transforms=transforms, - strict_parsing=strict_parsing, - request=request_obj, - args=args, - kwargs=kwargs, - dotted_path=dotted_path, - ) + def decorator(view: Callable | View): + if not view: + raise ValueError("A view must be provided to `view_to_component`") + + def wrapper( + request: HttpRequest | None = None, + args: Sequence | None = None, + kwargs: Dict | None = None, + ): + return _view_to_component( + view=view, + compatibility=compatibility, + transforms=transforms, + strict_parsing=strict_parsing, + request=request, + args=args, + kwargs=kwargs, + ) + + return wrapper + + return decorator(view) if view else decorator @component diff --git a/tests/test_app/components.py b/tests/test_app/components.py index c103e01c..5fa3a980 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -1,6 +1,7 @@ import inspect from django.http import HttpRequest +from django.shortcuts import render from idom import component, hooks, html, web from test_app.models import TodoItem @@ -236,38 +237,39 @@ def _render_items(items, toggle_item): ) -@component -def view_to_component_sync_func(): - return view_to_component(views.view_to_component_sync_func) - - -@component -def view_to_component_async_func(): - return view_to_component(views.view_to_component_async_func) - - -@component -def view_to_component_sync_class(): - return view_to_component(views.ViewToComponentSyncClass) - - -@component -def view_to_component_async_class(): - return view_to_component(views.ViewToComponentAsyncClass) - - -@component -def view_to_component_template_view_class(): - return view_to_component(views.ViewToComponentTemplateViewClass) +view_to_component_sync_func = view_to_component(views.view_to_component_sync_func) +view_to_component_async_func = view_to_component(views.view_to_component_async_func) +view_to_component_sync_class = view_to_component(views.ViewToComponentSyncClass) +view_to_component_async_class = view_to_component(views.ViewToComponentAsyncClass) +view_to_component_template_view_class = view_to_component( + views.ViewToComponentTemplateViewClass +) +_view_to_component_sync_func_compatibility = view_to_component( + views.view_to_component_sync_func_compatibility, compatibility=True +) +_view_to_component_async_func_compatibility = view_to_component( + views.view_to_component_async_func_compatibility, compatibility=True +) +_view_to_component_sync_class_compatibility = view_to_component( + views.ViewToComponentSyncClassCompatibility, compatibility=True +) +_view_to_component_async_class_compatibility = view_to_component( + views.ViewToComponentAsyncClassCompatibility, compatibility=True +) +_view_to_component_template_view_class_compatibility = view_to_component( + views.ViewToComponentTemplateViewClassCompatibility, compatibility=True +) +view_to_component_script = view_to_component(views.view_to_component_script) +_view_to_component_request = view_to_component(views.view_to_component_request) +_view_to_component_args = view_to_component(views.view_to_component_args) +_view_to_component_kwargs = view_to_component(views.view_to_component_kwargs) @component def view_to_component_sync_func_compatibility(): return html.div( {"id": inspect.currentframe().f_code.co_name}, - view_to_component( - views.view_to_component_sync_func_compatibility, compatibility=True - ), + _view_to_component_sync_func_compatibility(), html.hr(), ) @@ -276,9 +278,7 @@ def view_to_component_sync_func_compatibility(): def view_to_component_async_func_compatibility(): return html.div( {"id": inspect.currentframe().f_code.co_name}, - view_to_component( - views.view_to_component_async_func_compatibility, compatibility=True - ), + _view_to_component_async_func_compatibility(), html.hr(), ) @@ -287,9 +287,7 @@ def view_to_component_async_func_compatibility(): def view_to_component_sync_class_compatibility(): return html.div( {"id": inspect.currentframe().f_code.co_name}, - view_to_component( - views.ViewToComponentSyncClassCompatibility, compatibility=True - ), + _view_to_component_sync_class_compatibility(), html.hr(), ) @@ -298,9 +296,7 @@ def view_to_component_sync_class_compatibility(): def view_to_component_async_class_compatibility(): return html.div( {"id": inspect.currentframe().f_code.co_name}, - view_to_component( - views.ViewToComponentAsyncClassCompatibility, compatibility=True - ), + _view_to_component_async_class_compatibility(), html.hr(), ) @@ -309,18 +305,11 @@ def view_to_component_async_class_compatibility(): def view_to_component_template_view_class_compatibility(): return html.div( {"id": inspect.currentframe().f_code.co_name}, - view_to_component( - views.ViewToComponentTemplateViewClassCompatibility, compatibility=True - ), + _view_to_component_template_view_class_compatibility(), html.hr(), ) -@component -def view_to_component_script(): - return view_to_component(views.view_to_component_script) - - @component def view_to_component_request(): request, set_request = hooks.use_state(None) @@ -335,7 +324,7 @@ def on_click(_): {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, "Click me", ), - view_to_component(views.view_to_component_request, request=request), + _view_to_component_request(request=request), ) @@ -351,7 +340,7 @@ def on_click(_): {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, "Click me", ), - view_to_component(views.view_to_component_args, args=[params]), + _view_to_component_args(args=[params]), ) @@ -367,5 +356,23 @@ def on_click(_): {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, "Click me", ), - view_to_component(views.view_to_component_kwargs, kwargs={"success": params}), + _view_to_component_kwargs(kwargs={"success": params}), + ) + + +@view_to_component +def view_to_component_decorator(request): + return render( + request, + "view_to_component.html", + {"test_name": inspect.currentframe().f_code.co_name}, + ) + + +@view_to_component(strict_parsing=False) +def view_to_component_decorator_args(request): + return render( + request, + "view_to_component.html", + {"test_name": inspect.currentframe().f_code.co_name}, ) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index bea9893a..4b0dd93c 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -46,6 +46,8 @@

IDOM Test Page

{% component "test_app.components.view_to_component_sync_class_compatibility" %}
{% component "test_app.components.view_to_component_async_class_compatibility" %}
{% component "test_app.components.view_to_component_template_view_class_compatibility" %}
+
{% component "test_app.components.view_to_component_decorator" %}
+
{% component "test_app.components.view_to_component_decorator_args" %}
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 75f8350a..34a647ad 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -185,3 +185,11 @@ def test_view_to_component_iframe_registration(self): self.assertIn( "test_app.tests.test_components.lambda", IDOM_VIEW_COMPONENT_IFRAMES ) + + def test_view_to_component_decorator(self): + self.page.locator("#view_to_component_decorator[data-success=true]").wait_for() + + def test_view_to_component_decorator_args(self): + self.page.locator( + "#view_to_component_decorator_args[data-success=true]" + ).wait_for() From 31e815e6a8d74f38f3db174f5f7717130821c9ec Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 00:21:40 -0700 Subject: [PATCH 15/21] Iterable -> Sequence --- src/django_idom/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 1f4bd406..a13bbe8f 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Awaitable, Callable, Generic, Iterable, Optional, TypeVar, Union +from typing import Any, Awaitable, Callable, Generic, Optional, Sequence, TypeVar, Union from django.db.models.base import Model from django.db.models.query import QuerySet @@ -49,5 +49,5 @@ class Mutation(Generic[_Params]): @dataclass class ViewComponentIframe: view: View | Callable - args: Iterable + args: Sequence kwargs: dict From fc5c3f476316fd2392b73138c8da31cc6c98315a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 02:06:55 -0700 Subject: [PATCH 16/21] new docs for vtc --- docs/includes/examples.md | 23 ----- docs/src/features/components.md | 164 ++++++++++++++++++++------------ src/django_idom/components.py | 4 +- 3 files changed, 103 insertions(+), 88 deletions(-) diff --git a/docs/includes/examples.md b/docs/includes/examples.md index 623e60c3..e69de29b 100644 --- a/docs/includes/examples.md +++ b/docs/includes/examples.md @@ -1,23 +0,0 @@ - - -```python -from django.http import HttpResponse - -def hello_world_view(request, *args, **kwargs): - return HttpResponse("Hello World!") -``` - - - - - -```python -from django.http import HttpResponse -from django.views import View - -class HelloWorldView(View): - def get(self, request, *args, **kwargs): - return HttpResponse("Hello World!") -``` - - \ No newline at end of file diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 91e5241a..832e6023 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -6,20 +6,20 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible ```python from idom import component, html + from django.http import HttpResponse from django_idom.components import view_to_component - from .views import hello_world_view + + @view_to_component + def hello_world_view(request): + return HttpResponse("Hello World!") @component def my_component(): return html.div( - view_to_component(hello_world_view), + hello_world_view(), ) ``` -=== "views.py" - - {% include-markdown "../../includes/examples.md" start="" end="" %} - ??? example "See Interface" **Parameters** @@ -27,73 +27,128 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible | Name | Type | Description | Default | | --- | --- | --- | --- | | view | `Callable | View` | The view function or class to convert. | N/A | - | compatibility | `bool` | If True, the component will be rendered in an iframe. When using compatibility mode `tranforms`, `strict_parsing`, and `request` arguments will be ignored. | `False` | - | transforms | `Iterable[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `tuple` | + | compatibility | `bool` | If True, the component will be rendered in an iframe. When using compatibility mode `tranforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments will be ignored. | `False` | + | transforms | `Sequence[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `tuple` | | strict_parsing | `bool` | If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `True` | - | request | `HttpRequest | None` | Request object to provide to the view. | `None` | - | args | `Iterable` | The positional arguments to pass to the view. | `tuple` | - | kwargs | `Dict | None` | The keyword arguments to pass to the view. | `None` | **Returns** | Type | Description | | --- | --- | - | `Component` | An IDOM component. | - | `None` | No component render. | + | `Callable[[HttpRequest | None, Sequence | None, Dict | None], Component]` | A function that takes a `request=...`, `args=...`, and `kwargs=...` and returns an IDOM component. | + +??? warning "Existing limitations" + + There are currently several limitations of using `view_to_component` that may be resolved in a future version of `django_idom`. + + - Requires manual intervention to change request methods beyond `GET`. + - Does not currently load any HTML contained with a `` tag + - Has no option to automatically intercept local anchor link (ex. `#!html `) click events + + _Please note these limitations do not exist when using `compatibility` mode._ ??? question "How do I use this for Class Based Views?" - You can simply pass your Class Based View directly into this function. + You can simply pass your Class Based View directly into `view_to_component`. === "components.py" ```python from idom import component, html + from django.http import HttpResponse + from django.views import View from django_idom.components import view_to_component - from .views import HelloWorldView + + @view_to_component + class HelloWorldView(View): + def get(self, request): + return HttpResponse("Hello World!") @component def my_component(): return html.div( - view_to_component(HelloWorldView), + HelloWorldView(), ) ``` - === "views.py" +??? question "How do I transform views from external libraries?" + + === "components.py" + + In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator. + + ```python + from idom import component, html + from django.http import HttpResponse + from django_idom.components import view_to_component + from some_library.views import example_view + + converted_view = view_to_component(example_view) - {% include-markdown "../../includes/examples.md" start="" end="" %} + @component + def my_component(): + return html.div( + converted_view(), + ) + ``` -??? question "How do I pass arguments into the view?" +??? question "How do I provide `args` and `kwargs` to a view?" - You can use the `args` and `kwargs` parameters to pass arguments to the view. + You can use the `args` and `kwargs` parameters to provide positional and keyworded arguments to a view. === "components.py" ```python from idom import component, html + from django.http import HttpResponse from django_idom.components import view_to_component - from .views import hello_world_view + + @view_to_component + def hello_world_view(request, arg1, arg2, key1=None, key2=None): + return HttpResponse(f"Hello World! {arg1} {arg2} {key1} {key2}") @component def my_component(): return html.div( - view_to_component( - hello_world_view, + hello_world_view( args=["value_1", "value_2"], - kwargs={"key_1": "value_1", "key_2": "value_2"}, + kwargs={"key1": "abc", "key2": "123"}, ), ) ``` - === "views.py" +??? question "How do I provide a custom `request` object to a view?" + + You can use the `request` parameter to provide the view a custom request object. + + === "components.py" + + ```python + from idom import component, html + from django.http import HttpResponse, HttpRequest + from django_idom.components import view_to_component + + example_request = HttpRequest() + example_request.method = "PUT" - {% include-markdown "../../includes/examples.md" start="" end="" %} + @view_to_component + def hello_world_view(request): + return HttpResponse(f"Hello World! {request.method}") + + @component + def my_component(): + return html.div( + hello_world_view( + request=example_request, + ), + ) + ``` ??? question "What is `compatibility` mode?" For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe. - Any view can be rendered within compatibility mode. However, the `transforms`, `strict_parsing`, and `request` arguments do not apply to compatibility mode. + Any view can be rendered within compatibility mode. However, the `transforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments do not apply to compatibility mode. Please note that by default the iframe is unstyled, and thus won't look pretty until you add some CSS. @@ -101,20 +156,20 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible ```python from idom import component, html + from django.http import HttpResponse from django_idom.components import view_to_component - from .views import hello_world_view + + @view_to_component(compatibility=True) + def hello_world_view(request): + return HttpResponse("Hello World!") @component def my_component(): return html.div( - view_to_component(hello_world_view, compatibility=True), + hello_world_view(), ) ``` - === "views.py" - - {% include-markdown "../../includes/examples.md" start="" end="" %} - ??? question "What is `strict_parsing`?" By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5. @@ -127,20 +182,20 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible ```python from idom import component, html + from django.http import HttpResponse from django_idom.components import view_to_component - from .views import hello_world_view + + @view_to_component(strict_parsing=False) + def hello_world_view(request): + return HttpResponse(" Hello World ") @component def my_component(): return html.div( - view_to_component(hello_world_view, strict_parsing=False), + hello_world_view(), ) ``` - === "views.py" - - {% include-markdown "../../includes/examples.md" start="" end="" %} - Note that best-fit parsing is very similar to how web browsers will handle broken HTML. ??? question "What is `transforms`?" @@ -155,42 +210,25 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible ```python from idom import component, html + from django.http import HttpResponse from django_idom.components import view_to_component - from .views import hello_world_view def example_transform(vdom): attributes = vdom.get("attributes") - if attributes and attributes.get("id") == "hello-world": vdom["children"][0] = "Good Bye World!" + @view_to_component(transforms=[example_transform]) + def hello_world_view(request): + return HttpResponse("
Hello World!
") + @component def my_component(): - return view_to_component( - hello_world_view, - transforms=[example_transform], + return html.div( + hello_world_view(), ) ``` - === "views.py" - - ```python - from django.http import HttpResponse - - def hello_world_view(request, *args, **kwargs): - return HttpResponse("
Hello World!
") - ``` - -??? warning "Limitations" - - There are currently several limitations of using `view_to_component` that may be resolved in a future version of `django_idom`. - - Please note these limitations do not exist when using `compatibility` mode. - - - Requires manual intervention to change request methods beyond `GET`. - - Does not currently load any HTML contained with a `` tag - - Has no option to automatically intercept local anchor link (ex. `#!html `) click events - ## Django CSS Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). diff --git a/src/django_idom/components.py b/src/django_idom/components.py index acffaccd..3ce4f81b 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -116,8 +116,8 @@ def view_to_component( Keyword Args: view: The view function or class to convert. compatibility: If True, the component will be rendered in an iframe. - When using compatibility mode `tranforms`, `strict_parsing`, and `request` - arguments will be ignored. + When using compatibility mode `tranforms`, `strict_parsing`, `request`, + `args, and `kwargs` arguments will be ignored. transforms: A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. strict_parsing: If True, an exception will be generated if the HTML does not From f177d714fe9b73eb253247e65c0d7f4884282919 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 02:22:11 -0700 Subject: [PATCH 17/21] remove unneeded test --- tests/test_app/tests/test_components.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 34a647ad..68c0f721 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -180,12 +180,6 @@ def test_view_to_component_template_view_class_compatibility(self): "#ViewToComponentTemplateViewClassCompatibility[data-success=true]" ).wait_for() - def test_view_to_component_iframe_registration(self): - view_to_component(lambda x: None, compatibility=True) - self.assertIn( - "test_app.tests.test_components.lambda", IDOM_VIEW_COMPONENT_IFRAMES - ) - def test_view_to_component_decorator(self): self.page.locator("#view_to_component_decorator[data-success=true]").wait_for() From 6dcb22d2188c4be34bfb3c979fa0396b09edf31d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 02:45:07 -0700 Subject: [PATCH 18/21] fix styling --- tests/test_app/tests/test_components.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 68c0f721..e819efce 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -5,9 +5,6 @@ from channels.testing import ChannelsLiveServerTestCase from playwright.sync_api import TimeoutError, sync_playwright -from django_idom.components import view_to_component -from django_idom.config import IDOM_VIEW_COMPONENT_IFRAMES - class TestIdomCapabilities(ChannelsLiveServerTestCase): @classmethod From 492721e7fcb8b9105c5edae28e9c0477bee292ce Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 03:09:50 -0700 Subject: [PATCH 19/21] Dict -> Mapping --- docs/src/features/components.md | 2 +- src/django_idom/components.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 832e6023..fc36d650 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -35,7 +35,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible | Type | Description | | --- | --- | - | `Callable[[HttpRequest | None, Sequence | None, Dict | None], Component]` | A function that takes a `request=...`, `args=...`, and `kwargs=...` and returns an IDOM component. | + | `Callable[[HttpRequest | None, Sequence | None, Mapping | None], Component]` | A function that takes a `request=...`, `args=...`, and `kwargs=...` and returns an IDOM component. | ??? warning "Existing limitations" diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 3ce4f81b..349e5694 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -3,7 +3,7 @@ import json import os from inspect import iscoroutinefunction -from typing import Any, Callable, Dict, Sequence +from typing import Any, Callable, Mapping, Sequence from channels.db import database_sync_to_async from django.contrib.staticfiles.finders import find @@ -26,7 +26,7 @@ def _view_to_component( strict_parsing: bool, request: HttpRequest | None, args: Sequence | None, - kwargs: Dict | None, + kwargs: Mapping | None, ): converted_view, set_converted_view = hooks.use_state(None) args = args or () @@ -110,7 +110,7 @@ def view_to_component( compatibility: bool = False, transforms: Sequence[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, -) -> Callable[[HttpRequest | None, Sequence | None, Dict | None], Component]: +) -> Callable[[HttpRequest | None, Sequence | None, Mapping | None], Component]: """Converts a Django view to an IDOM component. Keyword Args: @@ -135,7 +135,7 @@ def decorator(view: Callable | View): def wrapper( request: HttpRequest | None = None, args: Sequence | None = None, - kwargs: Dict | None = None, + kwargs: Mapping | None = None, ): return _view_to_component( view=view, From ba93fcd5fbd8d2bd6c7022fb4d5c712165d165ea Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 03:23:10 -0700 Subject: [PATCH 20/21] Revert "Dict -> Mapping" This reverts commit 492721e7fcb8b9105c5edae28e9c0477bee292ce. --- docs/src/features/components.md | 2 +- src/django_idom/components.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index fc36d650..832e6023 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -35,7 +35,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible | Type | Description | | --- | --- | - | `Callable[[HttpRequest | None, Sequence | None, Mapping | None], Component]` | A function that takes a `request=...`, `args=...`, and `kwargs=...` and returns an IDOM component. | + | `Callable[[HttpRequest | None, Sequence | None, Dict | None], Component]` | A function that takes a `request=...`, `args=...`, and `kwargs=...` and returns an IDOM component. | ??? warning "Existing limitations" diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 349e5694..3ce4f81b 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -3,7 +3,7 @@ import json import os from inspect import iscoroutinefunction -from typing import Any, Callable, Mapping, Sequence +from typing import Any, Callable, Dict, Sequence from channels.db import database_sync_to_async from django.contrib.staticfiles.finders import find @@ -26,7 +26,7 @@ def _view_to_component( strict_parsing: bool, request: HttpRequest | None, args: Sequence | None, - kwargs: Mapping | None, + kwargs: Dict | None, ): converted_view, set_converted_view = hooks.use_state(None) args = args or () @@ -110,7 +110,7 @@ def view_to_component( compatibility: bool = False, transforms: Sequence[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, -) -> Callable[[HttpRequest | None, Sequence | None, Mapping | None], Component]: +) -> Callable[[HttpRequest | None, Sequence | None, Dict | None], Component]: """Converts a Django view to an IDOM component. Keyword Args: @@ -135,7 +135,7 @@ def decorator(view: Callable | View): def wrapper( request: HttpRequest | None = None, args: Sequence | None = None, - kwargs: Mapping | None = None, + kwargs: Dict | None = None, ): return _view_to_component( view=view, From 9d700db68221873ce75b762bbf2dd34ced8d2946 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 20:25:19 -0700 Subject: [PATCH 21/21] use protocol to type hint *args and **kwargs --- docs/src/features/components.md | 2 +- src/django_idom/components.py | 22 ++++++++++++++-------- tests/test_app/components.py | 12 ++++++------ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 832e6023..1aaa1f03 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -35,7 +35,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible | Type | Description | | --- | --- | - | `Callable[[HttpRequest | None, Sequence | None, Dict | None], Component]` | A function that takes a `request=...`, `args=...`, and `kwargs=...` and returns an IDOM component. | + | `_ViewComponentConstructor` | A function that takes `request: HttpRequest | None, *args: Any, **kwargs: Any` and returns an IDOM component. | ??? warning "Existing limitations" diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 3ce4f81b..f7320824 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -3,7 +3,7 @@ import json import os from inspect import iscoroutinefunction -from typing import Any, Callable, Dict, Sequence +from typing import Any, Callable, Dict, Protocol, Sequence from channels.db import database_sync_to_async from django.contrib.staticfiles.finders import find @@ -11,13 +11,19 @@ from django.urls import reverse from django.views import View from idom import component, hooks, html, utils -from idom.core.component import Component -from idom.types import VdomDict +from idom.types import ComponentType, VdomDict from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES from django_idom.types import ViewComponentIframe +class _ViewComponentConstructor(Protocol): + def __call__( + self, request: HttpRequest | None, *args: Any, **kwargs: Any + ) -> ComponentType: + ... + + @component def _view_to_component( view: Callable | View, @@ -110,7 +116,7 @@ def view_to_component( compatibility: bool = False, transforms: Sequence[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, -) -> Callable[[HttpRequest | None, Sequence | None, Dict | None], Component]: +) -> _ViewComponentConstructor: """Converts a Django view to an IDOM component. Keyword Args: @@ -124,8 +130,8 @@ def view_to_component( perfectly adhere to HTML5. Returns: - Callable: A function that takes a request, args, and kwargs and returns an IDOM - component. + Callable: A function that takes `request: HttpRequest | None, *args: Any, **kwargs: Any` + and returns an IDOM component. """ def decorator(view: Callable | View): @@ -134,8 +140,8 @@ def decorator(view: Callable | View): def wrapper( request: HttpRequest | None = None, - args: Sequence | None = None, - kwargs: Dict | None = None, + *args: Any, + **kwargs: Any, ): return _view_to_component( view=view, diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 5fa3a980..4275ef39 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -330,33 +330,33 @@ def on_click(_): @component def view_to_component_args(): - params, set_params = hooks.use_state("false") + success, set_success = hooks.use_state("false") def on_click(_): - set_params("") + set_success("") return html._( html.button( {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, "Click me", ), - _view_to_component_args(args=[params]), + _view_to_component_args(None, success), ) @component def view_to_component_kwargs(): - params, set_params = hooks.use_state("false") + success, set_success = hooks.use_state("false") def on_click(_): - set_params("") + set_success("") return html._( html.button( {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, "Click me", ), - _view_to_component_kwargs(kwargs={"success": params}), + _view_to_component_kwargs(success=success), )