-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTMX example for Class Based View, and more generic #10
Comments
I experimented a bit more with it, and am asking myself:
But this is only the tip of the iceberg. In your simple example, this is easy to achieve: 2 URL endpoints, 2 simple views with 3 lines. But in reality, you can't write these views in a bigger application. You need access control constraints, validation, etc. Here CBV are much better on the long term (sorry, no flame war intended). My form e.g. has one |
As I use CBV, I wrote an example, which works generically with CBV, crispy forms and HTMX. First, you need a mixin that uses GET data to prepopulate your form (This is just a separate class, to be reused elsewhery in my code too. It could be incorporated into the next class below, too): class PrepopulateFormMixin(FormView):
def get_initial(self):
"""Returns the initial data for the form."""
initial = super().get_initial()
initial.update(self.request.GET.dict())
return initial Then, I wrote another mixin that creates the class DynamicFormViewMixin(PrepopulateFormMixin):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["context"] = self.get_initial()
kwargs["context"].update(self.request.GET.dict())
return kwargs So you can use the def __init(self, *args, **kwargs):
self.fields["trigger_field"].widget.attrs.update({
"hx-trigger": "change", # not strictly necessary
"hx-get": ".",
"hx-target": "#div_id_dependent_field",
"hx-select": "#div_id_dependent_field",
}) This works perfectly. You could include these mixins in django-forms-dynamic if you want, and add some docs, and many people (like me) get happier ;-) Maybe the trigger field htmx action could be added in a descriptive way too, I'm working on that... |
The code above reduces that view to class MakeAndModelView(DynamicFormViewMixin, forms.Form): # or other generic view
...
initial = {"make": "audi"}
def __init(self,**kwargs):
self.fields["make"].widget.attrs.update(
{
"hx-trigger": "change",
"hx-get": ".",
"hx-target": "#div_id_model",
"hx-select": "#div_id_model",
}
) If you add a parameter like "dependency" to DynamicField which marks the field the DynamicField is dependent on, your business logic could add this widget attr automatically. What it doesn't solve, is when a trigger field should update more than one other fields. e.g. when the "make" field, when "Audi" is selected, triggers
This won't work, as HTMX only |
I would like to respond with an example that might help, but I only code function based views. Would you still want a writeup? |
No... in a bigger project I'm trying to get often reusable parts, and here are CBVs for me the best way (no flame war intended). I am using this meanwhile (shortened): class DynamicHtmxFormMixin(DynamicFormMixin):
"""
Mixin class for creating dynamic forms with HTMX.
[...]
"""
class Meta:
trigger_fields: list[str] = []
update_url: str = "."
trigger = "change"
def __init__(self, form_id, *args, **kwargs):
self.form_id = form_id
super().__init__(*args, **kwargs)
if not hasattr(self.Meta, "trigger_fields"):
raise AttributeError(
f"{self.__class__.__name__}.Meta has no 'trigger_fields' attribute."
)
trigger = self.Meta.trigger if hasattr(self.Meta, "trigger") else "changed"
include_list = ",".join(
[
f"[name={field}]"
for field in self.fields
if not self.fields[field].widget.is_hidden
]
)
self.context["form_attrs"] = f"hx-include={include_list}"
for field_name in self.Meta.trigger_fields:
field = self.fields.get(field_name)
if field:
field.widget.attrs.update(
{
"hx-trigger": trigger,
"hx-get": self.get_update_url(),
"hx-target": f"#{self.form_id}",
"hx-select": f"#{self.form_id}",
# "hx-push-url": "true",
"hx-swap": "outerHTML",
}
)
def get_update_url(self):
if hasattr(self.Meta, "update_url"):
return self.Meta.update_url
else:
return "."
def fields_required(self, fields: str | list[str], msg: str = None) -> None:
"""Helper method used for conditionally marking fields as required.
Can be called in your form's clean() method.
[...]
Credits go to
https://www.fusionbox.com/blog/detail/creating-conditionally-required-fields-in-django-forms/577/
"""
if type(fields) is str:
fields = [fields]
for field in fields:
if not self.cleaned_data.get(field, ""):
msg = (
forms.ValidationError(msg)
if msg
else forms.ValidationError(_("This field is required."))
)
self.add_error(field, msg) This works reasonably well. It just needs to get |
You provide one example with HTMX, where a model field is dependant on a make field and changes it's queryset accoding to the selected make. This is simple and comprehensive.
But in real life, there are more complicated structures. e.g., depending on the selection of a field, many other fields may change (or be included or not) in the form.
While the lambda functions work and can mirror the needed behaviour of each field, the widget-tweaks'
render_field
templatetag can not easily fetch one form field using HTMX, it must get all of the fields.As more interdependencies may occur in dynamic forms, it would be generally a good behaviour to get the whole form with each change and reload the form - this does not costs much and simplifies the whole workflow - in theory.
In practice, I can't wrap my head around this, I sat here for hours trying to solve it, no chance.
While the hx-attrs get attached correctly and "work", the View is always executed without the passed GET param (e.g.
?user_type=2
)And so, the Radioselect always stays the same.
I know, this is more a question, and not a bug. Or a docs request, to add documentation for HTMX forms.
Maybe you could provide a little example on how to use HTMX for more complicated forms...? Thank you very much in advance.
BTW, I use crispy-forms too (as it's the only way of keeping code maintainable in bigger projects).
The text was updated successfully, but these errors were encountered: