Tom-Select as Django Widget
Asked Answered
A

1

6

I would like to use tom-select as an Django autocomplete select-widget.

In the corresponding database table (Runners) are several hundred rows, so that I can't load all into the html page.

In the model RaceTeam I have a ForeignKey to Runner.

The select widget will be used in whole pages and in html fragments (via htmx).

I tried subclass django.forms.Select, but this sucks all db rows into widget.choices.

Azide answered 19/6, 2021 at 7:59 Comment(0)
A
4

I use this solution:

I copy the tom-select JS+CSS to static/relayrace/vendor/.

class RunnerSelectWidget(Select):
    class Media:
        css = {
            'all': ('relayrace/vendor/tom-select.css',)
        }
        js = ('relayrace/vendor/tom-select.complete.min.js',)

    def set_choices(self, value):
        # there can be several thousand choices. We don't want them here
        self._choices = []

    def get_choices(self):
        choices = list(self._choices)
        return choices

    choices = property(get_choices, set_choices)

    def get_context(self, name, value, attrs):
        context = super().get_context(name, value, attrs)
        context['widget']['attrs']['class']='runner-select'
        return context

    def optgroups(self, name, value, attrs=None):
        # Make the initial value available
        self._choices = [(id, runner_to_select_text(Runner.objects.get(pk=id))) for id in value]
        return super().optgroups(name, value, attrs)


def runner_select_json(request):
    # You might want to add some permission checking here. Otherwise everybody
    # can get data via this endpoint.
    query = request.GET.get('q', '').strip()
    qs = get_queryset(query)
    items = [dict(value=r.pk, text=runner_to_select_text(r)) for r in qs]
    return JsonResponse(dict(items=items, total_count=len(items)))

def runner_to_select_text(runner):
    return f'{runner.user.first_name} {runner.user.last_name}'

def get_queryset(query):
    if not query:
        return Runner.objects.none()
    return Runner.objects.filter(Q(user__username__icontains=query)|
                                     Q(user__first_name__icontains=query)|
                                     Q(user__last_name__icontains=query)).order_by('user__username')

Usage of the widget:

class RaceTeamForm(forms.ModelForm):
    class Meta:
        model = RaceTeam
        widgets = {
            'leader': RunnerSelectWidget(),
        }

JS to init the widget on whole page load and on fragment load:

htmx.onLoad(function(elt) {
    var allSelects = htmx.findAll(elt, ".runner-select")
    for( select of allSelects ) {
      new TomSelect(select, {
        // fetch remote data
        load: function(query, callback) {

            var url = '/runner_select_json?q=' + encodeURIComponent(query);
            fetch(url)
                .then(response => response.json())
                .then(json => {
                    callback(json.items);
                }).catch(()=>{
                    callback();
                });

        }
    })}});

urls.py

url('runner_select_json', runner_select_json, name='runner_select_json')
Azide answered 19/6, 2021 at 7:59 Comment(2)
The property (set_choices()) feels a bit dirty. Please tell me, if you know a simpler solution.Azide
In regards to htmx and JS-initialized widgets – how do you properly uninitialize your TomSelects before loading a new bit of a htmx-response so they do not bug out when using the browser history? Reference: htmx.org/docs/#3rd-partyJimjimdandy

© 2022 - 2024 — McMap. All rights reserved.