Tutorial

Note

For demo links to work, you need to run the test project on localhost.

Overview

dal_alight is a DAL autocomplete frontend built on native Web Components. No jQuery or third-party JS library is required.

Key differences from the Select2 frontend:

  • No jQuery / Select2 required. The component is a pure web component (<autocomplete-select>) with no external library dependency.

  • The view returns HTML fragments instead of JSON. Each result is a <div data-value="…">…</div> element; the JS component reads those divs to build the dropdown.

  • Static files served are dal_alight/autocomplete-light.css, dal_alight/autocomplete-light.js, and dal_alight/dal-django.js.

Install

Add dal_alight to INSTALLED_APPS before django.contrib.admin:

INSTALLED_APPS = [
    'dal',
    'dal_alight',
    # 'grappelli',
    'django.contrib.admin',
    ...
]

For Generic Foreign Key support also add dal_queryset_sequence:

INSTALLED_APPS = [
    'dal',
    'dal_alight',
    'dal_queryset_sequence',
    'dal_alight_queryset_sequence',  # bridges the two
    ...
]

Create an autocomplete view

Use AlightQuerySetView:

from dal import autocomplete

from your_countries_app.models import Country


class CountryAutocomplete(autocomplete.AlightQuerySetView):
    def get_queryset(self):
        if not self.request.user.is_authenticated:
            return Country.objects.none()

        qs = Country.objects.all()

        if self.q:
            qs = qs.filter(name__istartswith=self.q)

        return qs

Register the view

from django.urls import path

from your_countries_app.views import CountryAutocomplete

urlpatterns = [
    path(
        'country-autocomplete/',
        CountryAutocomplete.as_view(),
        name='country-autocomplete',
    ),
]

The simplest registration passes the model directly without a custom view class:

from dal import autocomplete
from your_countries_app.models import Country

urlpatterns = [
    path(
        'country-autocomplete/',
        autocomplete.AlightQuerySetView.as_view(model=Country),
        name='country-autocomplete',
    ),
]

Danger

As with all DAL views, the URL is public by default. Always check permissions in get_queryset().

Use the view in a Form widget

ForeignKey (single select)

Use ModelAlight for a ForeignKey field:

from dal import autocomplete
from django import forms


class PersonForm(forms.ModelForm):
    class Meta:
        model = Person
        fields = ('__all__',)
        widgets = {
            'birth_country': autocomplete.ModelAlight(url='country-autocomplete')
        }

ManyToManyField (multi select)

Use ModelAlightMultiple for a ManyToManyField:

widgets = {
    'visited_countries': autocomplete.ModelAlightMultiple(url='country-autocomplete')
}

Initial values on edit forms

ModelAlight and ModelAlightMultiple inject the currently selected object(s) into the <select> options at render time so they appear pre-selected without an extra AJAX call.

Automation with djhacker

import djhacker  # pip install djhacker
from django import forms

djhacker.formfield(
    Person.birth_country,
    forms.ModelChoiceField,
    widget=autocomplete.ModelAlight(url='country-autocomplete')
)

Using autocompletes in the admin

Register a ModelAdmin with your custom form:

from django.contrib import admin

from your_person_app.models import Person
from your_person_app.forms import PersonForm


class PersonAdmin(admin.ModelAdmin):
    form = PersonForm

admin.site.register(Person, PersonAdmin)

Inlines work the same way:

class PersonInline(admin.TabularInline):
    model = Person
    form = PersonForm

Using autocompletes outside the admin

Include {{ form.media }} — the widget’s media property loads autocomplete-light.js and dal-django.js automatically:

{% extends 'base.html' %}
{# Don't forget that one ! #}
{% load static %}

{% block content %}
<div>
    <form action="" method="post">
        {% csrf_token %}
        {{ form.as_p }}

        <div style="display: none" class="formset-empty">
            {{ view.formset.empty_form }}
        </div>

        <div class="formset-rows">
            {{ view.formset }}
        </div>

        <span id="add-form" class="button">Add form</span>

        <input type="submit" />
    </form>
</div>
{% endblock %}

{% block footer %}
<script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script>

{{ form.media }}

<script>
(function($) {
    $('#add-form').click(function() {
        var index = $('#id_inline_test_models-TOTAL_FORMS').val()
        var newForm = $('#id_inline_test_models-__prefix__-DELETE').closest('.formset-empty').clone()
        newForm.find(':input').each(function() {
            for (attr of ['name', 'id'])
                $(this).attr(
                    attr,
                    $(this).attr(attr).replace('__prefix__', index)
                )
        })
        newForm.removeAttr('style').removeClass('formset-empty')
        newForm.insertBefore($(this))
        $('#id_inline_test_models-TOTAL_FORMS').val(
            parseInt($('#id_inline_test_models-TOTAL_FORMS').val()) + 1
        )
        newForm.slideDown()
    })
})($)
</script>
{% endblock %}

Creation of new choices

Set create_field on the view to enable on-the-fly object creation:

urlpatterns = [
    path(
        'country-autocomplete/',
        CountryAutocomplete.as_view(create_field='name'),
        name='country-autocomplete',
    ),
]

When no exact match exists the view appends a <div data-create data-value="…">Create "…"</div> element to its response. Selecting it triggers a POST to the same URL; the view creates the object and returns the rendered HTML label directly (a <div data-value="…">…</div> fragment), which the component inserts into the selection deck.

Add validate_create=True to run full_clean() before saving:

CountryAutocomplete.as_view(create_field='name', validate_create=True)

Filtering results based on other form fields (forwarding)

The forward widget argument works the same way as in any DAL frontend:

class PersonForm(forms.ModelForm):
    continent = forms.ChoiceField(choices=CONTINENT_CHOICES)

    class Meta:
        model = Person
        fields = ('__all__',)
        widgets = {
            'birth_country': autocomplete.ModelAlight(
                url='country-autocomplete',
                forward=['continent'],
            )
        }

In the view, read the forwarded value from self.forwarded:

class CountryAutocomplete(autocomplete.AlightQuerySetView):
    def get_queryset(self):
        qs = Country.objects.all()
        continent = self.forwarded.get('continent', None)
        if continent:
            qs = qs.filter(continent=continent)
        if self.q:
            qs = qs.filter(name__istartswith=self.q)
        return qs

All forwarding features (forward.Field, forward.Const, forward.Self, forward.JavaScript, renaming) work the same — they live in dal.forward and are frontend-agnostic.

Autocompleting from a list of strings

Use AlightListView when results come from a plain Python list rather than a QuerySet:

class FruitAutocomplete(autocomplete.AlightListView):
    def get_list(self):
        return ['apple', 'mango', 'apricot', 'orange']

Register it as a named URL, then use ListAlight in your form:

widget = autocomplete.ListAlight(url='fruit-autocomplete')

To allow creating values that are not in the list, define a create method on the view:

class FruitAutocomplete(autocomplete.AlightListView):
    def get_list(self):
        return ['apple', 'mango', 'apricot', 'orange']

    def create(self, text):
        return text  # return the stored value

And use AlightListCreateChoiceField in the form so the submitted value passes validation:

from dal import autocomplete


class FruitForm(forms.ModelForm):
    fruit = autocomplete.AlightListCreateChoiceField(
        choice_list=['apple', 'mango', 'apricot', 'orange'],
        widget=autocomplete.ListAlight(url='fruit-autocomplete'),
    )

For a static list that only accepts existing values, use AlightListChoiceField instead.

Grouped results

QuerySet-backed groups

Use AlightGroupQuerySetView to render results grouped by a related field. Set group_by_related to the name of the ForeignKey whose target model provides the group label, and optionally related_field_name (default 'name') for the label attribute:

class CountryAutocomplete(autocomplete.AlightGroupQuerySetView):
    group_by_related = 'continent'
    related_field_name = 'name'

    def get_queryset(self):
        return Country.objects.all()

List-based groups

Use AlightGroupListView for grouped string lists. Return (group_name, item) pairs from get_list():

class FruitAutocomplete(autocomplete.AlightGroupListView):
    def get_list(self):
        return [
            ('Tropical', 'mango'),
            ('Tropical', 'papaya'),
            ('Temperate', 'apple'),
            ('Temperate', 'pear'),
        ]

Items with group=None are rendered without a group header.

Tags support

Free-text tags (no taggit)

Use TagAlight for a comma-separated tag field not backed by a taggit model. The widget stores tags as a comma-separated string.

django-taggit integration

The view works with AlightQuerySetView using the tag name as the result value:

from dal import autocomplete
from taggit.models import Tag


class TagAutocomplete(autocomplete.AlightQuerySetView):
    def get_result_value(self, result):
        return result.name

    def get_queryset(self):
        if not self.request.user.is_authenticated:
            return Tag.objects.none()
        qs = Tag.objects.all()
        if self.q:
            qs = qs.filter(name__istartswith=self.q)
        return qs

In the form use TaggitAlight:

class TestForm(autocomplete.FutureModelForm):
    class Meta:
        model = TestModel
        fields = ('name',)
        widgets = {
            'tags': autocomplete.TaggitAlight('your-taggit-autocomplete-url')
        }

Generic Foreign Key support

See GenericForeignKey for the model setup.

Automatic view using AlightGenericForeignKeyModelField:

from dal import autocomplete
from django.contrib.auth.models import Group


class TestForm(autocomplete.FutureModelForm):
    location = autocomplete.AlightGenericForeignKeyModelField(
        model_choice=[
            (Country, 'name'),
            (City, 'name', [('language', 'spoken_language')]),
        ],
    )

    class Meta:
        model = TestModel

Register the auto-generated URL in urls.py:

from .forms import TestForm

urlpatterns += TestForm.as_urls()

Manual view using GenericForeignKeyModelField with explicit widget and view:

from dal import autocomplete
from dal_alight_queryset_sequence.views import AlightQuerySetSequenceView
from dal_alight_queryset_sequence.widgets import QuerySetSequenceAlight


class TestForm(autocomplete.FutureModelForm):
    location = autocomplete.GenericForeignKeyModelField(
        model_choice=[(Country,), (City,)],
        widget=QuerySetSequenceAlight,
        view=AlightQuerySetSequenceView,
    )

    class Meta:
        model = TestModel

Class reference

Views

Class

Description

AlightQuerySetView

QuerySet-backed autocomplete, returns HTML fragments

AlightGroupQuerySetView

QuerySet-backed, results grouped by a related field

AlightListView

Autocomplete from a Python list

AlightGroupListView

Grouped autocomplete from a Python list

AlightQuerySetSequenceView

Multi-model Generic FK view (dal_alight_queryset_sequence)

Widgets

Class

Description

ModelAlight

Single select, QuerySet-backed (ForeignKey)

ModelAlightMultiple

Multi select, QuerySet-backed (ManyToManyField)

Alight

Single select, arbitrary choices

AlightMultiple

Multi select, arbitrary choices

ListAlight

Single select, list-backed

TagAlight

Free-text tag widget (comma-separated)

TaggitAlight

django-taggit integration

QuerySetSequenceAlight

Single select, multi-model GFK (dal_alight_queryset_sequence)

QuerySetSequenceAlightMultiple

Multi select, multi-model GFK (dal_alight_queryset_sequence)

Form fields

Class

Description

AlightListChoiceField

ChoiceField validated against a list or callable

AlightListCreateChoiceField

Like above, allows on-the-fly created values

AlightGenericForeignKeyModelField

Auto-wired GFK field (dal_alight_queryset_sequence)