Skip to content

Rework view_to_component #98

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ 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`.
- `view_to_component`, `django_css`, and `django_js` type hints will now display like normal functions.

## [1.2.0] - 2022-09-19

### Added
Expand Down Expand Up @@ -130,7 +137,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.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
Expand Down
2 changes: 1 addition & 1 deletion src/django_idom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
152 changes: 86 additions & 66 deletions src/django_idom/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,7 +20,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,
Expand All @@ -28,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:
Expand All @@ -47,98 +47,118 @@ 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)
request_obj = request
if not request:
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
)

# 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:
dotted_path = f"{view.__module__}.{view.__name__}"
dotted_path = dotted_path.replace("<", "").replace(">", "")
IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe(
view, args, kwargs
)
@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_rendered_view(
html.iframe(
{
"src": reverse("idom:view_to_component", args=[dotted_path]),
"loading": "lazy",
}
set_converted_view(
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):
render = 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()
else: # View
render = 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)()
else: # View
render = view_or_template_view

# Render Check 5: Sync function view
else:
render = await database_sync_to_async(view)(request_obj, *args, **kwargs)

# Signal that the view has been rendered
set_rendered_view(
utils.html_to_vdom(
render.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 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):
"""Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading.

Args:
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.

Args:
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):
Expand Down
9 changes: 9 additions & 0 deletions tests/test_app/tests/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)