Techvenience

Technology × Convenience - Vue / React / Next / Nuxt / ChatGPTなどのIT技術がもたらす便利さをお伝えします。最近はChatGPTなどのAI技術を使ってブログを書いています。

【Vue.js】Vue.jsとLocalStorageを使って献立管理アプリを作成 一覧画面 - Part3 - 【LocalStorage】

【Vue.js】Vue.jsとLocalStorageを使って献立管理アプリを作成 一覧画面 - Part3 - 【LocalStorage】

f:id:duo-taro100:20160218004611p:plain

前回まで
www.sky-limit-future.com

www.sky-limit-future.com

mainComponent.jsの解説 part2

一覧画面

一覧画面の機能は以下の通りです。

・一覧表示機能
・ソート機能
・削除機能
・編集画面への遷移機能
・全削除機能
・バックアップ機能
・データ復元機能

■html部分
今回はテーブルで一覧を作成しました。

template : 
  '<div class="contents">'+
    '<div class="search-menu">'+
      '<span class="labels">ワード検索:</span><input type="text" v-model="searchWord"><br>'+
      '<span class="labels">ジャンル検索:</span><select v-model="genre">'+
        '<option v-for="genres in menuGenre">{{genres}}</option>'+
      '</select>'+
    '</div>'+
    '<div class="menu-list">'+
      '<h5>メニューリスト</h5>'+
      <!-- リストが空の時に表示 -->
      '<div v-if="!listDispFlag" class="tac">'+
        '<h3>該当する献立がありません。</h3>'+
        '<router-link tag="button" to="/regist" class="submit">献立を登録する</router-link>'+
      '</div>'+

      <!-- リストが存在する時に表示 -->
      '<table class="menu-list-table" v-if="listDispFlag">'+
        '<thead>'+
          '<tr>'+
            '<th></th>'+
            '<th>献立名<span @click="desc(nameFlag)">▲</span><span @click="asc(nameFlag)">▼</span></th>'+
            '<th>ジャンル<span @click="desc(genreFlag)">▲</span><span @click="asc(genreFlag)">▼</span></th>'+
            '<th>日付<span @click="desc(dateFlag)">▲</span><span @click="asc(dateFlag)">▼</span></th>'+
          '</tr>'+
        '</thead>'+
        '<tbody>'+
          <!-- リストをループして表示 -->
          '<tr v-for="menu in menuList">'+
            '<td>'+
              '<button @click="deletMenu(menu.name)" class="submit">削除</button>'+
              '<button @click="updateMenu(menu.name)" class="submit">編集</button>'+
            '</td>'+
            '<td><a @click="getDateList(menu.name)" class="menu-name">{{menu.name}}</a></td>'+
            '<td>{{menu.genre}}</td>'+
            '<td>{{menu.latestDate}}</td>'+
          '</tr>'+
        '</tbody>'+
      '</table>'+
      '<div>'+
        '<button class="submit tac" @click="deleteAll()" v-if="listDispFlag">全削除</button>'+
        '<button class="submit tac" @click="buckupMenuList()" v-if="listDispFlag">バックアップ</button>'+
        '<button class="submit tac" @click="insertBuckup()">データ復元</button>'+
      '</div>'+
    '</div>'+
  '</div><!-- .contents -->',

■一覧表示機能
まずは一覧表示機能です。
ローカルストレージの「menuList」を取得して、それを表示するだけです。
一覧表示フラグ「listDispFlag」がtrueだったらテーブルを表示します。
mountedでmenuListを取得して、そのmenuListをループします。

mounted: function(){
    // ここは検索用プルダウンの作成なので無関係
    for(genre in MENU_GENRE) {
        this.menuGenre.push(MENU_GENRE[genre]);
    }

    // 一覧表示するリストを取得します。
    // getMenuList()はstorage.jaを参照
    this.menuList = getMenuList();
    // リストが空だったら一覧表示フラグをfalseにします。
    if(this.menuList == null || Object.keys(this.menuList).length == 0){
        this.listDispFlag = false;
    } else {
        this.listDispFlag = true;
    }
},

■ソート機能
「▲」「▼」を押すとソートするようにしました。
実装した後に、ボタンの意味合いと「asc(昇順)」「desc(降順)」が逆であることに気が付きました。
面倒なので、直さずにこのままです。(実装の方も逆かもしれませんが、面倒なのでこのままで)

'<thead>'+
  '<tr>'+
    '<th></th>'+
    '<th>献立名<span @click="desc(nameFlag)">▲</span><span @click="asc(nameFlag)">▼</span></th>'+
    '<th>ジャンル<span @click="desc(genreFlag)">▲</span><span @click="asc(genreFlag)">▼</span></th>'+
    '<th>日付<span @click="desc(dateFlag)">▲</span><span @click="asc(dateFlag)">▼</span></th>'+
  '</tr>'+
'</thead>'+

ソートの方法としてはmenuListの各キーに対するvalueを一旦、配列に格納します(配列A)。
valueの構成は以下のようになっています。

{name: 献立名, genre : ジャンル, date : 使用した日付の配列, latestDate : 使用した直近の日付}

なので、配列Aはこんな感じになります。

// 配列A
[
  {name: "焼肉", genre : "肉", date : [20170112], latestDate : 20170112},
  {name: "魚定食", genre : "魚", date : [20171112,20171005], latestDate : 20171112},
  {name: "ラーメン", genre : "麺類", date : [20180201], latestDate : 20180201},
  {name: "しゃぶしゃぶ", genre : "肉", date : [20180123], latestDate : 20180123}
]

この配列をsort関数でソートしていくという形です。
名前用、ジャンル用、日付用のメソッドを各々用意してもよかったですが、今回はどのプロパティでソートするかをパラメータで渡して処理を分けています。
最後に、配列Aの順序でmenuListにデータを格納しなおすといった感じです。データ量が多いとパフォーマンスが悪そう。。。
ちょっとデータを投入して試してみたいです。
実際の実装は以下のようになっています。

desc : function(flag){
    console.log('[START]desc sort.')
    this.menuList = {};
    var list = getMenuList();
    var copyMenuList = [];
    for(menu in list) {
        copyMenuList.push(list[menu]);
    }

    if(flag == 'name') {
        copyMenuList.sort(function(a,b) {
            if(a.name > b.name) return -1;
            if(a.name < b.name) return 1;
            return 0;
        })
    } else if (flag == 'genre') {
        copyMenuList.sort(function(a,b) {
            if(a.genre > b.genre) return -1;
            if(a.genre < b.genre) return 1;
            return 0;
        })
    } else if (flag == 'date') {
        copyMenuList.sort(function(a,b) {
            if(a.latestDate > b.latestDate) return -1;
            if(a.latestDate < b.latestDate) return 1;
            return 0;
        })
    }
    for(detail in copyMenuList) {
        this.menuList[copyMenuList[detail].name] = copyMenuList[detail];
    }
    console.log('[END]desc sort.')
},
asc : function(flag){
    console.log('[START]asc sort.')
    this.menuList = {};
    var list = getMenuList();
    var copyMenuList = [];
    for(menu in list) {
        copyMenuList.push(list[menu]);
    }

    if(flag == 'name') {
        copyMenuList.sort(function(a,b) {
            if(a.name < b.name) return -1;
            if(a.name > b.name) return 1;
            return 0;
        })
    } else if (flag == 'genre') {
        copyMenuList.sort(function(a,b) {
            if(a.genre < b.genre) return -1;
            if(a.genre > b.genre) return 1;
            return 0;
        })
    } else if (flag == 'date') {
        copyMenuList.sort(function(a,b) {
            if(a.latestDate < b.latestDate) return -1;
            if(a.latestDate > b.latestDate) return 1;
            return 0;
        })
    }
    for(detail in copyMenuList) {
        this.menuList[copyMenuList[detail].name] = copyMenuList[detail];
    }
    console.log('[END]asc sort.')
}

■削除機能 deletMenu
指定したキーのデータをmenuListから削除します。
ローカルストレージ側の実装はstorage.jsのdeleteMenu()を参照してください。
一致するキー以外のものを詰め直して、保存するという実装方法にしました。
キーは献立名なので、パラメータで渡します。

<button @click="deletMenu(menu.name)" class="submit">削除</button>
deletMenu : function(key) {
    console.log('[STRAT]deleteMenu with ' + key);
    // confirmで確認。OKが押されるとtrueが帰ってくる。
    var isDelete = confirm('献立を削除します。よろしいですか?');
    if(isDelete) {
        deleteMenu(key);
    }
    this.menuList = getMenuList();
    console.log('[END]deleteMenu with ' + key);
},

ここではやっていませんが、isDelete = false だったら return した方が良さそうです。

■編集画面への遷移機能 updateMenu
メソッド名がちょっとイケてないです。
こちらもパラメータに献立名を渡しています。

<button @click="updateMenu(menu.name)" class="submit">編集</button>
updateMenu : function(key) {
    console.log('[STRAT]updateMenu with ' + key);
    this.$router.push({path : 'regist' , query : { keyName : key}});
    console.log('[END]updateMenu with ' + key);
}

単純に献立登録画面へ遷移させるだけです。
この際に、献立名をパラメータで送っています。
書きながら思いましたが、ジャンルもパラメータで送る or 登録画面で、保存されている値を初期表示するという機能があるべきですね。
日付に関しては、当日のままでいいかと思います。

■全削除機能
嫁からいらないから機能をなくしてくれと言われました。
単純に、ローカルストレージから「menuList」を削除する実装です。
全て削除した場合に、テーブルを非表示にするようにしました。

deleteAll : function() {
    console.log('[STRAT]deleteAll.');
    var isDeleteAll = confirm('献立全てを削除します。よろしいですか?');
    if(isDeleteAll) {
        deleteMenuList();
    }
    this.menuList = {};
    this.listDispFlag = false;
    console.log('[END]deleteAll.');
},

■バックアップ機能
現在ローカルストレージに保存してある値をalertで表示するだけです。
その値をコピーして、どこかのメモ帳に保存してもらいます。
データが膨大になると、コピーするのも大変なので他の方法を考え中。

buckupMenuList : function() {
    console.log('[STRAT]buckupMenuList.');
    var buckupList = getMenuList();
    alert('以下の値をコピーして、メモ帳に保存してください。\n' + JSON.stringify(buckupList));
    console.log('[END]buckupMenuList.');
},

buckupListをそのまま表示しようとすると[Object Object]となるのでJSON.stringify()を使っています。

■データ復元機能
上記で保存しておいたメモを入力すると、データが復元できるものになります。
バリデーションは空チェックのみ。
入力値をJSON.parse()してローカルストレージに保存します。

insertBuckup : function(){
    console.log('[STRAT]insertBuckup.');
    var buckupData = prompt('献立のバックアップデータをコピペしてください。','');
    if(buckupData == '' || buckupData == undefined || buckupData == null){
        alert('データを入力してください。');
        return;
    } 
    setMenuList(JSON.parse(buckupData));
    this.menuList = getMenuList();
    if(this.menuList == null || Object.keys(this.menuList).length == 0){
        this.listDispFlag = false;
    } else {
        this.listDispFlag = true;
    }
    console.log('[END]insertBuckup.');
},


■全体
まだまだ改善の余地がありそうです。
バックアップ機能の改善と、登録画面へ遷移したときのジャンルの初期値設定がやらなければならない箇所ですね。

// listComponent
Vue.component('lists', {
    template : 
        '<div class="contents">'+
          '<div class="search-menu">'+
            '<span class="labels">ワード検索:</span><input type="text" v-model="searchWord"><br>'+
            '<span class="labels">ジャンル検索:</span><select v-model="genre">'+
              '<option v-for="genres in menuGenre">{{genres}}</option>'+
            '</select>'+
          '</div>'+
          '<div class="menu-list">'+
            '<h5>メニューリスト</h5>'+
            '<div v-if="!listDispFlag" class="tac">'+
              '<h3>該当する献立がありません。</h3>'+
              '<router-link tag="button" to="/regist" class="submit">献立を登録する</router-link>'+
            '</div>'+
            '<table class="menu-list-table" v-if="listDispFlag">'+
              '<thead>'+
                '<tr>'+
                  '<th></th>'+
                  '<th>献立名<span @click="desc(nameFlag)">▲</span><span @click="asc(nameFlag)">▼</span></th>'+
                  '<th>ジャンル<span @click="desc(genreFlag)">▲</span><span @click="asc(genreFlag)">▼</span></th>'+
                  '<th>日付<span @click="desc(dateFlag)">▲</span><span @click="asc(dateFlag)">▼</span></th>'+
                '</tr>'+
              '</thead>'+
              '<tbody>'+
                '<tr v-for="menu in menuList">'+
                  '<td>'+
                    '<button @click="deletMenu(menu.name)" class="submit">削除</button>'+
                    '<button @click="updateMenu(menu.name)" class="submit">編集</button>'+
                  '</td>'+
                  '<td><a @click="getDateList(menu.name)" class="menu-name">{{menu.name}}</a></td>'+
                  '<td>{{menu.genre}}</td>'+
                  '<td>{{menu.latestDate}}</td>'+
                '</tr>'+
              '</tbody>'+
            '</table>'+
            '<div>'+
              '<button class="submit tac" @click="deleteAll()" v-if="listDispFlag">全削除</button>'+
              '<button class="submit tac" @click="buckupMenuList()" v-if="listDispFlag">バックアップ</button>'+
              '<button class="submit tac" @click="insertBuckup()">データ復元</button>'+
            '</div>'+
          '</div>'+
        '</div><!-- .contents -->',
    data: function () {
        return {
            menuList : {},
            menuGenre : [],
            genre : MENU_GENRE.ALL,
            listDispFlag : true,
            searchWord : '',
            nameFlag : 'name',
            genreFlag : 'genre',
            dateFlag : 'date',
        }
    },
    methods: {
        deletMenu : function(key) {
            console.log('[STRAT]deleteMenu with ' + key);
            var isDelete = confirm('献立を削除します。よろしいですか?');
            if(isDelete) {
                deleteMenu(key);
            }
            this.menuList = getMenuList();
            console.log('[END]deleteMenu with ' + key);
        },
        updateMenu : function(key) {
            console.log('[STRAT]updateMenu with ' + key);
            this.$router.push({path : 'regist' , query : { keyName : key}});
            console.log('[END]updateMenu with ' + key);
        },
        getDateList : function(key) {
            console.log('[STRAT]getDateList with ' + key);
            var dateList = this.menuList[key].date;
            var dateStr = ""
            for(index in dateList) {
                dateStr = dateStr + dateList[index] + "\n"
            }
            alert(key + 'を使用したのは以下の日程です。\n' + dateStr);
            console.log('[END]getDateList with ' + key);
        },
        deleteAll : function() {
            console.log('[STRAT]deleteAll.');
            var isDeleteAll = confirm('献立全てを削除します。よろしいですか?');
            if(isDeleteAll) {
                deleteMenuList();
            }
            this.menuList = {};
            this.listDispFlag = false;
            console.log('[END]deleteAll.');
        },
        buckupMenuList : function() {
            console.log('[STRAT]buckupMenuList.');
            var buckupList = getMenuList();
            alert('以下の値をコピーして、メモ帳に保存してください。\n' + JSON.stringify(buckupList));
            console.log('[END]buckupMenuList.');
        },
        insertBuckup : function(){
            console.log('[STRAT]insertBuckup.');
            var buckupData = prompt('献立のバックアップデータをコピペしてください。','');
            if(buckupData == '' || buckupData == undefined || buckupData == null){
                alert('データを入力してください。');
                return;
            } 
            setMenuList(JSON.parse(buckupData));
            this.menuList = getMenuList();
            if(this.menuList == null || Object.keys(this.menuList).length == 0){
                this.listDispFlag = false;
            } else {
                this.listDispFlag = true;
            }
            console.log('[END]insertBuckup.');
        },
        desc : function(flag){
            console.log('[START]desc sort.')
            this.menuList = {};
            var list = getMenuList();
            var copyMenuList = [];
            for(menu in list) {
                copyMenuList.push(list[menu]);
            }

            if(flag == 'name') {
                copyMenuList.sort(function(a,b) {
                    if(a.name > b.name) return -1;
                    if(a.name < b.name) return 1;
                    return 0;
                })
            } else if (flag == 'genre') {
                copyMenuList.sort(function(a,b) {
                    if(a.genre > b.genre) return -1;
                    if(a.genre < b.genre) return 1;
                    return 0;
                })
            } else if (flag == 'date') {
                copyMenuList.sort(function(a,b) {
                    if(a.latestDate > b.latestDate) return -1;
                    if(a.latestDate < b.latestDate) return 1;
                    return 0;
                })
            }
            for(detail in copyMenuList) {
                this.menuList[copyMenuList[detail].name] = copyMenuList[detail];
            }
            console.log('[END]desc sort.')
        },
        asc : function(flag){
            console.log('[START]asc sort.')
            this.menuList = {};
            var list = getMenuList();
            var copyMenuList = [];
            for(menu in list) {
                copyMenuList.push(list[menu]);
            }

            if(flag == 'name') {
                copyMenuList.sort(function(a,b) {
                    if(a.name < b.name) return -1;
                    if(a.name > b.name) return 1;
                    return 0;
                })
            } else if (flag == 'genre') {
                copyMenuList.sort(function(a,b) {
                    if(a.genre < b.genre) return -1;
                    if(a.genre > b.genre) return 1;
                    return 0;
                })
            } else if (flag == 'date') {
                copyMenuList.sort(function(a,b) {
                    if(a.latestDate < b.latestDate) return -1;
                    if(a.latestDate > b.latestDate) return 1;
                    return 0;
                })
            }
            for(detail in copyMenuList) {
                this.menuList[copyMenuList[detail].name] = copyMenuList[detail];
            }
            console.log('[END]asc sort.')
        }
    },
    mounted: function(){
        for(genre in MENU_GENRE) {
            this.menuGenre.push(MENU_GENRE[genre]);
        }
        this.menuList = getMenuList();
        if(this.menuList == null || Object.keys(this.menuList).length == 0){
            this.listDispFlag = false;
        } else {
            this.listDispFlag = true;
        }
    },
    watch : {
        /**
         * ジャンル変更ウォッチャー
         * @param val
         */
        genre : function(val) {
            console.log('[START]search menu genre.')
            this.menuList = {};
            var copyMenuList = getMenuList();
            if(val == MENU_GENRE.ALL) {
                this.menuList = copyMenuList;
            } else {
                for(key in copyMenuList) {
                    var menuDetail = copyMenuList[key];
                    if(this.genre == menuDetail['genre']) {
                        this.menuList[menuDetail['name']] = menuDetail;
                    }
                }
            }
            if(this.menuList == null || Object.keys(this.menuList).length == 0){
                this.listDispFlag = false;
            } else {
                this.listDispFlag = true;
            }
            console.log('[END]search menu genre.')
        },
        /**
         * 検索ワードウォッチャー
         */
        searchWord : function(val) {
            console.log('[START]search menu word.')
            this.menuList = {};
            var copyMenuList = getMenuList();
            if(val == '' || val.length == 0) {
                this.menuList = copyMenuList;
            } else {
                for(key in copyMenuList) {
                    var menuDetail = copyMenuList[key];
                    if(menuDetail['name'].indexOf(val) >= 0) {
                        this.menuList[menuDetail['name']] = menuDetail;
                    }
                }
            }
            if(this.menuList == null || Object.keys(this.menuList).length == 0){
                this.listDispFlag = false;
            } else {
                this.listDispFlag = true;
            }
            console.log('[END]search menu word.')
        }
    }
})

以上、Vue.jsとLocalStorageを使って献立管理アプリを作成した内容のまとめでした。