diff --git a/README.md b/README.md index 2e575e51..2610985f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#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 - +
@@ -118,7 +118,7 @@ The `unicorn:` attributes bind the element to data and can also trigger methods ``` ```python -# ../components/todo.py +# todo.py from django_unicorn.components import UnicornView from django import forms diff --git a/django_unicorn/components/unicorn_template_response.py b/django_unicorn/components/unicorn_template_response.py index b23cb608..6868d974 100644 --- a/django_unicorn/components/unicorn_template_response.py +++ b/django_unicorn/components/unicorn_template_response.py @@ -13,6 +13,8 @@ from django_unicorn.errors import ( MissingComponentElementError, MissingComponentViewElementError, + MultipleRootComponentElementError, + NoRootComponentElementError, ) from django_unicorn.settings import get_minify_html_enabled, get_script_location from django_unicorn.utils import generate_checksum, sanitize_html @@ -59,6 +61,25 @@ def is_html_well_formed(html: str) -> bool: return len(stack) == 0 +def assert_has_single_wrapper_element(root_element: Tag, component_name: str) -> None: + # Check that the root element has at least one child + try: + next(root_element.descendants) + except StopIteration: + raise NoRootComponentElementError( + f"The '{component_name}' component does not appear to have one root element." + ) from None + + # Check that there is not more than one root element + parent_element = root_element.parent + tag_count = len([c for c in parent_element.children if isinstance(c, Tag)]) + + if tag_count > 1: + raise MultipleRootComponentElementError( + f"The '{component_name}' component appears to have multiple root elements." + ) from None + + class UnsortedAttributes(HTMLFormatter): """ Prevent beautifulsoup from re-ordering attributes. @@ -122,6 +143,12 @@ def render(self): # despite https://thehftguy.com/2020/07/28/making-beautifulsoup-parsing-10-times-faster/ soup = BeautifulSoup(content, features="html.parser") root_element = get_root_element(soup) + + try: + assert_has_single_wrapper_element(root_element, self.component.component_name) + except (NoRootComponentElementError, MultipleRootComponentElementError) as ex: + logger.warning(ex) + root_element["unicorn:id"] = self.component.component_id root_element["unicorn:name"] = self.component.component_name root_element["unicorn:key"] = self.component.component_key diff --git a/django_unicorn/errors.py b/django_unicorn/errors.py index 9735fa45..1ff83f05 100644 --- a/django_unicorn/errors.py +++ b/django_unicorn/errors.py @@ -32,5 +32,13 @@ class MissingComponentViewElementError(Exception): pass +class NoRootComponentElementError(Exception): + pass + + +class MultipleRootComponentElementError(Exception): + pass + + class ComponentNotValidError(Exception): pass diff --git a/docs/source/changelog.md b/docs/source/changelog.md index 0433d8a0..592aeca3 100644 --- a/docs/source/changelog.md +++ b/docs/source/changelog.md @@ -231,7 +231,7 @@ ** Breaking changes ** -- responses will be HTML encoded going forward (to explicitly opt-in to previous behavior use [safe](advanced.md#safe)) +- responses will be HTML encoded going forward (to explicitly opt-in to previous behavior use [safe](views.md#safe)) [All changes since 0.35.3](https://github.com/adamghill/django-unicorn/compare/0.35.3...0.36.0). @@ -250,7 +250,7 @@ ## v0.35.0 -- [Trigger](advanced.md#trigger-model-update) an `input` or `blur` event for a model element from JavaScript. +- [Trigger](javascript.md#trigger-model-update) an `input` or `blur` event for a model element from JavaScript. - [Visibility](visibility.md) event with `unicorn:visible` attribute. **Breaking changes** @@ -374,17 +374,17 @@ ## v0.21.0 - Bug fix: Prevent disabled polls from firing at all. -- Support [`Decimal` field type](components.md#supported-property-types). -- Support [`dataclass` field type](components.md#supported-property-types). -- Use [type hints](components.md#property-type-hints) to cast fields to primitive Python types if possible. +- Support [`Decimal` field type](views.md#class-variables). +- Support [`dataclass` field type](views.md#class-variables). +- Use [type hints](views.md#class-variable-type-hints) to cast fields to primitive Python types if possible. [All changes since 0.20.0](https://github.com/adamghill/django-unicorn/compare/0.20.0...0.21.0). ## v0.20.0 -- Add ability to exclude component view properties from JavaScript to reduce the amount of data initially rendered to the page with [`javascript_exclude`](advanced.md#javascript_exclude). -- Add [`complete`](advanced.md#complete), [`rendered`](advanced.md#renderedhtml), [`parent_rendered`](advanced.md#parent_renderedhtml) component hooks. -- Call [JavaScript functions](advanced.md#javascript-integration) from a component view's method. +- Add ability to exclude component view properties from JavaScript to reduce the amount of data initially rendered to the page with [`javascript_exclude`](views.md#javascript_exclude). +- Add [`complete`](views.md#complete), [`rendered`](views.md#renderedhtml), [`parent_rendered`](views.md#parent_renderedhtml) component hooks. +- Call [JavaScript functions](javascript.md) from a component view's method. [All changes since 0.19.0](https://github.com/adamghill/django-unicorn/compare/0.19.0...0.20.0). @@ -525,8 +525,8 @@ ## v0.10.0 -- Add support for [passing kwargs](components.md#component-arguments) into the component on the template -- Provide access to the [current request](advanced.md#request) in the component's methods +- Add support for [passing kwargs](components.md#pass-data-to-a-component) into the component on the template +- Provide access to the [current request](views.md#request) in the component's methods [All changes since 0.9.4](https://github.com/adamghill/django-unicorn/compare/0.9.4...0.10.0). @@ -554,7 +554,7 @@ - [Loading states](loading-states.md) for improved UX. - `$event` [special argument](actions.md#events) for `actions`. -- `u` [unicorn attribute](components.md#unicorn-attributes). +- `u` [unicorn attribute](templates.md#unicorn-attributes). - `APPS` [setting](settings.md#apps) for determing where to look for components. - Add support for parent elements for non-db models. - Fix: Handle if `Meta` doesn't exist for db models. @@ -628,7 +628,7 @@ - [Realtime validation](validation.md) of a Unicorn model. - [Polling](polling.md) for component updates. -- [More component hooks](advanced.md) +- [More component hooks](views.md) [All changes since 0.5.0](https://github.com/adamghill/django-unicorn/compare/0.5.0...0.6.0). @@ -652,7 +652,7 @@ ## v0.3.0 -- Add [mount hook](advanced.md#mount). +- Add [mount hook](views.md#mount). - Add [reset](actions.md#reset) action. - Remove lag when typing fast in a text input and overall improved performance. - Better error handling for exceptional cases. diff --git a/docs/source/components.md b/docs/source/components.md index 240d3a35..5d5e2e97 100644 --- a/docs/source/components.md +++ b/docs/source/components.md @@ -1,84 +1,55 @@ # Components -`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` with specific tags and a Python `view` class which provides the backend code for the template. +`Unicorn` uses the term "component" to refer to a set of interactive functionality. A component consists of a Python `view` class which provides the backend code and a Django HTML `template`. ## Create a component The easiest way to create your first component is to run the `startunicorn` Django management command after `Unicorn` is installed. -The first argument to `startunicorn` is the Django app to add your component to. Every argument after is a new component to create a template and view for. +The first argument to `startunicorn` is the Django app to add your component to. Every argument after the first is the name of a component to create. ```shell -# Create `hello-world` and `hello-magic` components in a `unicorn` app -python manage.py startunicorn unicorn hello-world hello-magic +# Create `hello-world` and `hello-magic` components in `myapp` +python manage.py startunicorn myapp hello-world hello-magic ``` -```{warning} -If the app does not already exist, `startunicorn` will ask if it should call `startapp` to create a new application. However, make sure to add the app name to `INSTALLED_APPS` in your Django settings file (normally `settings.py`). Otherwise Django will not be able to find the newly created component templates. +That would create a file structure like the following: ``` - -```{note} -Explicitly set which apps `Unicorn` looks in for components with the [APPS setting](settings.md#apps). Otherwise, all `INSTALLED_APPS` will be searched for components. +myapp/ + components/ + __init__.py + hello_world.py + hello_magic.py + templates/ + myapp/ + hello-world.html + hello-magic.html ``` -Then, add a `{% unicorn 'hello-world' %}` templatetag into the template where you want to load the new component. - ```{warning} -Make sure that there is a `{% csrf_token %}` rendered by the HTML template that includes the component to prevent cross-site scripting attacks while using `Unicorn`. +Make sure that the app name specified is in the `INSTALLED_APPS` list in your Django settings file (normally `settings.py`). ``` -## Component key - -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. - -## Component arguments +## Use a component -`args` and `kwargs` can be passed into the `unicorn` templatetag from the template. They will be available in the component [`component_args`](advanced.md#component_args) and [`component_kwargs`](advanced.md#component_kwargs) methods respectively. +Components usually reside in a regular Django template (unless it is a [direct view](direct-view.md)). The component is "included" (similar to the `include` templatetag) with a `unicorn` templatetag. ```html -{% unicorn 'hello-world' "Hello" name="World" %} -``` +{% load unicorn %} -```python -# hello_world.py -from django_unicorn.components import UnicornView + +{% csrf_token %} -class HelloWorldView(UnicornView): - def mount(self): - arg = self.component_args[0] - kwarg = self.component_kwargs["name"] - - assert f"{arg} {kwarg}" == "Hello World" + +{% unicorn 'hello-world' %} ``` -Regular Django template variables can also be passed in as an argument as long as it is available in the template context. - -```html - -{% unicorn 'hello-world' name=hello.world.name %} -``` +## A basic component -```python -# views.py -from django.shortcuts import render - -def index(request): - context = {"hello": {"world": {"name": "Galaxy"}}} - return render(request, "index.html", context) -``` - -```python -class HelloWorldView(UnicornView): - def mount(self): - kwarg = self.component_kwargs["name"] - - assert kwarg == "Galaxy" -``` - -## Example component - -A basic example component could consist of the following template and class. +An example component consists of the following Python view class and HTML template. ```python # hello_world.py @@ -96,154 +67,97 @@ class HelloWorldView(UnicornView):
``` -```{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 @@

django-unicorn