ブログの検索フォームをカッコよくする

Python Django Djangoカスタムウィジェット

概要

Djangoで、ブログを作るシリーズ①の1つです。ブログの検索フォームを改良していきます。

タグの一覧をお洒落にする

今回、タグは複数指定が可能です。検索の際も複数のタグで絞り込みができるようにしたいのですが、プルダウン選択だと複数選択がちょっと難しいです。なので、チェックボックスを使います。

通常のチェックボックスだと見た目的にアレなので、Django、カスタムチェックボックスの作成のような、カスタムチェックボックスを作ろうと思います。

アプリケーション内にwidgets.pyを作り、次のようにしておきます。

from django import forms


class CustomCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
    template_name = 'nblog1/widgets/custom_checkbox.html'
    option_template_name = 'nblog1/widgets/custom_checkbox_option.html'

    def __init__(self, attrs=None):
        super().__init__(attrs)
        if 'class' in self.attrs:
            self.attrs['class'] += ' custom-checkbox'
        else:
            self.attrs['class'] = 'custom-checkbox'

templates内にwidgetsディレクトリを作ります。その中に2つファイルを作ります。

custom_checkbox.html

{% for group, options, index in widget.optgroups %}{% for option in options %}{% include option.template_name with widget=option %}{% endfor %}{% endfor %}

custom_checkbox_option.html

{% include "django/forms/widgets/input.html" %}<label for="{{ widget.attrs.id }}" class="custom-checkbox-label">{{ widget.label }}</label>

今作ったウィジェットを使うようにします。forms.pyです。

from django import forms
from .models import Tag
from .widgets import CustomCheckboxSelectMultiple


class PostSearchForm(forms.Form):
    """記事検索フォーム。"""
    key_word = forms.CharField(
        label='検索キーワード',
        required=False,
    )

    tags = forms.ModelMultipleChoiceField(
        label='タグでの絞り込み',
        required=False,
        queryset=Tag.objects.order_by('name'),
        widget=CustomCheckboxSelectMultiple,
    )

カスタムチェックボックスのCSSを追記します。

/* カスタムチェックボックス */
.custom-checkbox {
    display: none;
}

.custom-checkbox:checked + .custom-checkbox-label {
    background: #00809d;
    color: #fff;
}

.custom-checkbox-label {
    box-sizing: border-box;
    display: inline-block;
    border-radius: 4px;
    text-align: center;
    text-decoration: none;
    border: solid 1px #ccc;
    transition: 0.25s;
    padding: 4px 16px;
    cursor: pointer;
    font-size: 14px;
    margin: 3px;
}

.custom-checkbox-label:hover {
    opacity: 0.5;
}

ここまでで、次のような見た目になります。

記事の部分を、よくある横スクロールにしましょう。slickといったjQueryプラグインを使ってもいいのですが、今回はシンプルに実装します。

次のCSSを追記します。

.inline-checkbox {
    overflow-x: auto;
    white-space: nowrap;
    -webkit-overflow-scrolling: touch;
    padding: 0;
}

すると、横スクロールできるようになりました。

入力欄とボタンもオシャレにしましょう。次のCSSを追記します。


input[type=text], input[type=email], input[type=number],
select, textarea {
    font-size: 14px;
    padding: 4px 8px;
    box-sizing: border-box;
    border-radius: 4px;
    border: solid 1px #ccc;
    background-color: #fff;
    font-family: "Ubuntu", "Noto Sans JP", sans-serif;
}

button, a.button {
    font-size: 14px;
    -webkit-appearance: none;
    padding: 4px 16px;
    border-radius: 4px;
    background-color: #fff;
    border: solid 1px #ccc;
    vertical-align: bottom;
    font-family: "Ubuntu", "Noto Sans JP", sans-serif;

    /* a要素用の設定 */
    box-sizing: border-box;
    display: inline-block;
    text-decoration: none;
    text-align: center;
    color: #333;

    /* button要素用の設定 */
    cursor: pointer;
}

button:hover, a.button:hover {
    opacity: 0.5;
}

button.btn-link, a.btn-link {
    border: none;
    color: #00809d;
    vertical-align: initial;
}

だいぶ良くなりました。

もっと便利にする

検索フォームを、もっと使いやすいようにしていきます。まずですが、チェックボックスをクリックすると、すぐに検索されるようにしましょう。いちいち「検索」ボタンを押さなくて済むようにします。これは簡単で、チェックボックスが押されたらフォームをサブミットすれば良いのです。

次のscript要素をどこかに入れておきます。私はbase.html</body>の直前に書きました。

<script>
    document.addEventListener('DOMContentLoaded', e => {

        const searchForm = document.getElementById('search-form');

        for (const check of document.getElementsByName('tags')) {
            check.addEventListener('change', () => {
                searchForm.submit();
            });
        }
    });
</script>

検索フォーム内のタグですが、毎回ここをスクロールしてお目当てのタグを探すのは大変です。なので、記事一覧内にあるタグも、クリックすればタグ選択したり解除できるようにしましょう。次の赤い部分です。

さきほどのscript要素を書き換えます。

<script>
    document.addEventListener('DOMContentLoaded', e => {

        const searchForm = document.getElementById('search-form');

        for (const tag of document.getElementsByClassName('tag')) {
            tag.addEventListener('click', () => {
                const pk = tag.dataset.pk;
                const checkbox = document.querySelector(`input[name="tags"][value="${pk}"]`);
                if (checkbox.checked) {
                    checkbox.checked = false;
                } else {
                    checkbox.checked = true;
                }
                searchForm.submit();
            });
        }

        for (const check of document.getElementsByName('tags')) {
            check.addEventListener('change', () => {
                searchForm.submit();
            });
        }
    });
</script>

記事一覧内のタグ部分にはtagというcssのclass名をつけていて、data-pk="{{ tag.pk }}"のようにして、タグモデルのpkも 持たせるように予めしていました。それを使っています。

さらに、現在選択しているタグを上部に表示するようにもしましょう。post_list.htmlに追記します。

{# 略 #}
    <section>
       {% if search_form.cleaned_data.tags %}
            <p class="tags" id="select-tags">選択しているタグ: {% for tag in search_form.cleaned_data.tags %}
                <span class="tag" data-pk="{{ tag.pk }}">{{ tag.name }}</span>{% endfor %}</p>
        {% endif %}  
        {% for post in post_list %}
            <article class="post">
{# 略 #}

この部分のタグにも、<span class="tag" data-pk="{{ tag.pk }}">{{ tag.name }}</span>としています。これにより、記事一覧内のタグと同じように、クリックで選択したり解除できます。

結果的に、

  1. 検索フォーム内のタグ
  2. 記事一覧内のタグ
  3. 「選択しているタグ:」に表示されているタグ

の3か所にあるタグ名をクリックすれば、絞り込んだり解除ができるということです。

選択しているタグのCSSを追加しておきます。

/* 選択したタグエリア */
#select-tags {
    margin-bottom: 48px;
    font-size: 14px;
}

これで、次のGIFのような便利なタグ検索ができるようになりました。

紐づく記事数も表示する

最後に、検索フォーム内のタグに紐づく記事数を表示するようにしましょう。Python(100)みたいな感じですね。forms.pyを編集します。

from django import forms
from django.db.models import Count  # 追加
from .models import Tag
from .widgets import CustomCheckboxSelectMultiple


class PostSearchForm(forms.Form):
    """記事検索フォーム。"""
    key_word = forms.CharField(
        label='検索キーワード',
        required=False,
    )

    tags = forms.ModelMultipleChoiceField(
        label='タグでの絞り込み',
        required=False,
        queryset=Tag.objects.annotate(post_count=Count('post')).order_by('name'),  # 変更
        widget=CustomCheckboxSelectMultiple,
    )

.annotate(post_count=Count('post'))の部分ですね。こういった集計に関する処理は、Djangoで、集計処理でも紹介しています。

models.pyを開いて、Tagモデルの__str__を書き換えて、件数用の属性があればそれを表示するようにします。

class Tag(models.Model):
    name = models.CharField('タグ名', max_length=255, unique=True)

    def __str__(self):
        # 検索フォーム等では、紐づいた記事数を表示する。その場合はpost_countという属性に記事数を持つ。
        if hasattr(self, 'post_count'):
            return f'{self.name}({self.post_count})'
        else:
            return self.name

これで、検索フォーム内のタグに件数が表示されるようになりました。

Relation Posts

Djangoで、集計処理

Djangoで、データの平均値を求めたり、何個のデータが紐づくか、といった集計処理のサンプルを紹介しています。

Python Django

Comment

記事にコメントする

まだコメントはありません。