diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
new file mode 100644
index 00000000..a87f2f1c
--- /dev/null
+++ b/.github/workflows/publish-docs.yml
@@ -0,0 +1,15 @@
+name: Publish Docs
+on:
+ push:
+ branches:
+ - main
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.x
+ - run: pip install requirements/build-docs.txt
+ - run: mkdocs gh-deploy --force
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0eeb2f49..6a696e82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,11 @@
All notable changes to this project will be documented in this file.
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+
+
+
## [Unreleased]
-- Nothing (yet)
+- Nothing (yet)
+
+## [1.0.0] - 2022-05-22
+
+### Added
+
+- Django-specific hooks! `use_websocket`, `use_scope`, and `use_location` are now available within the `django_idom.hooks` module.
+- Documentation has been placed into a formal docs webpage.
+- Logging for when a component fails to import, or if no components were found within Django.
+
+### Changed
+
+- `idom_component` template tag has been renamed to `component`
+- Bumped the minimum IDOM version to 0.38.0
+
+### Removed
+
+- `websocket` parameter for components has been removed. Functionally, it is replaced with `django_idom.hooks.use_websocket`.
## [0.0.5] - 2022-04-04
### Changed
-- Bumped the minimum IDOM version to 0.37.2
+- Bumped the minimum IDOM version to 0.37.2
### Fixed
-- ModuleNotFoundError: No module named `idom.core.proto` caused by IDOM 0.37.2
+- ModuleNotFoundError: No module named `idom.core.proto` caused by IDOM 0.37.2
## [0.0.4] - 2022-03-05
### Changed
-- Bumped the minimum IDOM version to 0.37.1
+- Bumped the minimum IDOM version to 0.37.1
## [0.0.3] - 2022-02-19
### Changed
-- Bumped the minimum IDOM version to 0.36.3
+- Bumped the minimum IDOM version to 0.36.3
## [0.0.2] - 2022-01-30
### Added
-- Ability to declare the HTML class of the top-level component `div`
-- `name = ...` parameter to IDOM HTTP paths for use with `django.urls.reverse()`
-- Cache versioning to automatically invalidate old web module files from the cache backend
-- Automatic pre-population of the IDOM component registry
-- Type hinting for `IdomWebsocket`
+- Ability to declare the HTML class of the top-level component `div`
+- `name = ...` parameter to IDOM HTTP paths for use with `django.urls.reverse()`
+- Cache versioning to automatically invalidate old web module files from the cache backend
+- Automatic pre-population of the IDOM component registry
+- Type hinting for `IdomWebsocket`
### Changed
-- Fetching web modules from disk and/or cache is now fully async
-- Static files are now contained within a `django_idom/` parent folder
-- Upgraded IDOM to version `0.36.0`
-- Minimum Django version required is now `4.0`
-- Minimum Python version required is now `3.8`
+- Fetching web modules from disk and/or cache is now fully async
+- Static files are now contained within a `django_idom/` parent folder
+- Upgraded IDOM to version `0.36.0`
+- Minimum Django version required is now `4.0`
+- Minimum Python version required is now `3.8`
### Removed
-- `IDOM_WEB_MODULES_PATH` has been replaced with Django `include(...)`
-- `IDOM_WS_MAX_RECONNECT_DELAY` has been renamed to `IDOM_WS_MAX_RECONNECT_TIMEOUT`
-- `idom_web_modules` cache backend has been renamed to `idom`
+- `IDOM_WEB_MODULES_PATH` has been replaced with Django `include(...)`
+- `IDOM_WS_MAX_RECONNECT_DELAY` has been renamed to `IDOM_WS_MAX_RECONNECT_TIMEOUT`
+- `idom_web_modules` cache backend has been renamed to `idom`
### Fixed
-- Increase test timeout values to prevent false positives
-- Windows compatibility for building Django-IDOM
+- Increase test timeout values to prevent false positives
+- Windows compatibility for building Django-IDOM
### Security
-- Fixed potential directory travesal attack on the IDOM web modules URL
+- Fixed potential directory travesal attack on the IDOM web modules URL
## [0.0.1] - 2021-08-18
### Added
-- Support for IDOM within the Django
+- Support for IDOM within the Django
-[unreleased]: https://github.com/idom-team/django-idom/compare/0.0.2...HEAD
+[unreleased]: https://github.com/idom-team/django-idom/compare/1.0.0...HEAD
+[1.0.0]: https://github.com/idom-team/django-idom/compare/0.0.5...1.0.0
[0.0.5]: https://github.com/idom-team/django-idom/compare/0.0.4...0.0.5
[0.0.4]: https://github.com/idom-team/django-idom/compare/0.0.3...0.0.4
[0.0.3]: https://github.com/idom-team/django-idom/compare/0.0.2...0.0.3
diff --git a/README.md b/README.md
index e24d5a07..0e72bd99 100644
--- a/README.md
+++ b/README.md
@@ -1,184 +1,79 @@
-# Django IDOM · [](https://github.com/idom-team/django-idom/actions?query=workflow%3ATest) [](https://pypi.python.org/pypi/django-idom) [](https://github.com/idom-team/django-idom/blob/main/LICENSE)
-
-`django-idom` allows Django to integrate with [IDOM](https://github.com/idom-team/idom), a reactive Python web framework for building **interactive websites without needing a single line of Javascript**.
-
-**You can try IDOM now in a Jupyter Notebook:**
-
-
-
-
-# Quick Example
-
-## `example_app/components.py`
-
-This is where you'll define your [IDOM](https://github.com/idom-team/idom) components. Ultimately though, you should
-feel free to organize your component modules as you wish. Any components created will ultimately be referenced
-by Python dotted path in `your-template.html`.
-
-```python
-from idom import component, html
-from django_idom import IdomWebsocket
-
-# Components are CamelCase by ReactJS convention
-@component
-def Hello(websocket: IdomWebsocket, greeting_recipient: str):
- return html.h1(f"Hello {greeting_recipient}!")
-```
-
-## [`example_app/templates/your-template.html`](https://docs.djangoproject.com/en/dev/topics/templates/)
-
-In your templates, you may add IDOM components into your HTML by using the `idom_component`
-template tag. This tag requires the dotted path to the component function.
-
-Additonally, you can pass in keyworded arguments into your component function.
-
-In context this will look a bit like the following...
-
-```jinja
-{% load idom %}
-
-
-
-
- Example Project
-
-
-
- {% idom_component "my_django_project.example_app.components.Hello" greeting_recipient="World" %}
-
-
-```
-
-# Installation
-
-Install `django-idom` via pip.
+
-```bash
-pip install django-idom
-```
-
-You'll also need to modify a few files in your Django project.
-
-## [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/)
-
-In your settings you'll need to add `channels` and `django_idom` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS).
-
-```python
-INSTALLED_APPS = [
- ...,
- "channels",
- "django_idom",
-]
-
-# Ensure ASGI_APPLICATION is set properly based on your project name!
-ASGI_APPLICATION = "my_django_project.asgi.application"
-```
-
-**Optional:** You can also configure IDOM settings.
+# Django IDOM · [](https://github.com/idom-team/django-idom/actions?query=workflow%3ATest) [](https://pypi.python.org/pypi/django-idom) [](https://github.com/idom-team/django-idom/blob/main/LICENSE)
-```python
-# If "idom" cache is not configured, then we'll use "default" instead
-CACHES = {
- "idom": {"BACKEND": ...},
-}
+
+
-# Maximum seconds between two reconnection attempts that would cause the client give up.
-# 0 will disable reconnection.
-IDOM_WS_MAX_RECONNECT_TIMEOUT: int = 604800
+Django-IDOM connects your project to a ReactJS frontend, allowing you to create **interactive websites without needing JavaScript!**
-# The URL for IDOM to serve websockets
-IDOM_WEBSOCKET_URL: str = "idom/"
-```
+Following ReactJS styling, web elements are combined into [reusable "components"](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/your-first-components/index.html#parametrizing-components). These components can utilize [hooks](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html) and [events](https://idom-docs.herokuapp.com/docs/guides/adding-interactivity/responding-to-events/index.html#async-event-handlers) to create infinitely complex web pages.
-## [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/)
+When needed, IDOM can [use components directly from NPM](https://idom-docs.herokuapp.com/docs/guides/escape-hatches/javascript-components.html#dynamically-loaded-components). For additional flexibility, components can also be [fully developed in JavaScript](https://idom-docs.herokuapp.com/docs/guides/escape-hatches/javascript-components.html#custom-javascript-components).
-Add IDOM HTTP paths to your `urlpatterns`.
+Any Python web framework with Websockets can support IDOM. See below for what frameworks are supported out of the box.
-```python
-from django.urls import include, path
+| Supported Frameworks | Supported Frameworks (External) |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [`Flask`, `FastAPI`, `Sanic`, `Tornado`](https://idom-docs.herokuapp.com/docs/guides/getting-started/installing-idom.html#officially-supported-servers) | [`Django`](https://github.com/idom-team/django-idom), [`Plotly-Dash`](https://github.com/idom-team/idom-dash), [`Jupyter`](https://github.com/idom-team/idom-jupyter) |
-urlpatterns = [
- path("idom/", include("django_idom.http.urls")),
- ...
-]
-```
+
-## [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/)
+---
-Register IDOM's websocket using `IDOM_WEBSOCKET_PATH`.
+# At a Glance
-_Note: If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html)._
+## `my_app/components.py`
-```python
+
-import os
-from django.core.asgi import get_asgi_application
+You'll need a file to define your [IDOM](https://github.com/idom-team/idom) components. We recommend creating a `components.py` file within your chosen **Django app** to start out.
-# Ensure DJANGO_SETTINGS_MODULE is set properly based on your project name!
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_django_project.settings")
-django_asgi_app = get_asgi_application()
+
+
-from channels.auth import AuthMiddlewareStack
-from channels.routing import ProtocolTypeRouter, URLRouter
-from channels.sessions import SessionMiddlewareStack
-from django_idom import IDOM_WEBSOCKET_PATH
+```python title="components.py"
+from idom import component, html
-application = ProtocolTypeRouter(
- {
- "http": django_asgi_app,
- "websocket": SessionMiddlewareStack(
- AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH]))
- ),
- }
-)
+@component
+def HelloWorld(recipient: str):
+ return html.h1(f"Hello {recipient}!")
```
-# Developer Guide
+
-If you plan to make code changes to this repository, you'll need to install the
-following dependencies first:
+## [`my_app/templates/my-template.html`](https://docs.djangoproject.com/en/dev/topics/templates/)
-- [NPM](https://docs.npmjs.com/try-the-latest-stable-version-of-npm) for
- installing and managing Javascript
-- [ChromeDriver](https://chromedriver.chromium.org/downloads) for testing with
- [Selenium](https://www.seleniumhq.org/)
+
-Once done, you should clone this repository:
+In your **Django app**'s HTML located within your `templates` folder, you can now embed your IDOM component using the `component` template tag. Within this tag, you will need to type in your dotted path to the component function as the first argument.
-```bash
-git clone https://github.com/idom-team/django-idom.git
-cd django-idom
-```
-
-Then, by running the command below you can:
+Additonally, you can pass in keyword arguments into your component function. For example, after reading the code below, pay attention to how the function definition for `HelloWorld` (_in the previous example_) accepts a `recipient` argument.
-- Install an editable version of the Python code
+
+
-- Download, build, and install Javascript dependencies
-
-```bash
-pip install -e . -r requirements.txt
+```jinja title="my-template.html"
+{% load idom %}
+
+
+
+ {% component "example_project.my_app.components.HelloWorld" recipient="World" %}
+
+
```
-Finally, to verify that everything is working properly, you'll want to run the test suite.
+
-## Running The Tests
+---
-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:
+# Resources
-```
-nox -s test
-```
+
-To run the tests using a headless browser:
+Follow the links below to find out more about this project.
-```
-nox -s test -- --headless
-```
+- [Try it Now](https://mybinder.org/v2/gh/idom-team/idom-jupyter/main?urlpath=lab/tree/notebooks/introduction.ipynb) - Check out IDOM in a Jupyter Notebook.
+- [Documentation](https://idom-team.github.io/django-idom) - Learn how to install, run, and use IDOM.
+- [Community Forum](https://github.com/idom-team/idom/discussions) - Ask questions, share ideas, and show off projects.
+
diff --git a/docs/changelog/index.md b/docs/changelog/index.md
new file mode 100644
index 00000000..a6e2f878
--- /dev/null
+++ b/docs/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/contribute/django-idom.md
new file mode 100644
index 00000000..4284ef66
--- /dev/null
+++ b/docs/contribute/django-idom.md
@@ -0,0 +1,35 @@
+???+ tip "Looking to contribute features that are not Django specific?"
+
+ Everything within the `django-idom` repository must be specific to Django integration. Check out the [IDOM Core documentation](https://idom-docs.herokuapp.com/docs/about/contributor-guide.html) to contribute general features, such as: components, hooks, events, etc.
+
+If you plan to make code changes to this repository, you'll need to install the following dependencies first:
+
+- [Python 3.8+](https://www.python.org/downloads/)
+- [Git](https://git-scm.com/downloads)
+- [NPM](https://docs.npmjs.com/try-the-latest-stable-version-of-npm) for installing and managing Javascript
+- [ChromeDriver](https://chromedriver.chromium.org/downloads) for testing with [Selenium](https://www.seleniumhq.org/)
+
+Once done, you should clone this repository:
+
+```bash
+git clone https://github.com/idom-team/django-idom.git
+cd django-idom
+```
+
+Then, by running the command below you can:
+
+- Install an editable version of the Python code
+- Download, build, and install Javascript dependencies
+
+```bash
+pip install -e . -r requirements.txt
+```
+
+Finally, to verify that everything is working properly, you can manually run the development webserver.
+
+```bash
+cd tests
+python manage.py runserver
+```
+
+Navigate to `http://127.0.0.1:8000` to see if the tests are rendering correctly.
diff --git a/docs/contribute/docs.md b/docs/contribute/docs.md
new file mode 100644
index 00000000..231fb603
--- /dev/null
+++ b/docs/contribute/docs.md
@@ -0,0 +1,28 @@
+If you plan to make changes to this documentation, you'll need to install the following dependencies first:
+
+- [Python 3.8+](https://www.python.org/downloads/)
+- [Git](https://git-scm.com/downloads)
+
+Once done, you should clone this repository:
+
+```bash
+git clone https://github.com/idom-team/django-idom.git
+cd django-idom
+```
+
+Then, by running the command below you can:
+
+- Install an editable version of the documentation
+- Self-host a test server for the documentation
+
+```bash
+pip install -r ./requirements/build-docs.txt --upgrade
+```
+
+Finally, to verify that everything is working properly, you can manually run the docs preview webserver.
+
+```bash
+mkdocs serve
+```
+
+Navigate to `http://127.0.0.1:8000` to view a preview of the documentation.
diff --git a/docs/contribute/running-tests.md b/docs/contribute/running-tests.md
new file mode 100644
index 00000000..287b9301
--- /dev/null
+++ b/docs/contribute/running-tests.md
@@ -0,0 +1,11 @@
+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/hooks.md b/docs/features/hooks.md
new file mode 100644
index 00000000..a024e816
--- /dev/null
+++ b/docs/features/hooks.md
@@ -0,0 +1,51 @@
+# Django Hooks
+
+## Use Websocket
+
+You can fetch the Django Channels websocket at any time by using `use_websocket`.
+
+```python title="components.py"
+from idom import component, html
+from django_idom.hooks import use_websocket
+
+@component
+def MyComponent():
+ my_websocket = use_websocket()
+ return html.div(my_websocket)
+```
+
+---
+
+## Use Scope
+
+This is a shortcut that returns the Websocket's `scope`.
+
+```python title="components.py"
+from idom import component, html
+from django_idom.hooks import use_scope
+
+@component
+def MyComponent():
+ my_scope = use_scope()
+ return html.div(my_scope)
+```
+
+---
+
+## Use Location
+
+??? info "This hook's behavior will be changed in a future update"
+
+ This hook will eventually be updated to return the client's current webpage URL. This will come in alongside our built-in [Single Page Application (SPA) support](https://github.com/idom-team/idom/issues/569).
+
+This is a shortcut that returns the Websocket's `path`.
+
+```python title="components.py"
+from idom import component, html
+from django_idom.hooks import use_location
+
+@component
+def MyComponent():
+ my_location = use_location()
+ return html.div(my_location)
+```
diff --git a/docs/features/templatetag.md b/docs/features/templatetag.md
new file mode 100644
index 00000000..26a2f4ad
--- /dev/null
+++ b/docs/features/templatetag.md
@@ -0,0 +1,84 @@
+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="" %}
+
+
+
+??? warning "Do not use context variables for the IDOM component name"
+
+ Our pre-processor relies on the template tag containing a string.
+
+ **Do not** use a Django context variable for the path string. Failure to follow this warning will result in a performance penalty and also jankiness when using the Django autoreloader.
+
+ For example, **do not** do the following:
+
+ ```python title="views.py"
+ def example_view():
+ context_vars = {"DontDoThis": "example_project.my_app.components.HelloWorld"}
+ return render(request, "my-template.html", context_vars)
+ ```
+
+ ```jinja title="my-template.html"
+
+ {% component DontDoThis recipient="World" %}
+
+
+ {% component "example_project.my_app.components.HelloWorld" recipient="World" %}
+ ```
+
+
+
+
+??? info "Reserved keyword arguments: `class` and `key`"
+
+ For this template tag, there are two reserved keyword arguments: `class` and `key`
+
+ - `class` allows you to apply a HTML class to the top-level component div. This is useful for styling purposes.
+ - `key` allows you to force the component to use a [specific key value](https://idom-docs.herokuapp.com/docs/guides/understanding-idom/why-idom-needs-keys.html?highlight=key). You typically won't need to set this.
+
+ ```jinja title="my-template.html"
+ ...
+ {% component "example.components.MyComponent" class="my-html-class" key=123 %}
+ ...
+ ```
+
+
+
+
+??? question "Can I use multiple components on one page?"
+
+ You can add as many components to a webpage as needed by using the template tag multiple times. Retrofitting legacy sites to use IDOM will typically involve many components on one page.
+
+ ```jinja
+ {% load idom %}
+
+
+
+ {% component "example_project.my_app.components.HelloWorld" recipient="World" %}
+ {% component "example_project.my_app_2.components.ClassComponent" class="bold small-font" %}
+ {% component "example_project.my_app_3.components.SimpleComponent" %}
+
+
+ ```
+
+ But keep in mind, in scenarios where you are trying to create a Single Page Application (SPA) within Django, you will only have one central component within your `#!html ` tag.
+
+
+
+
+??? question "Can I use positional arguments instead of keyword arguments?"
+
+ You can only pass in **keyword arguments** within the template tag. Due to technical limitations, **positional arguments** are not supported at this time.
+
+
+
+
+??? question "What is a "template tag"?"
+
+ You can think of template tags as Django's way of allowing you to run Python code within your HTML. Django IDOM uses a `#!jinja {% component ... %}` template tag to perform it's magic.
+
+ 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="" %}
+
+
diff --git a/docs/getting-started/create-component.md b/docs/getting-started/create-component.md
new file mode 100644
index 00000000..ca6e2a18
--- /dev/null
+++ b/docs/getting-started/create-component.md
@@ -0,0 +1,17 @@
+???+ summary
+
+ Create a component function using our decorator.
+
+---
+
+{% include-markdown "../../README.md" start="" end="" %}
+
+{% include-markdown "../../README.md" start="" end="" %}
+
+??? question "What should I name my IDOM files and functions?"
+
+ You have full freedom in naming/placement of your files and functions.
+
+ You should determine the best way to sort your Python modules and component functions to fit your needs.
+
+ Ultimately, components are referenced by Python dotted path in `my-template.html` (_see next step_). So, at minimum this path needs to be valid to Python's `importlib`.
diff --git a/docs/getting-started/initial-steps.md b/docs/getting-started/initial-steps.md
new file mode 100644
index 00000000..daef5d4d
--- /dev/null
+++ b/docs/getting-started/initial-steps.md
@@ -0,0 +1,19 @@
+???+ summary
+
+ Set up a Django Project with at least one app.
+
+---
+
+If you've reached this point, you should have already [installed Django-IDOM](../installation/index.md) through the previous steps.
+
+For the examples within this section, we will assume you've placed the files [generated by `startapp`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app) directly into your **Django project** folder. This is common for small projects.
+
+??? question "How do I organize my Django project for IDOM?"
+
+ Django-IDOM has no project structure requirements. Organize everything as you wish, just like any Django project.
+
+??? question "I've never used Django, what do I need to learn?"
+
+ {% include-markdown "../installation/index.md" start="" end="" %}
+
+ Afterwards, make sure to follow the [Django-IDOM installation instructions](../installation/index.md).
diff --git a/docs/getting-started/learn-more.md b/docs/getting-started/learn-more.md
new file mode 100644
index 00000000..abbc1099
--- /dev/null
+++ b/docs/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 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/getting-started/reference-component.md b/docs/getting-started/reference-component.md
new file mode 100644
index 00000000..af7dd77c
--- /dev/null
+++ b/docs/getting-started/reference-component.md
@@ -0,0 +1,23 @@
+???+ summary
+
+ Decide where the component will be displayed by using our template tag.
+
+---
+
+{% include-markdown "../../README.md" start="" end="" %}
+
+{% include-markdown "../../README.md" start="" end="" %}
+
+{% include-markdown "../features/templatetag.md" start="" end="" %}
+
+{% include-markdown "../features/templatetag.md" start="" end="" %}
+
+{% include-markdown "../features/templatetag.md" start="" end="" %}
+
+{% include-markdown "../features/templatetag.md" start="" end="" %}
+
+{% include-markdown "../features/templatetag.md" start="" end="" %}
+
+??? question "Where is my templates folder?"
+
+ If you do not have a `templates` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications).
diff --git a/docs/getting-started/render-view.md b/docs/getting-started/render-view.md
new file mode 100644
index 00000000..03fb8d6f
--- /dev/null
+++ b/docs/getting-started/render-view.md
@@ -0,0 +1,39 @@
+???+ summary
+
+ Select your template containing an IDOM component, and render it using a Django view.
+
+---
+
+We will assume you've [created a Django View](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) before, but here's a simple example below.
+
+Within your **Django app**'s `views.py` file, you'll need to create a function to render the HTML template containing your IDOM components.
+
+In this example, we will create a view that renders `my-template.html` (_from the previous step_).
+
+```python title="views.py"
+from django.shortcuts import render
+
+def index(request):
+ return render(request, "my-template.html")
+```
+
+We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view).
+
+```python title="urls.py"
+from django.urls import path
+from example_project.my_app import views
+
+urlpatterns = [
+ path("example/", views.index),
+]
+```
+
+Now, navigate to `http://127.0.0.1:8000/example/`. If you copy-pasted the component from the previous example, you will now see your component display "Hello World".
+
+??? question "Which urls.py do I add my views to?"
+
+ For simple Django projects, you can easily add all of your views directly into the **Django project**'s `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file.
+
+ Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**.
+
+ Then, within your **Django project**'s `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/) to link it all together.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..39874a5f
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,17 @@
+---
+hide:
+ - navigation
+ - toc
+---
+
+{% include-markdown "../README.md" start="" end="" %}
+
+## It's React 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/installation/index.md
new file mode 100644
index 00000000..cdf9820b
--- /dev/null
+++ b/docs/installation/index.md
@@ -0,0 +1,105 @@
+
+
+These docs assumes you have created [a basic **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which also involves generating/installing at least one **Django app**. If not, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_.
+
+
+
+## Install from PyPI
+
+```bash
+pip install django-idom
+```
+
+You'll also need to modify a few files in your Django project...
+
+---
+
+## Configure [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/)
+
+In your settings you'll need to add `django_idom` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS).
+
+```python title="settings.py"
+INSTALLED_APPS = [
+ "django_idom",
+ ...
+]
+```
+
+??? warning "Enabling ASGI on Django (Required)"
+
+ Django-IDOM requires ASGI in order to use Websockets.
+
+ If you haven't [enabled ASGI](https://channels.readthedocs.io/en/stable/installation.html) on your Django project yet, you'll need to add `channels` to `INSTALLED_APPS` and set your `ASGI_APPLICATION` variable.
+ ```python title="settings.py"
+ INSTALLED_APPS = [
+ "channels",
+ ...
+ ]
+ ASGI_APPLICATION = "example_project.asgi.application"
+ ```
+
+??? note "Configure IDOM settings (Optional)"
+
+ Below are a handful of values you can change within `settings.py` to modify the behavior of IDOM.
+
+ ```python title="settings.py"
+ # If "idom" cache is not configured, then we'll use "default" instead
+ CACHES = {
+ "idom": {"BACKEND": ...},
+ }
+
+ # Maximum seconds between two reconnection attempts that would cause the client give up.
+ # 0 will disable reconnection.
+ IDOM_WS_MAX_RECONNECT_TIMEOUT = 604800
+
+ # The URL for IDOM to serve websockets
+ IDOM_WEBSOCKET_URL = "idom/"
+ ```
+
+---
+
+## Configure [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/)
+
+Add IDOM HTTP paths to your `urlpatterns`.
+
+```python title="urls.py"
+from django.urls import include, path
+
+urlpatterns = [
+ path("idom/", include("django_idom.http.urls")),
+ ...
+]
+```
+
+---
+
+## Configure [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/)
+
+Register IDOM's Websocket using `IDOM_WEBSOCKET_PATH`.
+
+```python title="asgi.py"
+import os
+from django.core.asgi import get_asgi_application
+
+# Ensure DJANGO_SETTINGS_MODULE is set properly based on your project name!
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
+django_asgi_app = get_asgi_application()
+
+from channels.auth import AuthMiddlewareStack
+from channels.routing import ProtocolTypeRouter, URLRouter
+from channels.sessions import SessionMiddlewareStack
+from django_idom import IDOM_WEBSOCKET_PATH
+
+application = ProtocolTypeRouter(
+ {
+ "http": django_asgi_app,
+ "websocket": SessionMiddlewareStack(
+ AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH]))
+ ),
+ }
+)
+```
+
+??? question "Where is my asgi.py?"
+
+ If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html).
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..487f21ae
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,69 @@
+---
+nav:
+ - Home: index.md
+ - Installation: installation/index.md
+ - Getting Started:
+ - 1. Initial Steps: getting-started/initial-steps.md
+ - 2. Create a Component: getting-started/create-component.md
+ - 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:
+ - Hooks: features/hooks.md
+ - Template Tag: features/templatetag.md
+ - Contribute:
+ - Code: contribute/django-idom.md
+ - Docs: contribute/docs.md
+ - Running Tests: contribute/running-tests.md
+ - Changelog: changelog/index.md
+
+theme:
+ name: material
+ palette:
+ - scheme: slate
+ toggle:
+ icon: material/toggle-switch
+ name: Switch to light mode
+ primary: deep-orange
+ accent: deep-orange
+ - scheme: default
+ toggle:
+ icon: material/toggle-switch-off-outline
+ name: Switch to dark mode
+ primary: black
+ features:
+ - navigation.instant
+ - navigation.tracking
+ - navigation.tabs
+ - toc.integrate
+ - navigation.top
+
+ icon:
+ repo: fontawesome/brands/github
+
+markdown_extensions:
+ - toc:
+ permalink: true
+ - pymdownx.emoji:
+ emoji_index: !!python/name:materialx.emoji.twemoji
+ emoji_generator: !!python/name:materialx.emoji.to_svg
+ - pymdownx.highlight
+ - pymdownx.superfences
+ - pymdownx.details
+ - pymdownx.inlinehilite
+ - admonition
+ - attr_list
+
+plugins:
+ - search
+ - include-markdown
+ - git-revision-date-localized:
+ fallback_to_build_date: true
+
+site_name: Django IDOM Docs
+site_author: Archmonger
+site_description: React for Django developers.
+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
diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt
new file mode 100644
index 00000000..ce3fba84
--- /dev/null
+++ b/requirements/build-docs.txt
@@ -0,0 +1,4 @@
+mkdocs
+mkdocs-git-revision-date-localized-plugin
+mkdocs-material
+mkdocs-include-markdown-plugin
\ No newline at end of file
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
index 98683e1a..7b9f9420 100644
--- a/requirements/pkg-deps.txt
+++ b/requirements/pkg-deps.txt
@@ -1,3 +1,3 @@
-channels <4.0.0
-idom >=0.37.2, <0.38.0
-aiofile >=3.0, <4.0
+channels >=3.0.0
+idom >=0.38.0, <0.39.0
+aiofile >=3.0
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index eb1e19d8..af2dec38 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -1,6 +1,7 @@
+from . import hooks
from .websocket.consumer import IdomWebsocket
from .websocket.paths import IDOM_WEBSOCKET_PATH
-__version__ = "0.0.5"
-__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket"]
+__version__ = "1.0.0"
+__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks"]
diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py
new file mode 100644
index 00000000..5d088223
--- /dev/null
+++ b/src/django_idom/hooks.py
@@ -0,0 +1,39 @@
+from dataclasses import dataclass
+from typing import Awaitable, Callable, Dict, Optional, Type, Union
+
+from idom.backend.types import Location
+from idom.core.hooks import Context, create_context, use_context
+
+
+@dataclass
+class IdomWebsocket:
+ scope: dict
+ close: Callable[[Optional[int]], Awaitable[None]]
+ disconnect: Callable[[int], Awaitable[None]]
+ view_id: str
+
+
+WebsocketContext: Type[Context[Union[IdomWebsocket, None]]] = create_context(
+ None, "WebSocketContext"
+)
+
+
+def use_location() -> Location:
+ """Get the current route as a string"""
+ # TODO: Use the browser's current page, rather than the WS route
+ scope = use_scope()
+ search = scope["query_string"].decode()
+ return Location(scope["path"], f"?{search}" if search else "")
+
+
+def use_scope() -> Dict:
+ """Get the current ASGI scope dictionary"""
+ return use_websocket().scope
+
+
+def use_websocket() -> IdomWebsocket:
+ """Get the current IdomWebsocket object"""
+ websocket = use_context(WebsocketContext)
+ if websocket is None:
+ raise RuntimeError("No websocket. Are you running with a Django server?")
+ return websocket
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index 28def13e..5707733d 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -14,7 +14,7 @@
@register.inclusion_tag("idom/component.html")
-def idom_component(_component_id_, **kwargs):
+def component(_component_id_, **kwargs):
_register_component(_component_id_)
class_ = kwargs.pop("class", "")
diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py
index 178995c9..81783013 100644
--- a/src/django_idom/utils.py
+++ b/src/django_idom/utils.py
@@ -1,3 +1,4 @@
+import contextlib
import logging
import os
import re
@@ -11,7 +12,7 @@
from django_idom.config import IDOM_REGISTERED_COMPONENTS
-COMPONENT_REGEX = re.compile(r"{% *idom_component ((\"[^\"']*\")|('[^\"']*')).*?%}")
+COMPONENT_REGEX = re.compile(r"{% *component +((\"[^\"']*\")|('[^\"']*'))(.*?)%}")
_logger = logging.getLogger(__name__)
@@ -36,6 +37,7 @@ def _register_component(full_component_name: str) -> None:
) from error
IDOM_REGISTERED_COMPONENTS[full_component_name] = component
+ _logger.debug("IDOM has registered component %s", full_component_name)
class ComponentPreloader:
@@ -70,15 +72,12 @@ def _get_paths(self) -> Set:
"""Obtains a set of all template directories."""
paths = set()
for loader in self._get_loaders():
- try:
+ with contextlib.suppress(ImportError, AttributeError, TypeError):
module = import_module(loader.__module__)
get_template_sources = getattr(module, "get_template_sources", None)
if get_template_sources is None:
get_template_sources = loader.get_template_sources
paths.update(smart_str(origin) for origin in get_template_sources(""))
- except (ImportError, AttributeError, TypeError):
- pass
-
return paths
def _get_templates(self, paths: Set) -> Set:
@@ -91,7 +90,7 @@ def _get_templates(self, paths: Set) -> Set:
os.path.join(root, name)
for name in files
if not name.startswith(".")
- and any(fnmatch(name, "*%s" % glob) for glob in extensions)
+ and any(fnmatch(name, f"*{glob}") for glob in extensions)
)
return templates
@@ -100,7 +99,7 @@ def _get_components(self, templates: Set) -> Set:
"""Obtains a set of all IDOM components by parsing HTML templates."""
components = set()
for template in templates:
- try:
+ with contextlib.suppress(Exception):
with open(template, "r", encoding="utf-8") as template_file:
match = COMPONENT_REGEX.findall(template_file.read())
if not match:
@@ -108,16 +107,29 @@ def _get_components(self, templates: Set) -> Set:
components.update(
[group[0].replace('"', "").replace("'", "") for group in match]
)
- except Exception:
- pass
-
+ if not components:
+ _logger.warning(
+ "\033[93m"
+ "IDOM did not find any components! "
+ "You are either not using any IDOM components, "
+ "using the template tag incorrectly, "
+ "or your HTML templates are not registered with Django."
+ "\033[0m"
+ )
return components
def _register_components(self, components: Set) -> None:
"""Registers all IDOM components in an iterable."""
for component in components:
try:
+ _logger.info("IDOM preloader has detected component %s", component)
_register_component(component)
- _logger.info("IDOM has registered component %s", component)
except Exception:
- _logger.warning("IDOM failed to register component %s", component)
+ _logger.error(
+ "\033[91m"
+ "IDOM failed to register component '%s'! "
+ "This component path may not be valid, "
+ "or an exception may have occurred while importing."
+ "\033[0m",
+ component,
+ )
diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py
index 8f773a38..aa988afa 100644
--- a/src/django_idom/websocket/consumer.py
+++ b/src/django_idom/websocket/consumer.py
@@ -2,30 +2,22 @@
import asyncio
import json
import logging
-from dataclasses import dataclass
-from typing import Any, Awaitable, Callable, Optional
+from typing import Any
from urllib.parse import parse_qsl
from channels.auth import login
from channels.db import database_sync_to_async as convert_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
-from idom.core.dispatcher import dispatch_single_view
from idom.core.layout import Layout, LayoutEvent
+from idom.core.serve import serve_json_patch
from django_idom.config import IDOM_REGISTERED_COMPONENTS
+from django_idom.hooks import IdomWebsocket, WebsocketContext
_logger = logging.getLogger(__name__)
-@dataclass
-class IdomWebsocket:
- scope: dict
- close: Callable[[Optional[int]], Awaitable[None]]
- disconnect: Callable[[int], Awaitable[None]]
- view_id: str
-
-
class IdomAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer):
"""Communicates with the browser to perform actions on-demand."""
@@ -70,7 +62,7 @@ async def _run_dispatch_loop(self):
socket = IdomWebsocket(self.scope, self.close, self.disconnect, view_id)
try:
- component_instance = component_constructor(socket, **component_kwargs)
+ component_instance = component_constructor(**component_kwargs)
except Exception:
_logger.exception(
f"Failed to construct component {component_constructor} "
@@ -80,8 +72,8 @@ async def _run_dispatch_loop(self):
self._idom_recv_queue = recv_queue = asyncio.Queue()
try:
- await dispatch_single_view(
- Layout(component_instance),
+ await serve_json_patch(
+ Layout(WebsocketContext(component_instance, value=socket)),
self.send_json,
recv_queue.get,
)
diff --git a/src/django_idom/websocket/paths.py b/src/django_idom/websocket/paths.py
index 0d2920e0..f337c83e 100644
--- a/src/django_idom/websocket/paths.py
+++ b/src/django_idom/websocket/paths.py
@@ -6,8 +6,9 @@
IDOM_WEBSOCKET_PATH = path(
- IDOM_WEBSOCKET_URL + "/", IdomAsyncWebsocketConsumer.as_asgi()
+ f"{IDOM_WEBSOCKET_URL}/", IdomAsyncWebsocketConsumer.as_asgi()
)
+
"""A URL path for :class:`IdomAsyncWebsocketConsumer`.
Required in order for IDOM to know the websocket path.
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index d3452efb..2efd878c 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -1,13 +1,15 @@
import idom
+import django_idom
+
@idom.component
-def HelloWorld(websocket):
+def HelloWorld():
return idom.html.h1({"id": "hello-world"}, "Hello World!")
@idom.component
-def Button(websocket):
+def Button():
count, set_count = idom.hooks.use_state(0)
return idom.html.div(
idom.html.button(
@@ -22,7 +24,7 @@ def Button(websocket):
@idom.component
-def ParametrizedComponent(websocket, x, y):
+def ParametrizedComponent(x, y):
total = x + y
return idom.html.h1({"id": "parametrized-component", "data-value": total}, total)
@@ -32,5 +34,40 @@ def ParametrizedComponent(websocket, x, y):
@idom.component
-def SimpleBarChart(websocket):
+def SimpleBarChart():
return VictoryBar()
+
+
+@idom.component
+def UseWebsocket():
+ 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(
+ {"id": "use-websocket", "data-success": success},
+ idom.html.hr(),
+ f"UseWebsocket: {ws}",
+ idom.html.hr(),
+ )
+
+
+@idom.component
+def UseScope():
+ scope = django_idom.hooks.use_scope()
+ success = len(scope) >= 10 and scope["type"] == "websocket"
+ return idom.html.div(
+ {"id": "use-scope", "data-success": success},
+ f"UseScope: {scope}",
+ idom.html.hr(),
+ )
+
+
+@idom.component
+def UseLocation():
+ location = django_idom.hooks.use_location()
+ success = bool(location)
+ return idom.html.div(
+ {"id": "use-location", "data-success": success},
+ f"UseLocation: {location}",
+ idom.html.hr(),
+ )
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
index 29eadffa..fbacac81 100644
--- a/tests/test_app/settings.py
+++ b/tests/test_app/settings.py
@@ -121,3 +121,20 @@
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
+
+# Logging
+LOGGING = {
+ "version": 1,
+ "disable_existing_loggers": False,
+ "handlers": {
+ "console": {
+ "class": "logging.StreamHandler",
+ },
+ },
+ "loggers": {
+ "django_idom": {
+ "handlers": ["console"],
+ "level": "DEBUG",
+ },
+ },
+}
diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html
index 5b2311d2..0c52a8cc 100644
--- a/tests/test_app/templates/base.html
+++ b/tests/test_app/templates/base.html
@@ -12,11 +12,13 @@
IDOM Test Page
- {% idom_component "test_app.components.HelloWorld" class="hello-world" %}
- {% idom_component "test_app.components.Button" class="button" %}
- {% idom_component "test_app.components.ParametrizedComponent" class="parametarized-component" x=123 y=456 %}
-
- {% idom_component "test_app.components.SimpleBarChart" class="simple-bar-chart" %}
+ {% component "test_app.components.HelloWorld" class="hello-world" %}
+ {% component "test_app.components.Button" class="button" %}
+ {% component "test_app.components.ParametrizedComponent" class="parametarized-component" x=123 y=456 %}
+ {% component "test_app.components.SimpleBarChart" %}
+ {% component "test_app.components.UseWebsocket" %}
+ {% component "test_app.components.UseScope" %}
+ {% component "test_app.components.UseLocation" %}
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py
index cb10cca9..fbfda829 100644
--- a/tests/test_app/tests/test_components.py
+++ b/tests/test_app/tests/test_components.py
@@ -5,7 +5,7 @@
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
-from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support.wait import WebDriverWait
# These tests are broken on Windows due to Selenium
@@ -47,6 +47,18 @@ def test_component_from_web_module(self):
)
)
+ def test_use_websocket(self):
+ element = self.driver.find_element_by_id("use-websocket")
+ self.assertEqual(element.get_attribute("data-success"), "true")
+
+ def test_use_scope(self):
+ element = self.driver.find_element_by_id("use-scope")
+ self.assertEqual(element.get_attribute("data-success"), "true")
+
+ def test_use_location(self):
+ element = self.driver.find_element_by_id("use-location")
+ self.assertEqual(element.get_attribute("data-success"), "true")
+
def make_driver(page_load_timeout, implicit_wait_timeout):
options = webdriver.ChromeOptions()
diff --git a/tests/test_app/tests/test_regex.py b/tests/test_app/tests/test_regex.py
index 3e418f1d..c0942dba 100644
--- a/tests/test_app/tests/test_regex.py
+++ b/tests/test_app/tests/test_regex.py
@@ -6,22 +6,23 @@
class RegexTests(TestCase):
def test_component_regex(self):
for component in {
- r'{%idom_component "my.component"%}',
- r"{%idom_component 'my.component'%}",
- r'{% idom_component "my.component" %}',
- r"{% idom_component 'my.component' %}",
- r'{% idom_component "my.component" class="my_thing" %}',
- r'{% idom_component "my.component" class="my_thing" attr="attribute" %}',
+ r'{%component "my.component"%}',
+ r'{%component "my.component"%}',
+ r"{%component 'my.component'%}",
+ r'{% component "my.component" %}',
+ r"{% component 'my.component' %}",
+ r'{% component "my.component" class="my_thing" %}',
+ r'{% component "my.component" class="my_thing" attr="attribute" %}',
}:
self.assertRegex(component, COMPONENT_REGEX)
for fake_component in {
r'{% not_a_real_thing "my.component" %}',
- r"{% idom_component my.component %}",
- r"""{% idom_component 'my.component" %}""",
- r'{ idom_component "my.component" }',
- r'{{ idom_component "my.component" }}',
- r"idom_component",
+ r"{% component my.component %}",
+ r"""{% component 'my.component" %}""",
+ r'{ component "my.component" }',
+ r'{{ component "my.component" }}',
+ r"component",
r"{%%}",
r" ",
r"",