俺流?vue.jsの選択ダイアログを作る方法のまとめの話

はい、小ネタです。

ツンデレ本管理プロジェクトの管理画面周りにてvue.jsの書籍選択ダイアログを作っていた時の覚書です。

ポイントは、呼び出し元とはリンクしないということですね。最初、オブジェクトをそのまま投げていて、かなりきつかったです。

作成したダイアログはこんな感じです

モジュールとしてvue-js-modalを使っていますよ

        "vue-js-modal": "^2.0.1"

動きをまとめますと

このダイアログでは、シーン一覧からシーンを選択して対象の書籍を変更します

動きとしては

  1. 読書シーン(画面ではシーン(読書中))を送ります : メソッド showReadingBook ら辺
  2. 送られたシーンからタイトル、書籍を表示します
  3. 変更後の書籍一覧から対象の書籍は外します
  4. 変更後書籍を選んで「選択」を押下すると画面が閉じて更新処理が発生します : @select="selectedBook($event)"
  5. 既読ボタンを押下すると読書中の書籍を既読フラグをあげます: @readed="readed($event)"

ざっくりこんな感じ、ポイントは

  • シーン情報はポインタ渡し?ではなく、値渡し

ポインタ渡し?の場合はbind(props)を使ってやればOK。
ってかそれが一番の悩みところです。

ポインタ渡し?はこんな感じで呼び出し先のpropsにバインとしてやればOKです。
ただし、呼び出し先で変更すると、呼び出し元にも影響があるので悩ましいです
例では:bookにバインドしています

値だけを渡すには、

  • modalの呼び出し後のメソッド(@before-open)を指定してやる
  • 呼び出し時にparamsにオブジェクトを入れるってということ

です。

呼び出し時に

this.$modal.show('selectReadingBookModal',{reading_book : readingBook});

呼び出し後に値を引き継いでいます

呼び出し先に内部変数に値を入れます。

        beforeOpen($event){
            this.reading_book = $event.params.reading_book;
            this.init_items();
        }

この画面では、画面に表示している書籍一覧をapiから取ってきているので書籍情報を初期化するメソッド(init_items)をコールしています。
この部分がポイントで、bindではうまいこと表示が出ません…
ってかmountedでしか書籍一覧が更新されずに… 数日悩みました
明瞭的にダイアログ表示時にメソッドをコールする必要が楽だったです
updatedとかだったら無限ループに…

ともあれ、試行錯誤した結果がこれです
vue.jsの本を肘置きに悩んだ結果ですw(早く読めw)

次回に続く?

呼び出し元ソース(抜粋)

<template>
    <div>
        <div class="properties-row d-flex justify-content-center">
            <div class="col-4" v-for="readingBook in readingBooks">
                <div class="card">
                    <div class="card-body" >{{ readingBook.scene.title }}</div>
                    <div class="row card-body text-primary text-center d-flex justify-content-center">
                        <reading-book class="col-12" :book="readingBook.book" ></reading-book>
                    </div>
                    <div class="card-body">
                        <button class="col-12 btn btn-dark" v-on:click="showReadingBook(readingBook)">変更</button>
                    </div>
                </div>
            </div>
        </div>
        <select-reading-book-modal @select="selectedBook($event)" @readed="readed($event)"/>
    </div>
</template>
<script>
import SelectReadingBookModal from "./modals/SelectReadingBookModal";
export default {
    name: 'ReadingBookListComponent',
    data () {
        return {
            readingBooks : []
        }
    },
    methods: {
       /**
         * 読書中のモーダルダイアログでの選択ボタンアクション
         * @param $event イベント
         * @returns {Promise<void>}
         */
        async selectedBook($event){
            const currentBookData = $event.book !== null ? $event.book.id !== undefined ?{'id' : $event.book.id , 'asin' : $event.book.asin} : null : null;
            const scene = $event.scene.id !== undefined ?{'id' : $event.scene.id , 'title' : $event.scene.title} : null;
            const data = {
                id : $event.id
                , scene : scene
                , book : currentBookData
            };
            const url = API_URL + 'api/reading/save'

            await axios.post(url, data).then(x => {
                // 保存後のアクション

                // 読書中シーンを初期化します
                this.initReadingBooks();
            })
        },
        /**
         * 読書中書籍の状態を変更するボタンのアクション
         * @param readingBook 対象の書籍
         */
        showReadingBook (readingBook) {
            this.$modal.show('selectReadingBookModal',{reading_book : readingBook});
        },
    },
}

ダイアログソース

<template>
    <modal name="selectReadingBookModal"
           :width="600"
           :height="240"
           @before-open="beforeOpen">
        <div class="modal-body bg-warning">
            {{ reading_book.scene ? reading_book.scene.title : "" }}
        </div>
        <div class="modal-body book row">
            <div class="col-3">
                読書中
            </div>
            <div class="col-9 form-group">
                <div v-if="reading_book.book" class="truncate"> 【 {{ reading_book.book.asin }} 】{{ reading_book.book.title }}</div>
                <div v-else>読書中書籍なし</div>
            </div>
            <div class="col-3">
                変更後
            </div>
            <div class="col-9 form-group">
                <select class="form-group col-12 custom-select" v-model="select_item">
                    <option value="null">読書中なし</option>
                    <optgroup label="専門書">
                        <option v-for="book in technical_items" :value="{ id : book.id , asin : book.asin }" >【 {{ book.asin }} 】 {{ book.title }}</option>
                    </optgroup>
                    <optgroup label="コミック">
                        <option v-for="book in comic_items" :value="{ id : book.id , asin : book.asin }" >【 {{ book.asin }} 】 {{ book.title }}</option>
                    </optgroup>
                    <optgroup label="小説">
                        <option v-for="book in novel_items" :value="{ id : book.id , asin : book.asin }" >【 {{ book.asin }} 】 {{ book.title }}</option>
                    </optgroup>
                </select>
            </div>
            <div class="col-12 d-flex justify-content-end">
                <div class="col-4">
                </div>
                <div class="col-4">
                    <button class="btn btn-secondary form-group col-12" v-if="reading_book.book" v-on:click="readed" >既読</button>
                </div>
                <div class="col-4 d-flex justify-content-end">
                    <button class="btn btn-secondary form-group col-12" v-on:click="select">選択</button>
                </div>
            </div>
        </div>
    </modal>
</template>
<script>
import axios from "axios";

const API_URL = document.getElementsByName('API_URL')[0].content;

export default {
    name: 'SelectReadingBookModal',
    data() {
        return {
            select_item: [],
            technical_items: [],
            comic_items: [],
            novel_items: [],
            reading_book: []
        };
    },
    methods: {
        hide() {
            this.$modal.hide('selectReadingBookModal');
        },
        select() {
            const data = {
                id: this.reading_book.id
                , scene: this.reading_book.scene
                , book: this.select_item
            };
            this.$emit("select", data);
            this.hide();
        },
        readed() {
            const data = {
                id: this.reading_book.id
                , scene: this.reading_book.scene
                , book: this.reading_book.book
            };
            this.$emit("readed", data);
            this.hide();
        },
        /**
         * 読書中ダイアログの書籍一覧を更新します
         * @returns {Promise<void>}
         */
        async init_items() {
            const data = {
                filter: {
                    readed: false,
                },
            };
            const url = API_URL + "api/book/list";
            await axios.post(url, data).then(x => {
                this.technical_items = [];
                this.comic_items = [];
                this.novel_items = [];

                x.data.books.forEach(book => {
                    // 選択中の書籍は表示対象から外す
                    if (this.reading_book.book !== null) {
                        if (book.asin === this.reading_book.book.asin) {
                            return;
                        }
                    }
                    // 書籍種別による格納配列切り替え
                    switch(parseInt(book.kind_id)){
                        case 1 :
                            this.technical_items.push(book);
                            break;
                        case 2 :
                            this.comic_items.push(book);
                            break;
                        case 3 :
                            this.novel_items.push(book);
                            break;

                    }
                });
            })
        },
        beforeOpen($event){
            this.reading_book = $event.params.reading_book;
            this.init_items();
        }
    },
}
</script>