Search code examples
djangodjango-adminjquery-select2placeholderdjango-widget

How to insert placeholder in Django Admin autocomplete input?


I don't want to use my own JS or alter the template when Django has other mechanisms.

I've tried three methods, first as described in how-to-add-placeholder-text-to-a-django-admin-field and in the docs.

from django import forms

    class AppointmentAdmin(admin.ModelAdmin):
        
        def get_form(self, request, obj=None, **kwargs):
            kwargs['widgets'] = {
                'name': forms.TextInput(attrs={'placeholder': 'Type here'})
            }
            return super().get_form(request, obj, **kwargs)

Then the method described in django-add-placeholder-text-to-form-field, slightly altered to get around Python errors.

class AppointmentAdmin(admin.ModelAdmin):
    
   def get_form(self, request, obj=None, **kwargs):
      form = super().get_form(request, obj, **kwargs)
      form.base_fields['customer'].widget.attrs["placeholder"] = "Type here"
      return form

Finally, using formfield_overrides:

class AppointmentAdmin(admin.ModelAdmin):
    formfield_overrides = {
        models.TextField: { "widget":forms.TextInput(attrs={'placeholder':'Type here'})},
    }

None of these are working. I must be overlooking something simple?

The relevant portion of models.py:

class Appointment(models.Model):
    customer = models.ForeignKey(Customer, blank=False, null=True, default=None, on_delete=models.CASCADE, related_name='appointments')
    autocomplete_fields = ["customer"]

I removed the autocomplete_fields line to no effect vis a vis a placeholder. (Side note: I think autocomplete_fields should have a placeholder option rather than requiring all the jumping through hoops as described above.)

The 'help_text' option on the field in models.py would almost be a cheap replacement for a placeholder, except that it makes no sense in other parts of this program.

Here's the HTML that Django generates using the second method above (form.base_fields...etc.):

<div class="related-widget-wrapper" data-model-ref="customer">
<select name="customer" placeholder="Type here" required="" id="id_customer" class="admin-autocomplete select2-hidden-accessible" data-ajax--cache="true" data-ajax--delay="250" data-ajax--type="GET" data-ajax--url="/admin/autocomplete/" data-app-label="giraffe" data-model-name="appointment" data-field-name="customer" data-theme="admin-autocomplete" data-allow-clear="false" data-placeholder="" lang="en" data-select2-id="id_customer" tabindex="-1" aria-hidden="true">
</select>
        
<span class="select2-container select2-container--admin-autocomplete select2-container--open" style="position: absolute; top: 164.484px; left: 15px;">
<span class="select2-dropdown select2-dropdown--above" dir="ltr" style="width: 249px;">
<span class="select2-search select2-search--dropdown">
<input class="select2-search__field" type="search" tabindex="0" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" aria-controls="select2-id_customer-results">
</span>
<span class="select2-results">
<ul class="select2-results__options" role="listbox" id="select2-id_customer-results" aria-expanded="true" aria-hidden="false">
<li class="select2-results__option" role="option" aria-selected="false" data-select2-id="6">customer name here</li>
        (more li and closing of spans here)

UPDATE: Django implements autocomplete select boxes using Select2, whose docs say:

For single selects only, in order for the placeholder value to appear, you must have a blank as the first option in your control. This is because the browser tries to select the first option by default. If your first option were non-empty, the browser would display this instead of the placeholder.

But Django inserts a customer as the first option. Though it's non-empty, the browser doesn't display it as described, but maybe it's still breaking the Select2 placeholder insert? Maybe the answer is how to make Django insert an empty object?


Solution

    1. autocomplete_fields is defined on ModelAdmin, rather than Model.

    2. It is attrs['data-placeholder'] rather than attrs['placeholder']. However, Django doesn't properly support it as data-placeholder is overwritten in AutocompleteMixin:

      class AutocompleteMixin:
          ...
      
          def build_attrs(self, base_attrs, extra_attrs=None):
              ...
              attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
              ...
              attrs.update({
                  ...
                  'data-placeholder': '',  # Allows clearing of the input.
                  ...
              })
              return attrs
      

    You can patch AutocompleteSelect widget to restore data-placeholder:

    from django.contrib.admin import options
    
    
    class AutocompleteSelectWithPlaceholder(options.AutocompleteSelect):
        def build_attrs(self, base_attrs, extra_attrs=None):
            attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
            if 'data-placeholder' in base_attrs:
                attrs['data-placeholder'] = base_attrs['data-placeholder']
            return attrs
    
    
    options.AutocompleteSelect = AutocompleteSelectWithPlaceholder
    

    Usage:

    @admin.register(Appointment)
    class AppointmentAdmin(admin.ModelAdmin):
        autocomplete_fields = ['customer']
    
        def get_form(self, request, obj=None, **kwargs):
            form = super().get_form(request, obj, **kwargs)
            form.base_fields['customer'].widget.attrs['data-placeholder'] = 'Type here'
            return form