シンプルブログ、検索機能の実装

Django Django REST framework Vue.js

概要

DRFとVue.jsで、シンプルブログを作るシリーズの一つです。検索機能を実装していきます。

ストアにカテゴリ一覧を追加

カテゴリの一覧データは、ストア内に持たせることにします。コンポーネントに直接持たせても今のところは問題ないんどえすが、今後の拡張に備えてストアに持たせます。store/index.jsに追加します。

// 追加
import {UPDATE_POSTS, UPDATE_CATEGORIES} from "./mutation-types"

export default new Vuex.Store({
    state: {
        categories: [],  // 追加
    },
    getters: {
        // 追加
        categoryList(state) {
            return state.categories
        },
    },
    mutations: {
        // 追加
        [UPDATE_CATEGORIES](state, payload) {
            state.categories = payload
        },
    },
    actions: {
        // 追加
        [UPDATE_CATEGORIES]({commit}, payload) {
            commit(UPDATE_CATEGORIES, payload)
        },
    },
    modules: {}
})

カテゴリのゲッターやアクションを追加しておきます。また、store/mutation-types.jsにも定数を追加しておきましょう。

export const UPDATE_POSTS = 'updatePosts'
export const UPDATE_CATEGORIES = 'updateCategories'  // 追加

検索欄の作成

Header.vueを次のようにします。

<template>
    <header>
        <h1>
            <router-link :to="{name: 'posts'}">Design Note</router-link>
        </h1>
        <div id="form">
            <input type="text" placeholder="Search" class="text" v-model="keyword" @change="search">
            <div class="selectWrap">
                <select class="select" v-model="selected" @change="search">
                    <option value="" :key="-1">Category</option>
                    <option v-for="category of categoryList" :value="category.id" :key="category.id">{{category.name}}
                    </option>
                </select>
            </div>
        </div>
    </header>
</template>

<script>
    import {mapActions, mapGetters} from 'vuex'
    import {UPDATE_CATEGORIES, UPDATE_POSTS} from "@/store/mutation-types"

    export default {
        name: 'site-header',
        data() {
            return {
                keyword: '',
                selected: '',
            }
        },
        created() {
            this.$http(this.$httpCategories)
                .then(response => {
                    return response.json()
                })
                .then(data => {
                    this[UPDATE_CATEGORIES](data)
                })
        },
        computed: {
            ...mapGetters(['categoryList'])
        },
        methods: {
            ...mapActions([UPDATE_CATEGORIES, UPDATE_POSTS]),
            search() {
                this.$http(`${this.$httpPosts}?keyword=${this.keyword}&category=${this.selected}`)
                    .then(response => {
                        return response.json()
                    })
                    .then(data => {
                        this[UPDATE_POSTS](data)
                    })
            },
        }
    }
</script>

<style scoped>
    header {
        background-color: #000;
        color: #fff;
        height: 50px;
        display: grid;
        grid-template-columns: 20px 1fr 20px;
        grid-template-rows: 1fr;
        margin-bottom: 80px;
    }

    header > * {
        grid-row: 1;
        grid-column: 2;
    }

    h1 {
        justify-self: start;
        align-self: center;
        font-size: 20px;
        font-weight: normal;
    }

    h1 > a {
        color: #fff;
        text-decoration: none;
    }

    #form {
        justify-self: end;
        align-self: center;
        display: none;
    }

    .text {
        border-bottom: solid 1px #ccc;
        border-right: none;
        border-top: none;
        border-left: none;
        background-color: transparent;
        color: #fff;
        width: 200px;
        margin-left: 20px;
        padding-left: 6px;
        padding-bottom: 1px;
        font-family: fot-tsukuardgothic-std, sans-serif;
    }

    .selectWrap {
        margin-left: 20px;
        width: 150px;
        position: relative;
        display: inline-block;
    }

    .selectWrap::after {
        content: '';
        width: 6px;
        height: 6px;
        border: 0;
        border-bottom: solid 2px #ccc;
        border-right: solid 2px #ccc;
        -ms-transform: rotate(45deg);
        -webkit-transform: rotate(45deg);
        transform: rotate(45deg);
        position: absolute;
        top: 50%;
        right: 10px;
        margin-top: -4px;
    }

    .select {
        appearance: none;
        border-bottom: solid 1px #ccc;
        border-right: none;
        border-top: none;
        border-left: none;
        background-color: transparent;
        color: #fff;
        width: 100%;
        font-family: fot-tsukuardgothic-std, sans-serif;
    }

    ::placeholder {
        color: #fff;
        opacity: 1;
        font-family: fot-tsukuardgothic-std, sans-serif;
    }

    @media (min-width: 768px) {
        header {
            grid-template-columns: 1fr 700px 1fr;
        }

        #form {
            display: block;
        }
    }

    @media (min-width: 1024px) {
        header {
            grid-template-columns: 1fr 980px 1fr;
        }
    }
</style>

createdではカテゴリの一覧を取得し、ストア内のデータとして設定して、ゲッターを介してカテゴリの一覧をselect要素の選択肢として表示しています。

キーワード入力欄のテキストか、カテゴリが変更されたらsearchメソッドですが、キーワードやカテゴリをGETパラメータとして記事一覧のAPIにアクセスしています。入力内容はv-modelのようにすると、コンポーネントのdataと連動させることができます。

検索機能の実装(REST framework)

GETパラメータがあったら、それで記事一覧を絞り込むようにDjango側のビューも修正しましょう。

# 追加
from django.db.models import Q


class PostList(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = SimplePostSerializer
    pagination_class = StandardResultsSetPagination

    def get_queryset(self):
        queryset = super().get_queryset()

        keyword = self.request.query_params.get('keyword', None)
        if keyword:
            queryset = queryset.filter(
                Q(title__icontains=keyword) | Q(lead_text__icontains=keyword) | Q(main_text__icontains=keyword))

        category = self.request.query_params.get('category', None)
        if category:
            queryset = queryset.filter(category=category)

        return queryset

今回は素直に、get_querysetメソッドを上書きしました。

Relation Posts

Comment

記事にコメントする

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