What we are going to explain below is how to link the behaviour of one field to others in a GenModelForm.

To set conditional behaviour in your fields, the trade-off is that you must tune the fields using AngularJS code. Since form rendering is not a CPU-bound process, there is no problem with spending a little time rewriting the fields. To do it, we will use a Custom Widget inherited from your desired Form Widget.

Create a custom Widget

In your app, create a file named widgets.py that contains your custom widget OptionaTextArea that inherits from TextArea. So OptionalTextArea will do the same job as TextArea right now:

from django import forms

class OptionalTextArea(forms.widgets.Textarea):
    pass

Override a Widget in your form

Now, let’s replace the default widget with our custom OptionalTextArea. When you need to change the default HTML element used for a form field in a Django ModelForm, you can override the widgets in the form’s Meta class.

  • To define the widgets dictionary, inside the inner Meta class of your GenModelForm, create a widgets dictionary.
  • Map field names to widgets: in this dictionary, map the names of the fields you want to customize to either:
    • An instance of the desired widget class (e.g., OptionalTextarea(attrs={‘cols’: 80, ‘rows’: 20}))
    • The widget class itself (e.g., OptionalTextarea)

Let’s say you have an Author model and want the bio field to be an OptionalTextArea instead of the default TextArea input.

from codenerix.forms import GenModelForm
from myapp.models import Author
from myapp.widgets import OptionalTextArea

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title', 'birth_date', 'bio')
        widgets = {
            'bio': OptionalTextArea(attrs={'cols': 80, 'rows': 20}),
        }

In this example, the name bio in the form will now be rendered as an OptionalTextArea with 80 columns and 20 rows.  

Relax the constraints from the GenModelForm

Before writing the solution, we must ensure that your TextField is not mandatory. This can be done inside your model (it will become optional in your form). The main reason is that you are allowing this field not to be filled, so the model should not force us to fill it. In this way, the GenModelForm won’t fail when this field is empty.

Customize the OptionalTextArea widget

Now it is time to work on our OptionalTextArea widget, which is done in 2 steps:

  1. Inherit from TextArea to get the typical behaviour from it.
  2. Override the render() method to request the HTML to the TextArea widget through the super() call and then rewrite the HTML as needed. Take into account that the default front end is written with AngularJS, so any code added must conform to that framework.

The source code will look like:

from django import forms

class OptionalTextArea(forms.widgets.Textarea):

    def render(self, *args, **kwargs):

        # Get html
        html = super().render(*args, **kwargs)

        # Transform html
        html = html.replace(
            " id=",
            "ng-required='(selector_field!=undefined)&&(selector_field==\"ABC")||(isNaN(any_other_field))' "
            "ng-invalid='(this.value==undefined)||(this.value.length<=10) '"
            " id=",
        )

        print(html)  # For debugging

        return html

The code explained

Here, the render method is overridden to customize how the text area is rendered in HTML. This method is responsible for generating the widget’s HTML output.

The html.replace() function modifies the generated HTML. Specifically, it looks for the string ” id=” within the HTML and replaces it with additional AngularJS attributes (ng-required and ng-invalid) followed by ” id=”.
AngularJS Directives:

  • ng-required: this directive dynamically sets the required attribute on the text area based on a condition. The condition here checks if the selector_field is defined and equal to “ABC” or if another field (any_other_field) is not a number (isNaN).
  • ng-invalid: this directive dynamically sets the invalid attribute based on whether the text area’s value is undefined or has a length less than or equal to 10 characters.
  • The ” id=” part of the original HTML is kept intact, so the only change is the insertion of the AngularJS directives before it.

You should also override the clean_field() process on your form to check whether the selector has a specific value. This will ensure that you produce a ValidationError when the OptionalTextArea is empty or doesn’t meet the conditions in the front end.

Debugging the HTML generated

Since AngularJS already render the HTML source code, what you see in the browser may not be what you have returned from the render function. To keep track of your changes and what the browser is getting, you can print out the HTML variable to see its content on the Django console. Comment out this print() when you are done editing.

DynamicSelect

It happens that CODENERIX is coming with StaticSelect, DynamicSelect, MultiStaticSelect and MultiDynamicSelect, which help to produce selectors.

  • StaticSelect and MultiStaticSelect are the usual selectors for which content is sent by the server when the form is rendered. MultiStaticSelect allows several items to be selected; those are rendered using “Chips“.
  • DynamicSelect and MultiDynamicSelect are new selectors that render content in real-time; once you write in the search box, the server items are requested from the server using the introduced text in the search box. Those are very convenient when talking about a massive list of items inside the selectors because they drastically reduce the overload of the server, rendering many items that may not be used. Instead, you can define how they work using GenForeignKey. This particular kind of selector can send the values from other fields to the server when requesting items so the served items are aware of the context of the form (e.g., a select which Cities depend on a Country selector when a Country is selected to one specific Country, the items returned by the Cities DynamicSelect is reduced to those from that Country). This kind of selector can also overwrite the value from other fields in the same form when the item is selected. We can say they work bi-directionally. If you are willing to know how this kind of selector works, please visit: GenForm with Autofill