diff --git a/README.md b/README.md
index 2e575e51..2610985f 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
[](#contributors-)
-[Unicorn](https://www.django-unicorn.com) adds modern reactive component functionality to your Django templates without having to learn a new templating language or fight with complicated JavaScript frameworks. It seamlessly extends Django past its server-side framework roots without giving up all of its niceties or forcing you to re-building your application. With Django Unicorn, you can quickly and easily add rich front-end interactions to your templates, all while using the power of Django.
+[Unicorn](https://www.django-unicorn.com) adds modern reactive component functionality to your Django templates without having to learn a new templating language or fight with complicated JavaScript frameworks. It seamlessly extends Django past its server-side framework roots without giving up all of its niceties or forcing you to rebuild your application. With Django Unicorn, you can quickly and easily add rich front-end interactions to your templates, all while using the power of Django.
## ⚡ Getting started
@@ -61,12 +61,12 @@ urlpatterns = (
### 5. [Create a component](https://www.django-unicorn.com/docs/components/)
-`python manage.py startunicorn COMPONENT_NAME`
+`python manage.py startunicorn myapp COMPONENT_NAME`
`Unicorn` uses the term "component" to refer to a set of interactive functionality that can be put into templates. A component consists of a Django HTML template and a Python view class which contains the backend code. After running the management command, two new files will be created:
-- `your_app/templates/unicorn/COMPONENT_NAME.html` (component template)
-- `your_app/components/COMPONENT_NAME.py` (component view)
+- `myapp/templates/unicorn/COMPONENT_NAME.html` (component template)
+- `myapp/components/COMPONENT_NAME.py` (component view)
### 6. Add the component to your template
@@ -91,7 +91,7 @@ urlpatterns = (
The `unicorn:` attributes bind the element to data and can also trigger methods by listening for events, e.g. `click`, `input`, `keydown`, etc.
```html
-
+
```
-```{warning}
-`Unicorn` requires there to be one root element surrounding the component template.
-```
-
-`unicorn:model` is the magic that ties the input to the backend component. The Django template variable can use any property or method on the component as if they were context variables passed in from a view. The attribute passed into `unicorn:model` refers to the property in the component class and binds them together.
+`unicorn:model` is the magic that ties the input to the backend component. When this component renders, the input element will include a value of "World" because that is the value of the `name` field in the view. It will read "Hello World" below the input element. When a user types "universe" into the input element, the component is re-rendered with the new `name` -- the text will now be "Hello Universe".
```{note}
By default `unicorn:model` updates are triggered by listening to `input` events on the element. To listen for the `blur` event instead, use the [lazy](templates.md#lazy) modifier.
```
-When a user types into the text input, the information is passed to the backend and populates the component class, which is then used to generate the output of the template HTML. The template can use any normal Django templatetags or filters (e.g. the `title` filter above).
-
-## Component sub-folders
-
-Components can also be nested in sub-folders.
-
-```
-unicorn/
- components/
- __init__.py
- hello/
- __init__.py
- world.py
- templates/
- unicorn/
- hello/
- world.html
-```
+## Pass data to a component
-An example of how the above component would be included in a template.
+`args` and `kwargs` can be passed into the `unicorn` templatetag from the outer template. They will be available in the component [`component_args`](views.md#component_args) and [`component_kwargs`](views.md#component_kwargs) instance methods respectively.
```html
-{% unicorn 'hello.world' %}
-```
-
-## Unicorn attributes
-
-Attributes used in component templates usually start with `unicorn:`, however the shortcut `u:` is also supported. So, for example, `unicorn:model` could also be written as `u:model`.
-
-## Supported property types
-
-Properties of the component can be of many different types, including `str`, `int`, `list`, `dictionary`, `Decimal`,[`Django Model`](django-models.md#model), [`Django QuerySet`](django-models.md#queryset), [`dataclass`](https://docs.python.org/3.7/library/dataclasses.html), or `custom classes`.
+{% load unicorn %}
+{% csrf_token %}
-### Property type hints
-
-`Unicorn` will attempt to cast any properties with a `type hint` when the component is hydrated.
+{% unicorn 'hello-world' "Hello" name="World" %}
+```
```python
-# rating.py
+# hello_world.py
from django_unicorn.components import UnicornView
-class RatingView(UnicornView):
- rating: float = 0
+class HelloWorldView(UnicornView):
+ def mount(self):
+ arg = self.component_args[0]
+ kwarg = self.component_kwargs["name"]
- def calculate_percentage(self):
- print(self.rating / 100.0)
+ assert f"{arg} {kwarg}" == "Hello World"
```
-Without `rating: float`, when `calculate_percentage` is called Python will complain with an error message like the following.
+Any variable available in the template context can be passed in as an argument.
-```shell
-TypeError: unsupported operand type(s) for /: 'str' and 'int'`
-```
-
-### Accessing nested fields
+```html
+
+{% load unicorn %}
+{% csrf_token %}
-Fields in a `dictionary` or Django model can be accessed similarly to the Django template language with "dot-notation".
+
+{% unicorn 'hello-world' name=hello.world.name %}
+```
```python
-# hello_world.py
-from django_unicorn.components import UnicornView
-from book.models import Book
-
-class HelloWorldView(UnicornView):
- book = Book.objects.get(title='American Gods')
- book_ratings = {'excellent': {'title': 'American Gods'}}
-```
+# views.py
+from django.shortcuts import render
-```html
-
-
-
-
-
-```
+def index(request):
+ context = {"hello": {"world": {"name": "Galaxy"}}}
-```{note}
-[Django models](django-models.md) has many more details about using Django models in `Unicorn`.
+ return render(request, "index.html", context)
```
-### Django QuerySet
-
-`Django QuerySet` can be referenced similarly to the Django template language in a `unicorn:model`.
+The component view which can use the `name` kwarg.
```python
# hello_world.py
from django_unicorn.components import UnicornView
-from book.models import Book
class HelloWorldView(UnicornView):
- books = Book.objects.all()
-```
-
-```html
-
-
-
-
-```
+ def mount(self):
+ kwarg = self.component_kwargs["name"]
-```{note}
-[Django models](django-models.md#queryset) has many more details about using Django QuerySets in `Unicorn`.
+ assert kwarg == "Galaxy"
```
-### Custom class
-
-Custom classes need to define how they are serialized. If you have access to the object to serialize, you can define a `to_json` method on the object to return a dictionary that can be used to serialize. Inheriting from `unicorn.components.UnicornField` is a quick way to serialize a custom class, but note that it just calls `self.__dict__` under the hood, so it is not doing anything particularly smart.
-
-Another option is to set the `form_class` on the component and utilize Django's built-in forms and widgets to handle how the class should be deserialized. More details are provided in [validation](validation.md).
+## Component key
-```python
-# hello_world.py
-from django_unicorn.components import UnicornView, UnicornField
+If there are multiple of the same components on the page, a `key` kwarg can be passed into the template. For example, `{% unicorn 'hello-world' key='helloWorldKey' %}`. This is useful when a unique reference to a component is required, but it is optional.
-class Author(UnicornField):
- def mount(self):
- self.name = 'Neil Gaiman'
+## Component sub-folders
- # Not needed because inherited from `UnicornField`
- # def to_json(self):
- # return {'name': self.name}
+Components can be nested in sub-folders.
- class HelloWorldView(UnicornView):
- author = Author()
+```
+myapp/
+ components/
+ __init__.py
+ hello/
+ __init__.py
+ world.py
+ templates/
+ myapp/
+ hello/
+ world.html
```
+An example of how the above component would be included in a template.
+
```html
-
-
-
-
-```
+
+{% load unicorn %}
+{% csrf_token %}
-```{danger}
-Never put sensitive data into a public property because that information will publicly available in the HTML source code, unless explicitly prevented with [`javascript_exclude`](advanced.md#javascript_exclude).
+{% unicorn 'hello.world' %}
```
diff --git a/docs/source/django-models.md b/docs/source/django-models.md
index 0fc1d408..c245adc0 100644
--- a/docs/source/django-models.md
+++ b/docs/source/django-models.md
@@ -11,7 +11,7 @@ Using this functionality will serialize your entire model by default and expose
One option is to customize the serialization of the model into a dictionary to only expose the data that should be publicly available.
-Another option is to use [Meta.exclude](advanced.md#exclude) or [Meta.javascript_exclude](advanced.md#javascript_exclude) so those fields are not exposed.
+Another option is to use [Meta.exclude](views.md#exclude) or [Meta.javascript_exclude](views.md#javascript_exclude) so those fields are not exposed.
:::
:::{code} html
diff --git a/docs/source/index.md b/docs/source/index.md
index 06cfdb4b..8e4c3a58 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -8,6 +8,14 @@
self
installation
components
+```
+
+```{toctree}
+:caption: Components
+:maxdepth: 3
+:hidden:
+
+views
templates
actions
child-components
@@ -16,7 +24,7 @@ django-models
```{toctree}
:caption: Features
-:maxdepth: 2
+:maxdepth: 3
:hidden:
direct-view
@@ -28,14 +36,21 @@ partial-updates
polling
visibility
messages
-advanced
+```
+
+```{toctree}
+:caption: Advanced
+:maxdepth: 3
+:hidden:
+
+javascript
queue-requests
custom-morphers
```
```{toctree}
:caption: Misc
-:maxdepth: 2
+:maxdepth: 3
:hidden:
cli
@@ -44,7 +59,7 @@ settings
```{toctree}
:caption: Info
-:maxdepth: 2
+:maxdepth: 3
:hidden:
faq
diff --git a/docs/source/javascript.md b/docs/source/javascript.md
new file mode 100644
index 00000000..fd317984
--- /dev/null
+++ b/docs/source/javascript.md
@@ -0,0 +1,54 @@
+# JavaScript Integration
+
+## Call JavaScript from View
+
+To integrate with other JavaScript functions, view methods can call an arbitrary JavaScript function after it gets rendered.
+
+```html
+
+
+
+
+
+
+
+```
+
+```python
+# call_javascript.py
+from django_unicorn.components import UnicornView
+
+class CallJavascriptView(UnicornView):
+ name = ""
+
+ def mount(self):
+ self.call("hello", "world")
+
+ def hello(self):
+ self.call("hello", self.name)
+```
+
+## Trigger Model Update
+
+Normally when a model element gets changed by a user it will trigger an event which `Unicorn` listens for (either `input` or `blur` depending on if it has a `lazy` modifier). However, when setting an element with JavaScript those events do not fire. `Unicorn.trigger()` provides a way to trigger that event from JavaScript manually.
+
+The first argument to `trigger` is the component name. The second argument is the value for the element's `unicorn:key`.
+
+```html
+
+
+
+
+```
diff --git a/docs/source/templates.md b/docs/source/templates.md
index 340f4d49..dc67b9e4 100644
--- a/docs/source/templates.md
+++ b/docs/source/templates.md
@@ -1,15 +1,32 @@
# Templates
-Templates are just normal Django HTML templates, so anything you could normally do in a Django template will still work, including template tags, filters, loops, if statements, etc.
+Templates are normal Django HTML templates, so anything you could normally do in a Django template will still work, including template tags, filters, loops, if statements, etc.
```{warning}
-`Unicorn` requires there to be one root element surrounding the component template.
-```
+`Unicorn` requires there to be one root element that contains the component HTML. Valid HTML and a wrapper element is required for the DOM diffing algorithm to work correctly, so `Unicorn` will try to log a warning message if they seem invalid.
+
+For example, this is an **invalid** template:
+:::{code} html
+:force: true
+
+Name: {{ name }}
+:::
+
+This template is **valid**:
+:::{code} html
+:force: true
+
+
+ Name: {{ name }}
+
+:::
-```{note}
-To reduce the verbosity of templates, `u:` can be used as a shorthand for any attribute that starts with `unicorn:`. All of the examples in the documentation use `unicorn:` to be explicit, but both are supported.
```
+## Unicorn attributes
+
+`Unicorn` element attributes usually start with `unicorn:`, however the shortcut `u:` is also supported. So, for example, `unicorn:model` could also be written as `u:model`.
+
## Model modifiers
### Lazy
diff --git a/docs/source/validation.md b/docs/source/validation.md
index 9e3204c5..29880408 100644
--- a/docs/source/validation.md
+++ b/docs/source/validation.md
@@ -1,36 +1,8 @@
-# Validation
+# Forms and Validation
`Unicorn` has two options for validation. It can either use the standard Django `forms` infrastructure for re-usability or `ValidationError` can be raised for simpler use-cases.
-## ValidationError
-
-If you do not want to create a form class or you want to specifically target a nested field you can raise a `ValidationError` inside of an action method. The `ValidationError` must be instantiated with a `dict` with the model name as the key and error message as the value. A `code` keyword argument must also be passed in. The typical error codes used are `required` or `invalid`.
-
-```python
-# book_validation_error.py
-from django.utils.timezone import now
-from django_unicorn.components import UnicornView
-
-class BookView(UnicornView):
- book: Book
-
- def publish(self):
- if not self.book.title:
- raise ValidationError({"book.title": "Books must have a title"}, code="required")
-
- self.publish_date = now()
- self.book.save()
-```
-
-```html
-
-
-
-
-
-```
-
-## Django Form
+## Forms
`Unicorn` can use the Django `forms` infrastructure for validation. This means that a form could be re-used between any other Django views and a `Unicorn` component.
diff --git a/docs/source/advanced.md b/docs/source/views.md
similarity index 50%
rename from docs/source/advanced.md
rename to docs/source/views.md
index 95ef62de..da57536a 100644
--- a/docs/source/advanced.md
+++ b/docs/source/views.md
@@ -1,4 +1,186 @@
-# Advanced Views
+# Views
+
+Views contain a class that inherits from `UnicornView` for the component's Python code.
+
+To follow typical naming conventions, the view will convert the component's name to be more Pythonic. For example, if the component name is `hello-world`, the template file name will also be `hello-world.html`. However, the view file name will be `hello_world.py` and it will contain one class named `HelloWorldView`.
+
+This allows `Unicorn` to connect the template and view using convention instead of configuration. Using the `startunicorn` management command is the easiest way to make sure that components are created correctly.
+
+## Example view
+
+```python
+# hello_world.py
+from django_unicorn.components import UnicornView
+
+class HelloWorldView(UnicornView):
+ pass
+```
+
+## Class variables
+
+`Unicorn` will serialize/deserialize view class variables to JSON as needed for interactive parts of the component.
+
+Automatically handled field types:
+- `str`
+- `int`
+- `Decimal`
+- `float`
+- `list`
+- `dictionary`
+- [`Django Model`](django-models.md#model)
+- [`Django QuerySet`](django-models.md#queryset)
+- `dataclass`
+- `Pydantic` models
+- [Custom classes](views.md#custom-class)
+
+### A word of caution about mutable class variables
+
+Be careful when using a default mutable class variables, namely `list`, `dictionary`, and objects. As mentioned in [A Word About Names and Objects](https://docs.python.org/3.8/tutorial/classes.html#tut-object) using a class variable that is mutable can have subtle and unexpected consequences. Using mutable class variables in a field _will_ cause multiple component instances to share state.
+
+```python
+# sentence.py
+from django_unicorn.components import UnicornView
+
+# This will cause unexpected consequences
+class SentenceView(UnicornView):
+ words: list[str] = [] # all component instances will share a reference to one list
+ word_counts: dict[str, int] = {} # all component instances will share a reference to one dictionary
+
+ def add_word(self, word: str):
+ ...
+```
+
+The correct way to initialize a mutable object:
+
+```python
+# sentence.py
+from django_unicorn.components import UnicornView
+
+class SentenceView(UnicornView):
+ words: list[str] # no default value is valid
+ word_counts: dict[str, int] = None # using None for the default is valid
+
+ def mount(self):
+ self.words = [] # initialize a new list every time a component is mounted
+ self.word_counts = {} # initialize a new dictionary every time a component is mounted
+
+ def add_word(self, word: str):
+ ...
+```
+
+`list`, `dictionaries`, and objects will all run into this problem, so be sure to initialize any mutable object in the component's `mount` function.
+
+### Class variable type hints
+
+Type hints on fields help `Unicorn` ensure that the field will always have the appropriate type.
+
+```python
+# rating.py
+from django_unicorn.components import UnicornView
+
+class RatingView(UnicornView):
+ rating: float = 0
+
+ def calculate_percentage(self):
+ assert isinstance(rating, float)
+ print(self.rating / 100.0)
+```
+
+Without the `float` type hint on `rating`, Python will complain that `rating` is a `str`.
+
+### Accessing nested fields
+
+Fields in a `dictionary` or Django model can be accessed similarly to the Django template language with "dot notation".
+
+```python
+# hello_world.py
+from django_unicorn.components import UnicornView
+from book.models import Book
+
+class HelloWorldView(UnicornView):
+ book: Book
+ book_ratings: dict[str[dict[str, str]]]
+
+ def mount(self):
+ book = Book.objects.get(title='American Gods')
+ book_ratings = {'excellent': {'title': 'American Gods'}}
+```
+
+```html
+
+
+
+
+
+```
+
+```{note}
+[Django models](django-models.md) has many more details about using Django models in `Unicorn`.
+```
+
+### Django QuerySet
+
+A Django `QuerySet` can be accessed in a `unicorn:model` with "dot notation".
+
+```python
+# hello_world.py
+from django_unicorn.components import UnicornView
+from book.models import Book
+
+class HelloWorldView(UnicornView):
+ books = Book.objects.none()
+
+ def mount(self):
+ self.books = Book.objects.all()
+```
+
+```html
+
+
+
+
+```
+
+```{note}
+[Django models](django-models.md#queryset) has many more details about using Django QuerySets in `Unicorn`.
+```
+
+### Custom class
+
+Custom classes need to define how they are serialized. If you have access to the object to serialize, you can define a `to_json` method on the object to return a dictionary that can be used to serialize. Inheriting from `unicorn.components.UnicornField` is a quick way to serialize a custom class, but it uses `self.__dict__` under the hood, so it is not doing anything particularly smart.
+
+Another option is to set the `form_class` on the component and utilize [Django's built-in forms and widgets](validation.md) to handle how the class should be deserialized.
+
+```python
+# hello_world.py
+from django_unicorn.components import UnicornView, UnicornField
+
+class Author(UnicornField):
+ def mount(self):
+ self.name = 'Neil Gaiman'
+
+ # Not needed because inherited from `UnicornField`
+ # def to_json(self):
+ # return {'name': self.name}
+
+ class HelloWorldView(UnicornView):
+ author = Author()
+```
+
+```html
+
+
+
+
+```
+
+```{danger}
+Never put sensitive data into a public property because that information will publicly available in the HTML source code, unless explicitly prevented with [`javascript_exclude`](views.md#javascript_exclude).
+```
## Class properties
@@ -70,7 +252,7 @@ class HelloWorldView(UnicornView):
## Custom methods
-Defined component instance methods with no arguments are made available to the Django template context and can be called like a property.
+Defined component instance methods with no arguments (other than `self`) are available in the Django template context and can be called like a property.
```python
# states.py
@@ -272,7 +454,7 @@ class SafeExampleView(UnicornView):
```
````{note}
-A context variable can be marked as `safe` in the template with the normal Django template filter, as well.
+A context variable can also be marked as `safe` in the template with the normal Django template filter.
```html
@@ -282,58 +464,3 @@ A context variable can be marked as `safe` in the template with the normal Djang
```
````
-
-## JavaScript Integration
-
-### Call JavaScript from View
-
-To integrate with other JavaScript functions, view methods can call an arbitrary JavaScript function after it gets rendered.
-
-```html
-
-
-
-
-
-
-
-```
-
-```python
-# call_javascript.py
-from django_unicorn.components import UnicornView
-
-class CallJavascriptView(UnicornView):
- name = ""
-
- def mount(self):
- self.call("hello", "world")
-
- def hello(self):
- self.call("hello", self.name)
-```
-
-### Trigger Model Update
-
-Normally when a model element gets changed by a user it will trigger an event which `Unicorn` listens for (either `input` or `blur` depending on if it has a `lazy` modifier). However, when setting an element with JavaScript those events do not fire. `Unicorn.trigger()` provides a way to trigger that event from JavaScript manually.
-
-The first argument to `trigger` is the component name. The second argument is the value for the element's `unicorn:key`.
-
-```html
-
-
-
-
-```
diff --git a/example/www/templates/www/base.html b/example/www/templates/www/base.html
index c7471f3c..5963d216 100644
--- a/example/www/templates/www/base.html
+++ b/example/www/templates/www/base.html
@@ -38,7 +38,7 @@