Encadenar select’s HTML amb Django

4 de Enero de 2016 · 3 min de lectura

Logo APSL

Fa un temps que he descobert un plugin jQuery que va força be per encadenar select's Html amb Django, és a dir, que les opcións d'un select s'actualitzin en funció del valor seleccionat a un primer select. El plugin en qüestió és diu Chained Selects Plugin for jQuery and Zepto. Per fer-lo servir simplement hem d'estendre el "widget" per defecte per representar els select's html en Django.

La idea és crear codi html per tal que es mostri similar a la següent manera, és a dir, que la clau dels valors del primer select es fixin com a css class dels options del segon select.

<select id="mark" name="mark">
  <option value="">--</option>
  <option value="bmw">BMW</option>
  <option value="audi">Audi</option>
</select>
<select id="series" name="series">
  <option value="">--</option>
  <option value="series-3" class="bmw">3 series</option>
  <option value="series-5" class="bmw">5 series</option>
  <option value="series-6" class="bmw">6 series</option>
  <option value="a3" class="audi">A3</option>
  <option value="a4" class="audi">A4</option>
  <option value="a5" class="audi">A5</option>
</select>

Aixi doncs per fer que aixo sigui possible en Django estendrem i personalitzarem el widget Select que ve per defecte amb django.forms.

# -*- encoding: utf-8 -*-

from django.forms.widgets import Select
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe


class CustomSelect(Select):
    """
    Custom widget used in combination with http://www.appelsiini.net/projects/chained
    in order to have two or more related html select's. Whenever the first select is changed updates
    the options of the related one
    """

    def render_option(self, selected_choices, option_value, option_label):
        if option_value is None or option_value == '':
            option_value = ''
            css_class = ''
        else:
            css_class, option_value = option_value.split('___')

        option_value = force_text(option_value)

        if option_value in selected_choices:
            selected_html = mark_safe(' selected="selected"')
            if not self.allow_multiple_selected:
                # Only allow for a single selection.
                selected_choices.remove(option_value)
        else:
            selected_html = ''
        return format_html('<option value="{0}"{1} class="{2}">{3}</option>', option_value, selected_html,
                           mark_safe(css_class), force_text(option_label))

Llavors el nostre formulari ha de fer ús del widget personalitzat. En el formulari hem de parar atenció als camps group i type. El camp type depen del valor seleccionat a group. Per a cada valor de group hi ha una sèrie de valors a type. Cada cop que el select de group s'actualitzi, es canvii, es recalcularan els valors de type. Aquesta és la clau del problema, el valor de l'option del segon selector ha d'esser processat per tal d'extreure'n el propi valor de l'option així com el valor de l'element pare dins el primer selector. És per això que al mètode __init__ del formulari customitzam l'atribut choices del widget del camp type, per passar el valor en el format:

clau-option-select-dependent__clau-option-valor-pare
class ProductFilter(FilterFormMixin, django_filters.FilterSet):

    by_name = django_filters.CharFilter(lookup_type='icontains', name='translations__name',
                                        required=False, label=_('Nombre'))

    code = django_filters.CharFilter(required=False, label=_('Código'))

    place = django_filters.ModelChoiceFilter(queryset=Place.objects.all(), required=False,
                                             empty_label=_('Todos los recintos'), name='product_places')

    group = django_filters.ModelChoiceFilter(queryset=GroupProductType.objects.all(), required=False,
                                             name='type__group', empty_label=_('Todos los grupos'))

    type = django_filters.ChoiceFilter(choices=(), required=False, label=_('Todos los tipos'))

    tags = NamedModelMultipleChoiceField(queryset=Tag.objects.all(), required=False, label=_(u'Seleccione algún tag'))

    ENABLED_CHOICES = (
        ('all', _('Todos')),
        ('si', _('Activos')),
        ('no', _('Inactivos')),
    )
    enabled = django_filters.MethodFilter(required=False, label=_('Estado'),
                                          action='filter_enabled', widget=forms.Select(choices=ENABLED_CHOICES))

    @staticmethod
    def filter_enabled(queryset, value):
        if value == 'all':
            return queryset
        return queryset.filter(enabled=value == 'si')

    class Meta:
        model = Product
        fields = ('by_name', 'code', 'place', 'group', 'type', 'tags', 'enabled')

    def __init__(self, data=None, queryset=None, prefix=None, strict=None):

        choices = [('', _(u'Todos los tipos'))]
        choices += [('{0}___{1}'.format(it.group_id, it.pk), it.name) for it in ProductType.objects.order_by('name')]
        self.base_filters['type'].field.widget = CustomSelect(choices=choices)
        super().__init__(data, queryset, prefix, strict)

Per gestionar l'event del canvi hem de modificar el template que renderitza el formulari. D'aquesta manera incloem el Javascript necessari, el codi del propi plugin i el codi que s'encarrega de relacionar els dos selectors.

{% block extrajs2 %}
    <script type="text/javascript" src="{% static 'bower_components/chained/jquery.chained.min.js' %}"></script>
    <script type="text/javascript">
        $(function(){
            $("#id_type").chained("#id_group");
        });
    </script>
{% endblock extrajs2 %}

A l'exemple li deim que el selector amb identificador #id_type depen del selector amb identificador #id_group. Finalment senyalar que el plugin és bastant versàtil ja que permet enllaçar varis selectors a la vegada amb la mateixa tècnica.

http://www.appelsiini.net/projects/chained

Comparte este artículo
Artículos recientes