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#
# 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
dataclass
Pydantic
models
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 defining a mutable default for a class variable can have subtle and unexpected consequences – it will cause component instances to share state which is usually not the intention.
# sentence.py
from django_unicorn.components import UnicornView
# This will cause unexpected consequences
class SentenceView(UnicornView):
words: list[str] = [] # all SentenceView instances will share a reference to one list in memory
word_counts: dict[str, int] = {} # all SentenceView instances will share a reference to one dictionary in memory
def add_word(self, word: str):
...
The correct way to initialize a mutable object.
# sentence.py
from django_unicorn.components import UnicornView
class SentenceView(UnicornView):
words: list[str] # not setting a 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 SentenceView is initialized and mounted
self.word_counts = {} # initialize a new dictionary every time a SentenceView is initialized and mounted
def add_word(self, word: str):
...
list
, dictionaries
, and objects all run into this problem, so be sure to initialize mutable objects 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.
# 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
.
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 to handle how the class should be deserialized.
# 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()
<!-- hello-world.html -->
<div>
<input unicorn:model="author.name" type="text" id="author_name" />
</div>
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
.
Class properties#
template_name#
By default, the component name is used to determine what template should be used. For example, hello_world.HelloWorldView
would by default use unicorn/hello-world.html
. However, you can specify a particular template by setting template_name
in the component.
# hello_world.py
from django_unicorn.components import UnicornView
class HelloWorldView(UnicornView):
template_name = "unicorn/hello-world.html"
template_html#
Template HTML can be defined inline on the component instead of using an external HTML file.
# hello_world.py
from django_unicorn.components import UnicornView
class HelloWorldView(UnicornView):
template_html = """<div>
<div>
Count: {{ count }}
</div>
<button unicorn:click="increment">+</button>
<button unicorn:click="decrement">-</button>
</div>
"""
...
Instance properties#
component_args#
The arguments passed into the component.
<!-- index.html -->
{% unicorn 'hello-arg' 'World' %}
# hello_arg.py
from django_unicorn.components import UnicornView
class HelloArgView(UnicornView):
def mount(self):
assert self.component_args[0] == "World"
component_kwargs#
The keyword arguments passed into the component.
<!-- index.html -->
{% unicorn 'hello-kwarg' hello='World' %}
# hello_kwarg.py
from django_unicorn.components import UnicornView
class HelloKwargView(UnicornView):
def mount(self):
assert self.component_kwargs["hello"] == "World"
request#
The current request
.
# hello_world.py
from django_unicorn.components import UnicornView
class HelloWorldView(UnicornView):
def mount(self):
print("Initial request that rendered the component", self.request)
def test(self):
print("AJAX request that re-renders the component", self.request)
force_render#
Forces the component to render. This is not normally needed for the current component, but is sometimes needed when a child component needs a parent to re-render.
# filter.py
from django_unicorn.components import UnicornView
class FilterView(UnicornView):
search = ""
def updated_search(self, query):
self.parent.load_table()
if query:
self.parent.books = list(filter(lambda f: query.lower() in f.name.lower(), self.parent.books))
# Forces the parent to re-render instead of just the current child component
self.parent.force_render = True
Custom methods#
Defined component instance methods with no arguments (other than self
) are available in the Django template context and can be called like a property.
# states.py
from django_unicorn.components import UnicornView
class StateView(UnicornView):
def all_states(self):
return ["Alabama", "Alaska", "Arizona", ...]
<!-- states.html -->
<div>
<ul>
{% for state in all_states %}
<li>{{ state }}</li>
{% endfor %}
</ul>
</div>
{% endverbatim %}
Tip
If the method is intensive and will be called multiple times, it can be cached with Django’s cached_property
to prevent duplicate API requests or database queries. The method will only be executed once per component rendering.
# states.py
from django.utils.functional import cached_property
from django_unicorn.components import UnicornView
class StateView(UnicornView):
@cached_property
def all_states(self):
return ["Alabama", "Alaska", "Arizona", ...]
Instance methods#
mount()#
Gets called when the component gets initialized or reset.
# hello_world.py
from django_unicorn.components import UnicornView
class HelloWorldView(UnicornView):
name = "original"
def mount(self):
self.name = "mounted"
hydrate()#
Gets called when the component data gets set.
# hello_world.py
from django_unicorn.components import UnicornView
class HelloWorldView(UnicornView):
name = "original"
def hydrate(self):
self.name = "hydrated"
updating(property_name, property_value)#
Gets called before each property that will get set. This can be called multiple times in certain instances, e.g. during a debounce.
updated(property_name, property_value)#
Gets called after each property gets set. This can be called multiple times in certain instances, e.g. during a debounce.
resolved(property_name, property_value)#
Gets called after the specified property gets set. This will only get called once.
updating_{property_name}(property_value)#
Gets called before the specified property gets set. This can be called multiple times in certain instances, e.g. during a debounce.
updated_{property_name}(property_value)#
Gets called after the specified property gets set. This can be called multiple times in certain instances, e.g. during a debounce.
resolved_{property_name}(property_value)#
Gets called after the specified property gets set. This will only get called once.
calling(name, args)#
Gets called before each method that gets called.
called(name, args)#
Gets called after each method gets called.
complete()#
Gets called after all methods have been called.
rendered(html)#
Gets called after the component has been rendered.
parent_rendered(html)#
Gets called after the component’s parent has been rendered (if applicable).
Meta#
Classes that derive from UnicornView
can include a Meta
class that provides some advanced options for the component.
exclude#
By default, all public attributes of the component are included in the context of the Django template and available to JavaScript. One way to protect internal-only data is to prefix the attribute name with _
to indicate it should stay private.
# hello_state.py
from django_unicorn.components import UnicornView
class HelloStateView(UnicornView):
_all_states = (
"Alabama",
"Alaska",
...
"Wisconsin",
"Wyoming",
)
Another way to prevent that data from being available to the component template is to add it to the Meta
class’s exclude
tuple.
# hello_state.py
from django_unicorn.components import UnicornView
class HelloStateView(UnicornView):
all_states = (
"Alabama",
"Alaska",
...
"Wisconsin",
"Wyoming",
)
class Meta:
exclude = ("all_states", )
javascript_exclude#
To allow an attribute to be included in the the context to be used by a Django template, but not exposed to JavaScript, add it to the Meta
class’s javascript_exclude
tuple.
<!-- hello-state.html -->
<div>
{% for state in all_states %}
<div>{{ state }}</div>
{% endfor %}
</div>
# hello_state.py
from django_unicorn.components import UnicornView
class HelloStateView(UnicornView):
all_states = (
"Alabama",
"Alaska",
...
"Wisconsin",
"Wyoming",
)
class Meta:
javascript_exclude = ("all_states", )
safe#
By default, unicorn
HTML encodes updated field values to prevent XSS attacks. You need to explicitly opt-in to allow a field to be returned without being encoded by adding it to the Meta
class’s safe
tuple.
<!-- safe-example.html -->
<div>
<input unicorn:model="something_safe" />
{{ something_safe }}
</div>
# safe_example.py
from django_unicorn.components import UnicornView
class SafeExampleView(UnicornView):
something_safe = ""
class Meta:
safe = ("something_safe", )
Note
A context variable can also be marked as safe
in the template with the normal Django template filter.
<!-- safe-example.html -->
<div>
<input unicorn:model="something_safe" />
{{ something_safe|safe }}
</div>