dabapps / django-forms-dynamic

Resolve form field arguments dynamically when a form is instantiated
BSD 2-Clause "Simplified" License
142 stars 8 forks source link

Support for crispy forms with custom layouts #4

Open mschoettle opened 1 year ago

mschoettle commented 1 year ago

In our project we use django-crispy-forms and define the form layout in the form's __init__.

For the example in this repo it could look as follows (using the Bootstrap 5 template pack) to align the two fields next to each other in one row:

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Column, Row

def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.helper = FormHelper()
        self.helper.form_tag = False

        self.helper.layout = Layout(
            Row(
                Column('make'),
                Column('model'),
            ),
        )

If I want to now make the model field only appear when a make is selected by adding include=lambda form: form['make'].value() to the model field, crispy forms fails because initially the model field is not part of the form ("Could not resolve form field 'model'.").

I don't think it's a bug but I am wondering if there is a way to support this. I suspect it would require additional logic to create the form layout on the go.

j4mie commented 1 year ago

Sorry for the slow reply on this. I don't use django-crispy-forms myself so I don't know much about it, but from looking at the example I think you're right - you'd need to introspect which fields are present in your __init__ and change your layout accordingly.

fordmustang5l commented 1 year ago

Hi! I use both crispy and this package. I don’t (currently) change the layout, but what I have done is set the Model field to be hidden if the Make is blank. Not sure if that’s a suitable workaround

nerdoc commented 11 months ago

You could change the DynamicFormMixin code to the following:

  ...
                else:
                    self.fields[name].widget = forms.HiddenInput()
                    #del self.fields[name]

Hiding a field is better, as it still includes it in the form, and CrispyForms or the validation won't complain. Even better would be additionally removing the "required" attribute.

nerdoc commented 11 months ago

You could add an attribute to DynamicFormMixin (like hide_unincluded_fields = False) to either hide the field (as Hiddeninput), or just disable them.

Even better would be to make the "including" configureable on a per-field basis: the "include" parameter should hide the field, and a new, "disabled" parameter could dynamically disable the field, depending on the lambda outcome.

This would mask the "disabled" parameter of the read field, and would make it dynamic!

nerdoc commented 11 months ago

This is even more complicated, as hiding the field does a good job when just using django-forms-dynamic. But when you are using e.g. HTMX to dynamically change field querysets in the frontend together with crispy forms, it gets messy, as crispy forms adds a wrapper div which contains the label. And hiding just the e.g. input field does not hide the label. Hiding there must be done by adding a display:none to the wrapper div's class. Which would be easier by "d-none" when using Bootstrap. Any ideas?

nerdoc commented 10 months ago

I managed to solve the problem by reloading the whole form using HTMX, and hx-includeing all parameters. This works almost perfect. I put the hx-include="[name=field1],[name=field2],..." at the form tag, as it is inherited. So I can reload the whole crispy form without modifications using a GET request. You just need to make sure that the GET parameters are placed into the fields again, I am using a simple mixin here:

class PrepopulateFormViewMixin:
    """Prepopulates the form with values from GET parameters."""
    def get_initial(self):
        initial = super().get_initial()
        initial.update(clean_dict(self.request.GET.dict()))
        return initial