Can't change type attribute in django-crispy-forms

Hi all,

I’m using django-crispy-forms with the crispy-bootstrap5 package. (As far as I know, I need the latter one for using the newest bootstrap version 5. If the impression I gained from some google results is wrong - or there is any other best practice -, please let me know.)

I’m not able to change the type-attribute from text to e.g. date or range in the following scenario:

views.py

class PresentationForm(ModelForm):
    class Meta:
        model = PreviewEntry
        fields = ["name", "first_day", "second_day", "starttime", "endtime"]
    @property
    def helper(self):
        helper = FormHelper()
        helper.layout = Layout(
            Field('name', wrapper_class='mt-3 row'),
# =====>  type='date' is later rendered as type='text' 
            Field('first_day', wrapper_class='mt-3 row', type='date'),
            Field('second_day', wrapper_class='mt-3 row', type='date'),
               # starttime and endtime are left out

class CreateEntry(generic.CreateView):
    model = PreviewEntry
    template_name = "mytemplate.html"
    form_class = PresentationForm
    success_url = 'success.html'

settings.py:

INSTALLED_APPS = [
...
    'crispy_forms',
    'crispy_bootstrap5',
]

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"

mytemplate.html

{% extends base.html %}
{% block content %}
    {% crispy form %}
{% endblock %}

This works perfectly except of the fact that the type-field of my “first_day” and “second_day” element are rendered as type=‘text’ instead of type=‘date’ (where the browser would provide a datepicker).

<input type="text" name="first_day" class="dateinput form-control" required="" id="id_first_day">

Later in the form I would like to use type=“range”. This is also not possible.

Of course I could do something like this:

helper.layout = Layout(

...

HTML('''
        <div id="div_id_starttime" class="mb-3 row">
            <label for="id_starttime" class="form-label col-4 requiredField">Starttime<span class="asteriskField"></span> </label>
            <div class="col-8">
                <input class="col-10" name="starttime" id="id_starttime" type="range" value="8" min="0" max="24" oninput="this.nextElementSibling.value = this.value + ' Uhr' ">
                <output>8 Uhr</output>
            </div>
        </div>
        '''),
)
...

This approach works really fine. But I doubt that there isn’t a better way to do.

Any ideas on what is best practice to change the type in a crispy-form? Already searched the documentation [2] but didn’t find anything about that.

[1] crispy-bootstrap5 · PyPI
[2] Search — django-crispy-forms 1.11.1 documentation

There are a couple thoughts that come to mind, I’m not sure any are “optimal” or “best” or even “good”.

  • The crispy Field class renders the type attribute from the input_type attribute of the instance of the Field object. So you could subclass Field to create a crispy DateField class with the desired input_type attribute.

  • Specify the widget for the field, setting the ‘type’ in the attrs for that widget.

  • Create your own template that overrides the base system template allowing for a separate setting to set the type attribute in the input tag.

1 Like

I am not sure I understood the questions directly. I am answering assuming you just want to have a datepicker on your forms. If that’s the case, here is how I do my datepickers.

from django import forms
from django.forms import ModelForm

class DateInput(forms.DateInput):
    input_type = "date"

class PresentationForm(forms.ModelForm):
     class Meta:
           model = PreviewEntry
           fields = ["name", "first_day", "second_day", "starttime", "endtime"]
            widgets = {
            "first_day": DateInput(),
            "second_day" : DateInput()
}

Hopefully that helps!

3 Likes

@KenWhitesell Argh. I already got that feeling that there isn’t an easy one-minute approach available. :smiley: Thanks a lot (again!) for your input. Option 1 and Option 2 sound pretty interesting to me.

@Jonathan-Adly Thanks for your answer. I would like to have a datepicker and a range in my form. Also I would like to let crispy render my form, by using the “helper” function in the PresentationForm(ModelForm).
Your solution works fine for me if I don’t use crispy and solely create my form using “fields = []”. But when I use “form_class” instead of “fields” (to reference to my PresentationForm(ModelForm) for using all those handy crispy stuff) I don’t see a possibility to use the DateInput object. That’s possibly the case because I missed something. So if there is any option to integrate the DateInput object in my ModelForm, I would appreciate the broad hint. :slight_smile:

Thanks and BR

@Jonathan-Adly has effectively implemented my second option. It should work for you. Your reference to the form_class in the CBV doesn’t alter his solution.

He’s showing the change necessary to your PresentationForm to use the custom DateInput field defined immediately before it. (Phyically - you add his DateInput class just before your PresentationForm class.)

1 Like

Arg! Sorry, I just didn’t get it… :roll_eyes:

Thanks @Jonathan-Adly and @KenWhitesell for your great support. The solution proposed by @Jonathan-Adly works perfectly well. Will re-read the widget’s documentation… :slight_smile:

Just a short status update:

The main issue regarding “date” was solved.

My issue regarding the type=“range” can’t be fixed that easy. Everything works fine with the HTML workaround and probably I will use no. 3 of @KenWhitesell 's idea to create an own template for that single case, but I was curious how this could be solved using idea no. 1, by subclassing the Field to create a crispy RangeField class with the desired attribute.

If anynoe is interested in helping, this would be very much appreciated (just spent about three hours trying to solve this by myself…). Just wanted to say: Everything works and this is just for my personal curiosity. :slight_smile:

The issue
I cannot overwrite the default “class” attribute “textinput textInput form-control” with the attribute “col-10”, so the range button looks really ugly.

What I would like to receive:
<input type="range" name="starttime" class="col-10" input_type="range" required="" id="id_starttime">

What I receive:
<input type="range" name="starttime" class="col-10 textinput textInput form-control" input_type="range" required="" id="id_starttime">

views.py:

    class Meta:
        model = PreviewCampaign
        fields = [""first_day", "last_day", "starttime", "endtime",]
        widgets = {
            "first_day": DateInput(),
            "last_day": DateInput(),
            "starttime": forms.TextInput(attrs={'type': 'range', 'class': 'col-10'})
<!== =======> Also doesn't work: ==>
            "starttime": forms.TextInput(attrs={'type': 'range', 'css_class': 'col-10'})
...

So I searched for the Field Instance and found it here [1]. Also had a lookt at the source code [2]. Overwriting wouldn’t be an issue, but I cannot find the code where the “css_class” attribute is specified. So just wondering what I’ve overseen :confused:

[1] layout — django-crispy-forms 1.11.1 documentation
[2] GitHub - django-crispy-forms/django-crispy-forms: The best way to have DRY Django forms. The app provides a tag and filter that lets you quickly render forms in a div format while providing an enormous amount of capability to configure and control the rendered HTML.

I don’t understand the problem here - you’re getting col-10 as one of the css classes, which is what you’re asking for.

Ahh, ok, you’ve got the issue where you don’t want the Bootstrap / Crispy css classes to be added.

Don’t have any immediate solutions at my fingertips right now, but I know there are multiple solutions for this.
But first, what is the issue with what you’re getting? You’re describing it as “really ugly”, but what does that translate to?

This is how it should look, if class= “col-10”
with

And this is how it looks like if class=" col-10 textinput textInput form-control"
without

I can’t recreate the image you’re showing.

My only suggestion for this would be to look at the styling being applied in your browser’s developer tools to see exactly what element is creating the problem. Once you’ve done that, you can override it in your css.

@KenWhitesell Thanks a lot for your time. I had a look at the attributes in the developer tools. The “form-control” attribute is the one that changes the look of the range-slider and makes it look ugly.

BUT:
I spent two evenings trying to understand widgets and writing my first own widget. 3/4th of the time was spent for unsuccessfully overwriting methods of the forms.Widget class, before realizing that the code I used from stackoverflow as an inspiration was totally useless.

So:
Here us the code of my first widget that renders a range slider. It works fine. Feedback is of course appreciated. :slight_smile:

views.py:

class MyWidget(forms.Widget):
    template_name = 'myprojectpath/widgets/testwidget.html'

class PresentationForm(ModelForm):

    class Meta:
        model = MyModel
        fields = [""first_day", "last_day", "starttime", "endtime"]
        widgets = {
            "first_day": DateInput(),
            "last_day": DateInput(),
            "starttime": MyWidget(attrs={'min': 0, 'max': 24, 'def_value': 8, 'unit': 'Uhr'})
        }
    @property
    def helper(self):
        helper = FormHelper()
        helper.layout = Layout(
            ...
            Field(starttime)
            ...

...

    class CreateMyView(generic.CreateView):
        model = MyModel
        template_name = "template.html"
        form_class = PresentationForm
        success_url = 'success.html'

testwidget.html

        <input class="col-10" name="{{widget.name}}" id="id_{{ widget.name }}"
               type="range"
            {% if 'defvalue' in widget.attrs %}
            value="{{ widget.attrs.defvalue }}"
            {% endif %}

               min="{{ widget.attrs.min }}"
               max="{{ widget.attrs.max }}"
               oninput="this.nextElementSibling.value = this.value
               {% if 'unit' in widget.attrs %}
               + ' ' + ' {{widget.attrs.unit}} '
               {% endif %}
                " >
        <output>{{ widget.attrs.defvalue }}
            {% if 'unit' in widget.attrs %}
            {{widget.attrs.unit}}
            {% endif %}
        </output>

That’s certainly the more direct, flexible, and probably “appropriate” solution.

I had a different solution in mind.

Note: the following is untried - just something I’m winging off-hand.

My first attempt to fix this would be to add something like this to my css file:

input[type=range].form-control {
    attribute: setting;
}

This would be a high-priority selector that I believe would override a selector just based on css class, and you can then reset whatever attribute(s) is being set by the form-control class that is causing the problem.

1 Like

Just tried it out.

input[type=range].form-control {
    appearance: auto;
}

also works fine. Thanks! :slight_smile:


P.S. / Update: Just trying around: Then I would also need to add the “5 o’clock” behind the … part. Not sure if there is a nice and easy solution for that one.