Skip to content

Commit

Permalink
Add model update before a callMethod if it is a lazy model. Add `is_v…
Browse files Browse the repository at this point in the history
…alid` to base component for a quicker check for validation errors. Handle validation errors slightly better when there are multiple models.
  • Loading branch information
adamghill committed Sep 17, 2020
1 parent 1747e8c commit 3f7ac76
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 22 deletions.
49 changes: 31 additions & 18 deletions django_unicorn/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def __init__(self, **kwargs):
if "request" in kwargs:
self.setup(kwargs["request"])

self._validate_called = False
self.errors = {}
self._set_default_template_name()
self._set_caches()
Expand Down Expand Up @@ -358,6 +359,9 @@ def get_context_data(self, **kwargs):

return context

def is_valid(self, model_names: List = None) -> bool:
return len(self.validate(model_names).keys()) == 0

def validate(self, model_names: List = None) -> Dict:
"""
Validates the data using the `form_class` set on the component.
Expand All @@ -367,30 +371,35 @@ def validate(self, model_names: List = None) -> Dict:
"""
# TODO: Handle form.non_field_errors()?

if self._validate_called:
return self.errors

self._validate_called = True

data = self._attributes()
form = self._get_form(data)

if form:
form_errors = form.errors.get_json_data(escape_html=True)

if model_names is not None:
# This code is confusing, but handles this use-case:
# the component has two models, one that starts with an error and one
# that is valid. Validating the valid one should not show an error for
# the invalid one. Only after the invalid field is updated, should the
# error show up and persist, even after updating the valid form.
if self.errors:
keys_to_remove = []

for key, value in self.errors.items():
if key in form_errors:
self.errors[key] = value
else:
keys_to_remove.append(key)

for key in keys_to_remove:
self.errors.pop(key)
# This code is confusing, but handles this use-case:
# the component has two models, one that starts with an error and one
# that is valid. Validating the valid one should not show an error for
# the invalid one. Only after the invalid field is updated, should the
# error show up and persist, even after updating the valid form.
if self.errors:
keys_to_remove = []

for key, value in self.errors.items():
if key in form_errors:
self.errors[key] = value
else:
keys_to_remove.append(key)

for key in keys_to_remove:
self.errors.pop(key)

if model_names is not None:
for key, value in form_errors.items():
if key in model_names:
self.errors[key] = value
Expand Down Expand Up @@ -514,6 +523,7 @@ def _is_public(self, name: str) -> bool:
"calling",
"called",
"validate",
"is_valid",
"get_frontend_context_variables",
"errors",
"updated",
Expand Down Expand Up @@ -551,7 +561,9 @@ def create(
key = f"{component_name}-{component_id}"

if key in constructed_views_cache:
return constructed_views_cache[key]
component = constructed_views_cache[key]
component._validate_called = False
return component

if component_name in views_cache:
component = views_cache[component_name](
Expand Down Expand Up @@ -582,6 +594,7 @@ def create(
key = f"{component_name}-{component_id}"
constructed_views_cache[key] = component

component._validate_called = False
return component

locations = []
Expand Down
5 changes: 5 additions & 0 deletions django_unicorn/static/js/unicorn.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,11 @@ const Unicorn = (() => {
this.actionEvents[eventType].forEach((element) => {
// Use isSameNode (not isEqualNode) because we want to check the nodes reference the same object
if (targetElement.el.isSameNode(element.el)) {
if (!isEmpty(targetElement.model) && targetElement.model.isLazy) {
const action = { type: "syncInput", payload: { name: targetElement.model.name, value: targetElement.getValue() } };
this.actionQueue.push(action);
}

if (element.action.key) {
if (element.action.key === toKebabCase(event.key)) {
this.callMethod(element.action.name);
Expand Down
12 changes: 10 additions & 2 deletions example/unicorn/components/todo.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from django_unicorn.components import UnicornView
from django import forms


class TodoForm(forms.Form):
task = forms.CharField(min_length=3, max_length=10, required=True)


class TodoView(UnicornView):
form_class = TodoForm

task = ""
tasks = []

def add(self):
self.tasks.append(self.task)
self.task = ""
if self.is_valid():
self.tasks.append(self.task)
self.task = ""
7 changes: 5 additions & 2 deletions example/unicorn/templates/unicorn/todo.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<div>
<div>{{ unicorn.errors.task.0.message }}</div>

<div class="field has-addons">
<p class="control">
<input class="input" type="text" unicorn:model.lazy="task" placeholder="New task" id="task"></input>
<input class="input" type="text" unicorn:model.lazy="task" unicorn:keyup.enter="add" placeholder="New task" id="task"></input>
</p>
<p class="control">
<button unicorn:click="add" class="button is-info">Add</button>&nbsp;&nbsp;<button unicorn:click="reset" class="button is-danger" {% if not tasks %}disabled{% endif %} style="border-radius: 4px;">Clear all tasks</button>
<button unicorn:click="add" class="button is-info">Add</button>&nbsp;&nbsp;
<button unicorn:click="reset" class="button is-danger" {% if not tasks %}disabled{% endif %} style="border-radius: 4px;">Clear all tasks</button>
</p>
</div>

Expand Down

0 comments on commit 3f7ac76

Please sign in to comment.