Implementing a Datalist

Hi There,

I’m having trouble implementing a datalist using forms in django. It’s very confusing and I can’t seem to figure out how to do it. Seems like it should be the type of thing that takes 5 minutes. Any idea why I can’t seem to use this basic HTML5 element?

Thanks,

Chris.

It looks like you could use this by creating a custom widget that would use that html element when rendering the field. This will likely involve creating a custom template to use when rendering that widget.

This doesn’t work because there is no forms.DataList

I don’t understand how they built this…There’s a list of built in widgets and datalist isn’t one of them. Is there no way to create a custom widget and tell it what HTML5 element I want, and have it try to render that?

Yes, that’s exactly what you want to do - create a custom widget with a template that uses datalist.

First, read about specifying widgets. Notice how the widget is a parameter in the form field definition, and refers to a class. Since it’s just a class, you could write your own class and use it, instead.

Now, if you read about Widget, you’ll see how classes inheriting from the base Widget class have a method named get_context. The get_context dictionary identifies that there’s a key in that dictionary named template_name, which identifies the name of the template to be used to render that widget.

So from reading this, it appears to me like you have two similar options for replacing the template name on a widget by creating a subclass of the Select widget. You could either just create your subclass and define a new value for the self.template_name attribute in your init method, or you could create a get_context method in your subclass that replaces the value of template_name within the context. (IMO, the first is easier, the second is more flexible - really depends upon your needs for further customization.) Both allow you to specific your own template to use to render your field. I would then look at the standard template to get some ideas as to how to code your template - perhaps even just copying it to replace the parts you need replaced.

Just a couple notes to keep in mind:

  • Django is just python. All the facilities that exist within python are available to you. So it’s all just code that can be modified / replaced as needed.
  • Django is designed to be extended / enhanced / modified. In most cases, anytime you want to change how something behaves, you can pretty much expect there to be a ‘hook’ or connection point for you to make your modifications.

Could you show me an example? I’m totally boggled by what you’re saying.

The best examples are the Django source code itself, browsable at github

In particular, I’d like to point your attention to the system provided widgets for Input and TextInput. Notice how TextInput extends the Input class.

I tried to interpret what you said, and this is what I can understand you asking me to do:

class mySubclass(forms.Form):
    def __init__(self, template_name):
        self.template_name = template_name

class CommentForm(forms.Form):
    name = forms.CharField()
    url = forms.URLField()
    comment = forms.CharField(widget=mySubclass("datalist"))

but I am getting the error: TypeError: init () missing 1 required positional argument: ‘template_name’

Do you know what I’m doing wrong?

Thanks.

Ok, you’re off to a good start. But yes, there are a couple things to improve.

  • Your mySubclass should be a subclass of a widget, not a form.

  • Did you look at the TextInput class I referenced in a previous reply? Reading and understanding what it is doing, should address the specific error you’ve listed.

How would I subclass a widget? I can’t see how to import Widget or Input or any of those classes from Django.

And yes I read the TextInput link you sent. It seems to be a class that sets two variables, “input_type” to ‘text’ and “template_name” to a django template.

I’m still not seeing the connection on how this creates a datalist in the end.

Importing a widget - no different than importing any other class. You use the import statement with the module structure being the path to the appropriate file:

from django.forms.widgets import Input

The template_name is the name of the template to render for your widget. You’ll need to create your own template. You can start by looking at the Django-provided select widget which contains an html select element - which is what I believe you want to replace with the datalist element .

OK, so I did that and just straight copied their TextInput subclass, and the server crashes. I get the error:

‘mySubclass’ object has no attribute ‘attrs’

class mySubclass(Input):
    input_type = 'text'
    template_name ="django/forms/widgets/text.html"

Any idea? I’m not sure where it got the idea it needs attrs, or even where that series of letters comes from whatsoever.

What’s the code that’s actually trying to use the widget?

If it’s what you posted above:

class CommentForm(forms.Form):
    name = forms.CharField()
    url = forms.URLField()
    comment = forms.CharField(widget=mySubclass("datalist"))

Then your comment field isn’t correct. Notice that the widget parameter is looking for a class, not an instance of a class.

Yah that’s the part trying to use it, but it doesn’t even get that far. It gives me an error on an exact copy of their subclass.

class mySubclass(Input):
    input_type = 'text'
    template_name ="django/forms/widgets/text.html"

class CommentForm(forms.Form):
    name = forms.CharField()
    url = forms.URLField()
    comment = forms.CharField(widget=mySubclass)

error:

venv\lib\site-packages\django\forms\widgets.py", line 205, in __deepcopy__
    obj.attrs = self.attrs.copy()
AttributeError: 'mySubclass' object has no attribute 'attrs'

Interesting. I can do the following from the shell:

>>> class myI(Input):
...     input_type = 'text'
...     template_name = 'django/forms/widgets/text.html'
...
>>> m = myI()
>>> m.render("x", "y")
'<input type="text" name="x" value="y">'

>>>
>>> class CF(forms.Form):
...     name = forms.CharField()
...     comment = forms.CharField(widget=myI)
...

>>> cf = CF()
>>> cf.as_p()
'<p><label for="id_name">Name:</label> <input type="text" name="name" required id="id_name"></p>\n<p><label for="id_comment">Comment:</label> <input type="text" name="comment" required id="id_comment"></p>'
>>>

Don’t know why you’re getting that error. Other classes are inheriting from Input without doing anything special.

However, you could try:

class mySubclass(Input):
    input_type = 'text'
    template_name = 'django/forms/widgets/text.html'

    def __init__(self, attrs=None):
        super().__init__(attrs)

That will at least ensure that your class has an attrs attribute.

OK sweet, got that part working without the need for the “attrs” attribute. Seems I just needed to manually stop and restart the server. I feel like I’m getting close now. Thanks for all your help on this by the way! 4 hours so far! WOW! amazing!

So now I’ve copied the code from the select widget as you suggested, and it looks like this after modification:

<datalist name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
    <optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
    {% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
    </optgroup>{% endif %}{% endfor %}
</datalist>

I’m pretty sure I still need to change that “django/forms/widgets/attrs.html” to something, but I’m not sure what. And right now, I’ve got another issue locating the template.

my template dirs looks like this:

'DIRS': [os.path.join(BASE_DIR, 'templates')],

So I have my templates for my apps on the same level as the manage.py file and the other apps.
I put the copied template code from above into “templates/forms/datalist.html”. I also modified mySubclass to be:

class mySubclass(Input):
    input_type = 'text'
    template_name ="forms/datalist.html"

But now I’m getting the error:

venv\lib\site-packages\django\template\backends\django.py", line 84, in reraise
    raise new from exc
django.template.exceptions.TemplateDoesNotExist: forms/datalist.html

Any idea what I might be doing wrong?

Thanks again!

You probably don’t need to change the reference to the attrs.html template. It’s a pre-existing hook for supplying custom attributed to the html element. That can stay the same.

I’d start by looking at how you’re referencing other templates in your app to see what directories you’re using and how you’re using them in your code. (I organize my templates differently - I use the pattern of having a templates directory in each app. But this is one topic that always trips me up.)

So I dug up one of my functional settings files, the templates section looks like this:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates',
                 ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'menu.context_processors.menu',
            ],
        },
    },
]

This looks for my template files in:

 <projectname>/<appname>/templates/<appname>

So if I have two apps named reports and work in a project named fmc, then the templates are stored in:

fmc/reports/templates/reports/

and

fmc/work/templates/work/

I did it simply using a choiche field and overriding the template directly in form class. It seems to work:

class Customers(forms.Form):
    CUSTOMER_CHOICES = (
        (None, ""),
        (1, "Customer A"),
        (2, "Customer B"),
        (3, "Customer C"),
        (4, "Customer D"),
        (5, "Customer E"),
    )
    customer = forms.ChoiceField(
                   choices = CUSTOMER_CHOICES,
                   coerce = str,
                   required=False
                  )
    customer.widget.template_name = 'widgets/datalist.html'

for the widget template i started from select widget template:

 <input list="{{ widget.name }}s" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
 <datalist id="{{ widget.name }}s">
	 {% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
	   <optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
	   {% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
	   </optgroup>{% endif %}{% endfor %}
 </datalist>

To be able to override widgets template You need to install “django.forms” as app in your settings and set variable:
FORM_RENDERER = ‘django.forms.renderers.TemplatesSetting’

1 Like

How do fix the error with free text “Select a valid choice. That choice is not one of the available choices”

I suggest you open a new topic for this, including all the relevent code.