From 3f7ac7661037e79e74ac965d7c211f6ed4bfc83e Mon Sep 17 00:00:00 2001 From: Adam Hill Date: Wed, 16 Sep 2020 23:05:25 -0400 Subject: [PATCH] Add model update before a callMethod if it is a lazy model. Add `is_valid` to base component for a quicker check for validation errors. Handle validation errors slightly better when there are multiple models. --- django_unicorn/components.py | 49 +++++++++++++-------- django_unicorn/static/js/unicorn.js | 5 +++ example/unicorn/components/todo.py | 12 ++++- example/unicorn/templates/unicorn/todo.html | 7 ++- 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/django_unicorn/components.py b/django_unicorn/components.py index 5a277e53..2713cb66 100644 --- a/django_unicorn/components.py +++ b/django_unicorn/components.py @@ -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() @@ -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. @@ -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 @@ -514,6 +523,7 @@ def _is_public(self, name: str) -> bool: "calling", "called", "validate", + "is_valid", "get_frontend_context_variables", "errors", "updated", @@ -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]( @@ -582,6 +594,7 @@ def create( key = f"{component_name}-{component_id}" constructed_views_cache[key] = component + component._validate_called = False return component locations = [] diff --git a/django_unicorn/static/js/unicorn.js b/django_unicorn/static/js/unicorn.js index 951d7114..4255c214 100644 --- a/django_unicorn/static/js/unicorn.js +++ b/django_unicorn/static/js/unicorn.js @@ -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); diff --git a/example/unicorn/components/todo.py b/example/unicorn/components/todo.py index 71675878..6a5a22f0 100644 --- a/example/unicorn/components/todo.py +++ b/example/unicorn/components/todo.py @@ -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 = "" diff --git a/example/unicorn/templates/unicorn/todo.html b/example/unicorn/templates/unicorn/todo.html index 5ed343c0..95d9750a 100644 --- a/example/unicorn/templates/unicorn/todo.html +++ b/example/unicorn/templates/unicorn/todo.html @@ -1,10 +1,13 @@
+
{{ unicorn.errors.task.0.message }}
+

- +

-    +    +