diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8e1bb14..471f872e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,9 +24,14 @@ Using the following categories, list your changes in this order:
### Added
-- `auth_required` decorator to prevent your components from rendered to unauthenticated users.
+- `auth_required` decorator to prevent your components from rendering to unauthenticated users.
- `use_query` hook for fetching database values.
- `use_mutation` hook for modifying database values.
+- `view_to_component` utility to convert legacy Django views to IDOM components.
+
+### Changed
+
+- Bumped the minimum IDOM version to 0.40.2
### Fixed
diff --git a/docs/changelog/index.md b/docs/changelog/index.md
deleted file mode 100644
index a6e2f878..00000000
--- a/docs/changelog/index.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-hide:
- - navigation
- - toc
----
-
-!!! note "Attribution"
-
- {% include-markdown "../../CHANGELOG.md" start="" end="" %}
-
-{% include-markdown "../../CHANGELOG.md" start="" %}
diff --git a/docs/contribute/running-tests.md b/docs/contribute/running-tests.md
deleted file mode 100644
index 287b9301..00000000
--- a/docs/contribute/running-tests.md
+++ /dev/null
@@ -1,11 +0,0 @@
-This repo uses [Nox](https://nox.thea.codes/en/stable/) to run scripts which can be found in `noxfile.py`. For a full test of available scripts run `nox -l`. To run the full test suite simple execute:
-
-```
-nox -s test
-```
-
-If you want to run the tests in the background (headless):
-
-```
-nox -s test -- --headless
-```
diff --git a/docs/features/components.md b/docs/features/components.md
deleted file mode 100644
index fa6b45f0..00000000
--- a/docs/features/components.md
+++ /dev/null
@@ -1,121 +0,0 @@
-## 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/).
-
-```python title="components.py"
-from idom import component, html
-from django_idom.components import django_css
-
-@component
-def my_component():
- return html.div(
- django_css("css/buttons.css"),
- html.button("My Button!"),
- )
-```
-
-??? question "Should I put `django_css` at the top of my component?"
-
- Yes, if the stylesheet contains styling for your component.
-
-??? question "Can I load static CSS using `html.link` instead?"
-
- While you can load stylesheets with `html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will be loaded after your component is displayed. This would likely cause some visual jankiness, so use this at your own discretion.
-
- Here's an example on what you should avoid doing for Django static files:
-
- ```python
- from idom import component, html
- from django.templatetags.static import static
-
- @component
- def my_component():
- return html.div(
- html.link({"rel": "stylesheet", "href": static("css/buttons.css")}),
- html.button("My Button!"),
- )
- ```
-
-??? question "How do I load external CSS?"
-
- `django_css` can only be used with local static files.
-
- For external CSS, substitute `django_css` with `html.link`.
-
- ```python
- from idom import component, html
-
- @component
- def my_component():
- return html.div(
- html.link({"rel": "stylesheet", "href": "https://example.com/external-styles.css"}),
- html.button("My Button!"),
- )
- ```
-
-??? question "Why not load my CSS in `#!html
`?"
-
- Traditionally, stylesheets are loaded in your `#!html ` using the `#!jinja {% load static %}` template tag.
-
- To help improve webpage load times, you can use the `django_css` component to defer loading your stylesheet until it is needed.
-
-## Django JS
-
-Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/).
-
-```python title="components.py"
-from idom import component, html
-from django_idom.components import django_js
-
-@component
-def my_component():
- return html.div(
- html.button("My Button!"),
- django_js("js/scripts.js"),
- )
-```
-
-??? question "Should I put `django_js` at the bottom of my component?"
-
- Yes, if your scripts are reliant on the contents of the component.
-
-??? question "Can I load static JavaScript using `html.script` instead?"
-
- While you can load JavaScript with `html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed.
-
- Here's an example on what you should avoid doing for Django static files:
-
- ```python
- from idom import component, html
- from django.templatetags.static import static
-
- @component
- def my_component():
- return html.div(
- html.script({"src": static("js/scripts.js")}),
- html.button("My Button!"),
- )
- ```
-
-??? question "How do I load external JS?"
-
- `django_js` can only be used with local static files.
-
- For external JavaScript, substitute `django_js` with `html.script`.
-
- ```python
- from idom import component, html
-
- @component
- def my_component():
- return html.div(
- html.script({"src": "https://example.com/external-scripts.js"}),
- html.button("My Button!"),
- )
- ```
-
-??? question "Why not load my JS in `#!html `?"
-
- Traditionally, JavaScript is loaded in your `#!html ` using the `#!jinja {% load static %}` template tag.
-
- To help improve webpage load times, you can use the `django_js` component to defer loading your JavaScript until it is needed.
diff --git a/docs/getting-started/learn-more.md b/docs/getting-started/learn-more.md
deleted file mode 100644
index abbc1099..00000000
--- a/docs/getting-started/learn-more.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# :confetti_ball: Congratulations :confetti_ball:
-
-If you followed the previous steps, you've now created a "Hello World" component!
-
-The docs you are reading only covers our Django integration.
-
-To learn more about our advanced features, such as interactive events and hooks, check out the [IDOM Core Documentation](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html)!
-
-| Learn More |
-| --- |
-| [Django-IDOM — Exclusive Features](../features/hooks.md){ .md-button } [IDOM Core — Hooks, Events, and More](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html){ .md-button } |
diff --git a/docs/includes/examples.md b/docs/includes/examples.md
new file mode 100644
index 00000000..623e60c3
--- /dev/null
+++ b/docs/includes/examples.md
@@ -0,0 +1,23 @@
+
+
+```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/index.md b/docs/index.md
deleted file mode 100644
index 9b11f2dc..00000000
--- a/docs/index.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-hide:
- - navigation
- - toc
----
-
-{% include-markdown "../README.md" start="" end="" %}
-
-## ReactJS for Django Developers.
-
----
-
-{% include-markdown "../README.md" start="" end="" %}
-
-## Resources
-
-{% include-markdown "../README.md" start="" end="" %}
diff --git a/docs/src/changelog/index.md b/docs/src/changelog/index.md
new file mode 100644
index 00000000..a4a0f241
--- /dev/null
+++ b/docs/src/changelog/index.md
@@ -0,0 +1,11 @@
+---
+hide:
+ - navigation
+ - toc
+---
+
+!!! note "Attribution"
+
+ {% include-markdown "../../../CHANGELOG.md" start="" end="" %}
+
+{% include-markdown "../../../CHANGELOG.md" start="" %}
diff --git a/docs/contribute/django-idom.md b/docs/src/contribute/django-idom.md
similarity index 100%
rename from docs/contribute/django-idom.md
rename to docs/src/contribute/django-idom.md
diff --git a/docs/contribute/docs.md b/docs/src/contribute/docs.md
similarity index 100%
rename from docs/contribute/docs.md
rename to docs/src/contribute/docs.md
diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md
new file mode 100644
index 00000000..ece8d1a7
--- /dev/null
+++ b/docs/src/contribute/running-tests.md
@@ -0,0 +1,15 @@
+This repo uses [Nox](https://nox.thea.codes/en/stable/) to run scripts which can be found in `noxfile.py`. For a full test of available scripts run `nox -l`. To run the full test suite simple execute:
+
+```
+nox -s test
+```
+
+If you do not want to run the tests in the background:
+
+```
+nox -s test -- --headed
+```
+
+!!! warning "Most tests will not run on Windows"
+
+ Due to [bugs within Django Channels](https://github.com/django/channels/issues/1207), functional tests are not run on Windows. In order for Windows users to test Django-IDOM functionality, you will need to run tests via [Windows Subsystem for Linux](https://code.visualstudio.com/docs/remote/wsl).
diff --git a/docs/src/features/components.md b/docs/src/features/components.md
new file mode 100644
index 00000000..cc8d9bf4
--- /dev/null
+++ b/docs/src/features/components.md
@@ -0,0 +1,304 @@
+## View To Component
+
+Convert any Django view into a IDOM component by usng this decorator. Compatible with sync/async [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/).
+
+=== "components.py"
+
+ ```python
+ from idom import component, html
+ from django_idom.components import view_to_component
+ from .views import hello_world_view
+
+ @component
+ def my_component():
+ return html.div(
+ view_to_component(hello_world_view),
+ )
+ ```
+
+=== "views.py"
+
+ {% include-markdown "../../includes/examples.md" start="" end="" %}
+
+??? example "See Interface"
+
+ **Parameters**
+
+ | 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. Strict parsing does not apply to compatibility mode. | `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` |
+ | 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. Custom request objects cannot be used in compatibility mode. | `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. |
+
+??? question "How do I use this for Class Based Views?"
+
+ You can simply pass your Class Based View directly into this function.
+
+ === "components.py"
+
+ ```python
+ from idom import component, html
+ from django_idom.components import view_to_component
+ from .views import HelloWorldView
+
+ @component
+ def my_component():
+ return html.div(
+ view_to_component(HelloWorldView),
+ )
+ ```
+
+ === "views.py"
+
+ {% include-markdown "../../includes/examples.md" start="" end="" %}
+
+??? question "How do I pass arguments into the view?"
+
+ You can use the `args` and `kwargs` parameters to pass arguments to the view.
+
+ === "components.py"
+
+ ```python
+ from idom import component, html
+ from django_idom.components import view_to_component
+ from .views import hello_world_view
+
+ @component
+ def my_component():
+ return html.div(
+ view_to_component(
+ hello_world_view,
+ args=["value_1", "value_2"],
+ kwargs={"key_1": "value_1", "key_2": "value_2"},
+ ),
+ )
+ ```
+
+ === "views.py"
+
+ {% include-markdown "../../includes/examples.md" start="" end="" %}
+
+??? 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 `strict_parsing` argument does 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.
+
+ === "components.py"
+
+ ```python
+ from idom import component, html
+ from django_idom.components import view_to_component
+ from .views import hello_world_view
+
+ @component
+ def my_component():
+ return html.div(
+ view_to_component(hello_world_view, compatibility=True),
+ )
+ ```
+
+ === "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.
+
+ However, there are some circumstances where you may not have control over the original HTML, so you may be unable to fix it. Or you may be relying on non-standard HTML tags such as `#!html Hello World `.
+
+ In these scenarios, you may want to rely on best-fit parsing by setting the `strict_parsing` parameter to `False`.
+
+ === "components.py"
+
+ ```python
+ from idom import component, html
+ from django_idom.components import view_to_component
+ from .views import hello_world_view
+
+ @component
+ def my_component():
+ return html.div(
+ view_to_component(hello_world_view, strict_parsing=False),
+ )
+ ```
+
+ === "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`?"
+
+ After your view has been turned into [VDOM](https://idom-docs.herokuapp.com/docs/reference/specifications.html#vdom) (python dictionaries), `view_to_component` will call your `transforms` functions on every VDOM node.
+
+ This allows you to modify your view prior to rendering.
+
+ For example, if you are trying to modify the text of a node with a certain `id`, you can create a transform like such:
+
+ === "components.py"
+
+ ```python
+ from idom import component, html
+ 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!"
+
+ @component
+ def my_component():
+ return view_to_component(
+ hello_world_view,
+ transforms=[example_transform],
+ )
+ ```
+
+ === "views.py"
+
+ ```python
+ from django.http import HttpResponse
+
+ def hello_world_view(request, *args, **kwargs):
+ return HttpResponse("
Hello World!
")
+ ```
+
+## 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/).
+
+```python title="components.py"
+from idom import component, html
+from django_idom.components import django_css
+
+@component
+def my_component():
+ return html.div(
+ django_css("css/buttons.css"),
+ html.button("My Button!"),
+ )
+```
+
+??? question "Should I put `django_css` at the top of my component?"
+
+ Yes, if the stylesheet contains styling for your component.
+
+??? question "Can I load static CSS using `html.link` instead?"
+
+ While you can load stylesheets with `html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will be loaded after your component is displayed. This would likely cause some visual jankiness, so use this at your own discretion.
+
+ Here's an example on what you should avoid doing for Django static files:
+
+ ```python
+ from idom import component, html
+ from django.templatetags.static import static
+
+ @component
+ def my_component():
+ return html.div(
+ html.link({"rel": "stylesheet", "href": static("css/buttons.css")}),
+ html.button("My Button!"),
+ )
+ ```
+
+??? question "How do I load external CSS?"
+
+ `django_css` can only be used with local static files.
+
+ For external CSS, substitute `django_css` with `html.link`.
+
+ ```python
+ from idom import component, html
+
+ @component
+ def my_component():
+ return html.div(
+ html.link({"rel": "stylesheet", "href": "https://example.com/external-styles.css"}),
+ html.button("My Button!"),
+ )
+ ```
+
+??? question "Why not load my CSS in `#!html `?"
+
+ Traditionally, stylesheets are loaded in your `#!html ` using the `#!jinja {% load static %}` template tag.
+
+ To help improve webpage load times, you can use the `django_css` component to defer loading your stylesheet until it is needed.
+
+## Django JS
+
+Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/).
+
+```python title="components.py"
+from idom import component, html
+from django_idom.components import django_js
+
+@component
+def my_component():
+ return html.div(
+ html.button("My Button!"),
+ django_js("js/scripts.js"),
+ )
+```
+
+??? question "Should I put `django_js` at the bottom of my component?"
+
+ Yes, if your scripts are reliant on the contents of the component.
+
+??? question "Can I load static JavaScript using `html.script` instead?"
+
+ While you can load JavaScript with `html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed.
+
+ Here's an example on what you should avoid doing for Django static files:
+
+ ```python
+ from idom import component, html
+ from django.templatetags.static import static
+
+ @component
+ def my_component():
+ return html.div(
+ html.script({"src": static("js/scripts.js")}),
+ html.button("My Button!"),
+ )
+ ```
+
+??? question "How do I load external JS?"
+
+ `django_js` can only be used with local static files.
+
+ For external JavaScript, substitute `django_js` with `html.script`.
+
+ ```python
+ from idom import component, html
+
+ @component
+ def my_component():
+ return html.div(
+ html.script({"src": "https://example.com/external-scripts.js"}),
+ html.button("My Button!"),
+ )
+ ```
+
+??? question "Why not load my JS in `#!html `?"
+
+ Traditionally, JavaScript is loaded in your `#!html ` using the `#!jinja {% load static %}` template tag.
+
+ To help improve webpage load times, you can use the `django_js` component to defer loading your JavaScript until it is needed.
diff --git a/docs/features/decorators.md b/docs/src/features/decorators.md
similarity index 100%
rename from docs/features/decorators.md
rename to docs/src/features/decorators.md
diff --git a/docs/features/hooks.md b/docs/src/features/hooks.md
similarity index 99%
rename from docs/features/hooks.md
rename to docs/src/features/hooks.md
index 1951f43d..09c8daa9 100644
--- a/docs/features/hooks.md
+++ b/docs/src/features/hooks.md
@@ -1,6 +1,6 @@
???+ tip "Looking for more hooks?"
- Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html?highlight=hooks) on hooks!
+ Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html#basic-hooks) on hooks!
## Use Query
diff --git a/docs/features/settings.md b/docs/src/features/settings.md
similarity index 100%
rename from docs/features/settings.md
rename to docs/src/features/settings.md
diff --git a/docs/features/templatetag.md b/docs/src/features/templatetag.md
similarity index 94%
rename from docs/features/templatetag.md
rename to docs/src/features/templatetag.md
index 5f917f92..43910f4c 100644
--- a/docs/features/templatetag.md
+++ b/docs/src/features/templatetag.md
@@ -1,6 +1,6 @@
Integrated within Django IDOM, we bundle a template tag. Within this tag, you can pass in keyword arguments directly into your component.
-{% include-markdown "../../README.md" start="" end="" %}
+{% include-markdown "../../../README.md" start="" end="" %}
@@ -81,6 +81,6 @@ Integrated within Django IDOM, we bundle a template tag. Within this tag, you ca
Keep in mind, in order to use the `#!jinja {% component ... %}` tag, you'll need to first call `#!jinja {% load idom %}` to gain access to it.
- {% include-markdown "../../README.md" start="" end="" %}
+ {% include-markdown "../../../README.md" start="" end="" %}
diff --git a/docs/getting-started/create-component.md b/docs/src/getting-started/create-component.md
similarity index 71%
rename from docs/getting-started/create-component.md
rename to docs/src/getting-started/create-component.md
index ca6e2a18..29281051 100644
--- a/docs/getting-started/create-component.md
+++ b/docs/src/getting-started/create-component.md
@@ -4,9 +4,9 @@
---
-{% include-markdown "../../README.md" start="" end="" %}
+{% include-markdown "../../../README.md" start="" end="" %}
-{% include-markdown "../../README.md" start="" end="" %}
+{% include-markdown "../../../README.md" start="" end="" %}
??? question "What should I name my IDOM files and functions?"
diff --git a/docs/getting-started/initial-steps.md b/docs/src/getting-started/initial-steps.md
similarity index 100%
rename from docs/getting-started/initial-steps.md
rename to docs/src/getting-started/initial-steps.md
diff --git a/docs/src/getting-started/learn-more.md b/docs/src/getting-started/learn-more.md
new file mode 100644
index 00000000..31449ea9
--- /dev/null
+++ b/docs/src/getting-started/learn-more.md
@@ -0,0 +1,11 @@
+# :confetti_ball: Congratulations :confetti_ball:
+
+If you followed the previous steps, you've now created a "Hello World" component!
+
+The docs you are reading only covers our Django integration. To learn more about features, such as interactive events and hooks, check out the [IDOM Core Documentation](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html)!
+
+Additionally, the vast majority of tutorials/guides you find for React can be applied to IDOM.
+
+| Learn More |
+| --- |
+| [Django-IDOM Exclusive Features](../features/hooks.md){ .md-button } [IDOM Hooks, Events, and More](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html){ .md-button } [Ask Questions on GitHub Discussions](https://github.com/idom-team/idom/discussions){ .md-button .md-button--primary } |
diff --git a/docs/getting-started/reference-component.md b/docs/src/getting-started/reference-component.md
similarity index 83%
rename from docs/getting-started/reference-component.md
rename to docs/src/getting-started/reference-component.md
index af7dd77c..db5c4a5e 100644
--- a/docs/getting-started/reference-component.md
+++ b/docs/src/getting-started/reference-component.md
@@ -4,9 +4,9 @@
---
-{% include-markdown "../../README.md" start="" end="" %}
+{% include-markdown "../../../README.md" start="" end="" %}
-{% include-markdown "../../README.md" start="" end="" %}
+{% include-markdown "../../../README.md" start="" end="" %}
{% include-markdown "../features/templatetag.md" start="" end="" %}
diff --git a/docs/getting-started/render-view.md b/docs/src/getting-started/render-view.md
similarity index 100%
rename from docs/getting-started/render-view.md
rename to docs/src/getting-started/render-view.md
diff --git a/docs/src/index.md b/docs/src/index.md
new file mode 100644
index 00000000..07ac4bb5
--- /dev/null
+++ b/docs/src/index.md
@@ -0,0 +1,17 @@
+---
+hide:
+ - navigation
+ - toc
+---
+
+{% include-markdown "../../README.md" start="" end="" %}
+
+## ReactJS for Django Developers.
+
+---
+
+{% include-markdown "../../README.md" start="" end="" %}
+
+## Resources
+
+{% include-markdown "../../README.md" start="" end="" %}
diff --git a/docs/installation/index.md b/docs/src/installation/index.md
similarity index 100%
rename from docs/installation/index.md
rename to docs/src/installation/index.md
diff --git a/docs/stylesheets/extra.css b/docs/src/stylesheets/extra.css
similarity index 100%
rename from docs/stylesheets/extra.css
rename to docs/src/stylesheets/extra.css
diff --git a/mkdocs.yml b/mkdocs.yml
index 60ece595..3d7287b9 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,7 +8,7 @@ nav:
- 3. Use the Template Tag: getting-started/reference-component.md
- 4. Render Your View: getting-started/render-view.md
- 5. Learn More: getting-started/learn-more.md
- - Exclusive Features:
+ - Usage:
- Components: features/components.md
- Hooks: features/hooks.md
- Decorators: features/decorators.md
@@ -86,3 +86,4 @@ repo_url: https://github.com/idom-team/django-idom
site_url: https://idom-team.github.io/django-idom
repo_name: idom-team/django-idom
edit_uri: edit/docs
+docs_dir: docs/src
diff --git a/noxfile.py b/noxfile.py
index f382980d..c0c63855 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,10 +1,7 @@
from __future__ import annotations
-import os
import re
-import subprocess
from pathlib import Path
-from typing import List, Tuple
import nox
from nox.sessions import Session
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index 631913a9..c34467fc 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -1,4 +1,4 @@
-from django_idom import components, decorators, hooks, types
+from django_idom import components, decorators, hooks, types, utils
from django_idom.types import IdomWebsocket
from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH
@@ -6,9 +6,10 @@
__version__ = "1.1.0"
__all__ = [
"IDOM_WEBSOCKET_PATH",
- "types",
"IdomWebsocket",
"hooks",
"components",
"decorators",
+ "types",
+ "utils",
]
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index 1433d30a..9c5aae6c 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -1,9 +1,122 @@
+from __future__ import annotations
+
+import json
import os
+from inspect import iscoroutinefunction
+from typing import Any, Callable, Dict, Iterable
+from channels.db import database_sync_to_async
from django.contrib.staticfiles.finders import find
-from idom import component, html
+from django.http import HttpRequest
+from django.urls import reverse
+from django.views import View
+from idom import component, hooks, html, utils
+from idom.types import VdomDict
-from django_idom.config import IDOM_CACHE
+from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES
+from django_idom.types import ViewComponentIframe
+
+
+# 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,
+ transforms: Iterable[Callable[[VdomDict], Any]] = (),
+ strict_parsing: bool = True,
+ request: HttpRequest | None = None,
+ args: Iterable = (),
+ kwargs: Dict | None = None,
+) -> VdomDict | None:
+ """Converts a Django view to an IDOM component.
+
+ Args:
+ view: The view function or class to convert.
+
+ Keyword Args:
+ compatibility: If True, the component will be rendered in an iframe.
+ Strict parsing does not apply to compatibility mode.
+ 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
+ perfectly adhere to HTML5.
+ request: Request object to provide to the view.
+ Custom request objects cannot be used in compatibility mode.
+ args: The positional arguments to pass to the view.
+ 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"
+
+ # 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
+ )
+
+ # Signal that the view has been rendered
+ set_rendered_view(
+ html.iframe(
+ {
+ "src": reverse("idom:view_to_component", args=[dotted_path]),
+ "loading": "lazy",
+ }
+ )
+ )
+ 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
@component
@@ -14,7 +127,7 @@ 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._(html.style(_cached_static_contents(static_path)))
+ return html.style(_cached_static_contents(static_path))
@component
@@ -40,13 +153,24 @@ def _cached_static_contents(static_path: str):
# Cache is preferrable to `use_memo` due to multiprocessing capabilities
last_modified_time = os.stat(abs_path).st_mtime
cache_key = f"django_idom:static_contents:{static_path}"
- file_contents = IDOM_CACHE.get(cache_key, version=last_modified_time)
+ file_contents = IDOM_CACHE.get(cache_key, version=int(last_modified_time))
if file_contents is None:
with open(abs_path, encoding="utf-8") as static_file:
file_contents = static_file.read()
IDOM_CACHE.delete(cache_key)
IDOM_CACHE.set(
- cache_key, file_contents, timeout=None, version=last_modified_time
+ cache_key, file_contents, timeout=None, version=int(last_modified_time)
)
return file_contents
+
+
+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__"):
+ if hasattr(object, "__name__"):
+ return f"{object.__module__}.{object.__name__}"
+ if hasattr(object, "__class__"):
+ return f"{object.__module__}.{object.__class__.__name__}"
+ return None
diff --git a/src/django_idom/config.py b/src/django_idom/config.py
index cb037ed1..b87fb7e9 100644
--- a/src/django_idom/config.py
+++ b/src/django_idom/config.py
@@ -1,11 +1,14 @@
from typing import Dict
from django.conf import settings
-from django.core.cache import DEFAULT_CACHE_ALIAS, caches
+from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches
from idom.core.types import ComponentConstructor
+from django_idom.types import ViewComponentIframe
+
IDOM_REGISTERED_COMPONENTS: Dict[str, ComponentConstructor] = {}
+IDOM_VIEW_COMPONENT_IFRAMES: Dict[str, ViewComponentIframe] = {}
IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "idom/")
IDOM_WS_MAX_RECONNECT_TIMEOUT = getattr(
@@ -13,7 +16,8 @@
)
# Determine if using Django caching or LRU cache
-if "idom" in getattr(settings, "CACHES", {}):
- IDOM_CACHE = caches["idom"]
-else:
- IDOM_CACHE = caches[DEFAULT_CACHE_ALIAS]
+IDOM_CACHE: BaseCache = (
+ caches["idom"]
+ if "idom" in getattr(settings, "CACHES", {})
+ else caches[DEFAULT_CACHE_ALIAS]
+)
diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py
index 0659938a..5800d220 100644
--- a/src/django_idom/decorators.py
+++ b/src/django_idom/decorators.py
@@ -1,5 +1,7 @@
+from __future__ import annotations
+
from functools import wraps
-from typing import Callable, Union
+from typing import Callable
from idom.core.types import ComponentType, VdomDict
@@ -7,9 +9,9 @@
def auth_required(
- component: Union[Callable, None] = None,
+ component: Callable | None = None,
auth_attribute: str = "is_active",
- fallback: Union[ComponentType, VdomDict, None] = None,
+ fallback: ComponentType | VdomDict | None = None,
) -> Callable:
"""If the user passes authentication criteria, the decorated component will be rendered.
Otherwise, the fallback component will be rendered.
@@ -29,16 +31,9 @@ def _wrapped_func(*args, **kwargs):
if getattr(websocket.scope["user"], auth_attribute):
return component(*args, **kwargs)
-
- if callable(fallback):
- return fallback(*args, **kwargs)
- return fallback
+ return fallback(*args, **kwargs) if callable(fallback) else fallback
return _wrapped_func
- # Return for @authenticated(...)
- if component is None:
- return decorator
-
- # Return for @authenticated
- return decorator(component)
+ # Return for @authenticated(...) and @authenticated respectively
+ return decorator if component is None else decorator(component)
diff --git a/src/django_idom/http/urls.py b/src/django_idom/http/urls.py
index 6f7021b7..50b0c4ec 100644
--- a/src/django_idom/http/urls.py
+++ b/src/django_idom/http/urls.py
@@ -10,5 +10,10 @@
"web_module/",
views.web_modules_file, # type: ignore[arg-type]
name="web_modules",
- )
+ ),
+ path(
+ "iframe/",
+ views.view_to_component_iframe, # type: ignore[arg-type]
+ name="view_to_component",
+ ),
]
diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py
index 4134c058..033e9ffd 100644
--- a/src/django_idom/http/views.py
+++ b/src/django_idom/http/views.py
@@ -1,11 +1,13 @@
import os
+from inspect import iscoroutinefunction
from aiofile import async_open
+from channels.db import database_sync_to_async
from django.core.exceptions import SuspiciousOperation
from django.http import HttpRequest, HttpResponse
from idom.config import IDOM_WED_MODULES_DIR
-from django_idom.config import IDOM_CACHE
+from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES
async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
@@ -23,12 +25,47 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
# Fetch the file from cache, if available
last_modified_time = os.stat(path).st_mtime
cache_key = f"django_idom:web_module:{str(path).lstrip(str(web_modules_dir))}"
- response = await IDOM_CACHE.aget(cache_key, version=last_modified_time)
+ response = await IDOM_CACHE.aget(cache_key, version=last_modified_time) # type: ignore[attr-defined]
if response is None:
async with async_open(path, "r") as fp:
response = HttpResponse(await fp.read(), content_type="text/javascript")
- await IDOM_CACHE.adelete(cache_key)
- await IDOM_CACHE.aset(
+ await IDOM_CACHE.adelete(cache_key) # type: ignore[attr-defined]
+ await IDOM_CACHE.aset( # type: ignore[attr-defined]
cache_key, response, timeout=None, version=last_modified_time
)
return response
+
+
+async def view_to_component_iframe(
+ request: HttpRequest, view_path: str
+) -> HttpResponse:
+ """Returns a view that was registered by view_to_component.
+ This view is intended to be used as iframe, for compatibility purposes."""
+ # Get the view from IDOM_REGISTERED_IFRAMES
+ iframe = IDOM_VIEW_COMPONENT_IFRAMES.get(view_path)
+ if not iframe:
+ raise ValueError(f"No view registered for component {view_path}.")
+
+ # Render Check 1: Async function view
+ if iscoroutinefunction(iframe.view):
+ response = await iframe.view(request, *iframe.args, **iframe.kwargs) # type: ignore[operator]
+
+ # Render Check 2: Async class view
+ elif getattr(iframe.view, "view_is_async", False):
+ response = await iframe.view.as_view()(request, *iframe.args, **iframe.kwargs) # type: ignore[misc, union-attr]
+
+ # Render Check 3: Sync class view
+ elif getattr(iframe.view, "as_view", None):
+ response = await database_sync_to_async(iframe.view.as_view())( # type: ignore[union-attr]
+ request, *iframe.args, **iframe.kwargs
+ )
+
+ # Render Check 4: Sync function view
+ else:
+ response = await database_sync_to_async(iframe.view)(
+ request, *iframe.args, **iframe.kwargs
+ )
+
+ # Ensure page can be rendered as an iframe
+ response["X-Frame-Options"] = "SAMEORIGIN"
+ return response
diff --git a/src/django_idom/types.py b/src/django_idom/types.py
index 88f9c32f..1f4bd406 100644
--- a/src/django_idom/types.py
+++ b/src/django_idom/types.py
@@ -1,10 +1,11 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar, Union
+from typing import Any, Awaitable, Callable, Generic, Iterable, Optional, TypeVar, Union
from django.db.models.base import Model
from django.db.models.query import QuerySet
+from django.views.generic import View
from typing_extensions import ParamSpec
@@ -43,3 +44,10 @@ class Mutation(Generic[_Params]):
loading: bool
error: Exception | None
reset: Callable[[], None]
+
+
+@dataclass
+class ViewComponentIframe:
+ view: View | Callable
+ args: Iterable
+ kwargs: dict
diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py
index 27d106a4..2f6782ab 100644
--- a/src/django_idom/utils.py
+++ b/src/django_idom/utils.py
@@ -6,6 +6,7 @@
import re
from fnmatch import fnmatch
from importlib import import_module
+from typing import Callable
from django.template import engines
from django.utils.encoding import smart_str
@@ -28,11 +29,17 @@
)
-def _register_component(full_component_name: str) -> None:
- if full_component_name in IDOM_REGISTERED_COMPONENTS:
+def _register_component(dotted_path: str) -> None:
+ if dotted_path in IDOM_REGISTERED_COMPONENTS:
return
- module_name, component_name = full_component_name.rsplit(".", 1)
+ IDOM_REGISTERED_COMPONENTS[dotted_path] = _import_dotted_path(dotted_path)
+ _logger.debug("IDOM has registered component %s", dotted_path)
+
+
+def _import_dotted_path(dotted_path: str) -> Callable:
+ """Imports a dotted path and returns the callable."""
+ module_name, component_name = dotted_path.rsplit(".", 1)
try:
module = import_module(module_name)
@@ -41,15 +48,7 @@ def _register_component(full_component_name: str) -> None:
f"Failed to import {module_name!r} while loading {component_name!r}"
) from error
- try:
- component = getattr(module, component_name)
- except AttributeError as error:
- raise RuntimeError(
- f"Module {module_name!r} has no component named {component_name!r}"
- ) from error
-
- IDOM_REGISTERED_COMPONENTS[full_component_name] = component
- _logger.debug("IDOM has registered component %s", full_component_name)
+ return getattr(module, component_name)
class ComponentPreloader:
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index f8069378..c103e01c 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -1,134 +1,144 @@
-import idom
+import inspect
+
+from django.http import HttpRequest
+from idom import component, hooks, html, web
from test_app.models import TodoItem
import django_idom
+from django_idom.components import view_to_component
from django_idom.hooks import use_mutation, use_query
+from . import views
+
-@idom.component
+@component
def hello_world():
- return idom.html.h1({"id": "hello-world"}, "Hello World!")
+ return html._(html.h1({"id": "hello-world"}, "Hello World!"), html.hr())
-@idom.component
+@component
def button():
- count, set_count = idom.hooks.use_state(0)
- return idom.html.div(
- idom.html.button(
- {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
- "Click me!",
- ),
- idom.html.p(
- {"id": "counter-num", "data-count": count},
- f"Current count is: {count}",
+ count, set_count = hooks.use_state(0)
+ return html._(
+ html.div(
+ html.button(
+ {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)},
+ "Click me!",
+ ),
+ html.p(
+ {"id": "counter-num", "data-count": count},
+ f"Current count is: {count}",
+ ),
),
+ html.hr(),
)
-@idom.component
+@component
def parameterized_component(x, y):
total = x + y
- return idom.html.h1({"id": "parametrized-component", "data-value": total}, total)
+ return html._(
+ html.h1({"id": "parametrized-component", "data-value": total}, total),
+ html.hr(),
+ )
-victory = idom.web.module_from_template("react", "victory-bar", fallback="...")
-VictoryBar = idom.web.export(victory, "VictoryBar")
+victory = web.module_from_template("react", "victory-bar", fallback="...")
+VictoryBar = web.export(victory, "VictoryBar")
-@idom.component
+@component
def simple_bar_chart():
- return VictoryBar()
+ return html._(VictoryBar(), html.hr())
-@idom.component
+@component
def use_websocket():
ws = django_idom.hooks.use_websocket()
- ws.scope = "..."
success = bool(ws.scope and ws.close and ws.disconnect and ws.view_id)
- return idom.html.div(
+ return html.div(
{"id": "use-websocket", "data-success": success},
- idom.html.hr(),
f"use_websocket: {ws}",
- idom.html.hr(),
+ html.hr(),
)
-@idom.component
+@component
def use_scope():
scope = django_idom.hooks.use_scope()
success = len(scope) >= 10 and scope["type"] == "websocket"
- return idom.html.div(
+ return html.div(
{"id": "use-scope", "data-success": success},
f"use_scope: {scope}",
- idom.html.hr(),
+ html.hr(),
)
-@idom.component
+@component
def use_location():
location = django_idom.hooks.use_location()
success = bool(location)
- return idom.html.div(
+ return html.div(
{"id": "use-location", "data-success": success},
f"use_location: {location}",
- idom.html.hr(),
+ html.hr(),
)
-@idom.component
+@component
def django_css():
- return idom.html.div(
+ return html.div(
{"id": "django-css"},
django_idom.components.django_css("django-css-test.css"),
- idom.html.div({"style": {"display": "inline"}}, "django_css: "),
- idom.html.button("This text should be blue."),
- idom.html.hr(),
+ html.div({"style": {"display": "inline"}}, "django_css: "),
+ html.button("This text should be blue."),
+ html.hr(),
)
-@idom.component
+@component
def django_js():
success = False
- return idom.html._(
- idom.html.div(
+ return html._(
+ html.div(
{"id": "django-js", "data-success": success},
f"django_js: {success}",
django_idom.components.django_js("django-js-test.js"),
),
- idom.html.hr(),
+ html.hr(),
)
-@idom.component
+@component
@django_idom.decorators.auth_required(
- fallback=idom.html.div(
+ fallback=html.div(
{"id": "unauthorized-user-fallback"},
"unauthorized_user: Success",
- idom.html.hr(),
+ html.hr(),
)
)
def unauthorized_user():
- return idom.html.div(
+ return html.div(
{"id": "unauthorized-user"},
"unauthorized_user: Fail",
- idom.html.hr(),
+ html.hr(),
)
-@idom.component
+@component
@django_idom.decorators.auth_required(
auth_attribute="is_anonymous",
- fallback=idom.html.div(
+ fallback=html.div(
{"id": "authorized-user-fallback"},
"authorized_user: Fail",
- idom.html.hr(),
+ html.hr(),
),
)
def authorized_user():
- return idom.html.div(
+ return html.div(
{"id": "authorized-user"},
"authorized_user: Success",
- idom.html.hr(),
+ html.hr(),
)
@@ -153,30 +163,30 @@ def toggle_item_mutation(item: TodoItem):
item.save()
-@idom.component
+@component
def todo_list():
- input_value, set_input_value = idom.use_state("")
+ input_value, set_input_value = hooks.use_state("")
items = use_query(get_items_query)
toggle_item = use_mutation(toggle_item_mutation, refetch=get_items_query)
if items.error:
- rendered_items = idom.html.h2(f"Error when loading - {items.error}")
+ rendered_items = html.h2(f"Error when loading - {items.error}")
elif items.data is None:
- rendered_items = idom.html.h2("Loading...")
+ rendered_items = html.h2("Loading...")
else:
- rendered_items = idom.html._(
- idom.html.h3("Not Done"),
+ rendered_items = html._(
+ html.h3("Not Done"),
_render_items([i for i in items.data if not i.done], toggle_item),
- idom.html.h3("Done"),
+ html.h3("Done"),
_render_items([i for i in items.data if i.done], toggle_item),
)
add_item = use_mutation(add_item_mutation, refetch=get_items_query)
if add_item.loading:
- mutation_status = idom.html.h2("Working...")
+ mutation_status = html.h2("Working...")
elif add_item.error:
- mutation_status = idom.html.h2(f"Error when adding - {add_item.error}")
+ mutation_status = html.h2(f"Error when adding - {add_item.error}")
else:
mutation_status = ""
@@ -188,9 +198,9 @@ def on_submit(event):
def on_change(event):
set_input_value(event["target"]["value"])
- return idom.html.div(
- idom.html.label("Add an item:"),
- idom.html.input(
+ return html.div(
+ html.label("Add an item:"),
+ html.input(
{
"type": "text",
"id": "todo-input",
@@ -201,16 +211,17 @@ def on_change(event):
),
mutation_status,
rendered_items,
+ html.hr(),
)
def _render_items(items, toggle_item):
- return idom.html.ul(
+ return html.ul(
[
- idom.html.li(
+ html.li(
{"id": f"todo-item-{item.text}"},
item.text,
- idom.html.input(
+ html.input(
{
"id": f"todo-item-{item.text}-checkbox",
"type": "checkbox",
@@ -223,3 +234,138 @@ def _render_items(items, toggle_item):
for item in items
]
)
+
+
+@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)
+
+
+@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
+ ),
+ html.hr(),
+ )
+
+
+@component
+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
+ ),
+ html.hr(),
+ )
+
+
+@component
+def view_to_component_sync_class_compatibility():
+ return html.div(
+ {"id": inspect.currentframe().f_code.co_name},
+ view_to_component(
+ views.ViewToComponentSyncClassCompatibility, compatibility=True
+ ),
+ html.hr(),
+ )
+
+
+@component
+def view_to_component_async_class_compatibility():
+ return html.div(
+ {"id": inspect.currentframe().f_code.co_name},
+ view_to_component(
+ views.ViewToComponentAsyncClassCompatibility, compatibility=True
+ ),
+ html.hr(),
+ )
+
+
+@component
+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
+ ),
+ 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)
+
+ def on_click(_):
+ post_request = HttpRequest()
+ post_request.method = "POST"
+ set_request(post_request)
+
+ return html._(
+ html.button(
+ {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click},
+ "Click me",
+ ),
+ view_to_component(views.view_to_component_request, request=request),
+ )
+
+
+@component
+def view_to_component_args():
+ params, set_params = hooks.use_state("false")
+
+ def on_click(_):
+ set_params("")
+
+ return html._(
+ html.button(
+ {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click},
+ "Click me",
+ ),
+ view_to_component(views.view_to_component_args, args=[params]),
+ )
+
+
+@component
+def view_to_component_kwargs():
+ params, set_params = hooks.use_state("false")
+
+ def on_click(_):
+ set_params("")
+
+ return html._(
+ html.button(
+ {"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}),
+ )
diff --git a/tests/test_app/static/django-css-test.css b/tests/test_app/static/django-css-test.css
index 41f98461..40266ebb 100644
--- a/tests/test_app/static/django-css-test.css
+++ b/tests/test_app/static/django-css-test.css
@@ -1,3 +1,3 @@
#django-css button {
- color: rgb(0, 0, 255);
+ color: rgb(0, 0, 255);
}
diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html
index b22f0bd1..bea9893a 100644
--- a/tests/test_app/templates/base.html
+++ b/tests/test_app/templates/base.html
@@ -12,6 +12,14 @@