Vue.js/ファイラーっぽいUIを作ってみる/080_Storeを使う

Vue.js/ファイラーっぽいUIを作ってみる/080_Storeを使う

実は少しすっ飛ばしてから戻ってきている。

今回のファイラーのような多重構造の親子関係を持つモノを管理する場合にはそのやり取りにいくつも設定が必要で、 親子に対して複雑な繋ぎ込みを大量にする必要がある。 親子だけならいいが兄弟とかも入ってくると管理不能になる。

そこで、それぞれのデータを個別に管理するのをやめて、画面で1個だけしかデータが内容にするという設計にすることで、その繋がりを断ち切るという設計にする。

このような画面設計を Flux と呼ぶ。Java とかの DI コンテナとかも似たような考え方である。 依存性を複雑化させるより、DIコンテナ1個だけに依存することにして他の関係を断ち切るという設計だ。

この Flux を Vue.js で実現するためのライブラリが Vuex となる。

data を state へ移植する

Vuex ではその画面に1個しか無いとしているデータを state と呼んで管理する。

さらにこの state を管理する一式を store と呼ぶ。なので store を作りそこに state を設定する。

var store = new Vuex.Store({
    state: data
});

このようにして store オブジェクトを生成する。 一番最初に CDN から Vuex を読み込んでいるので使える。

データのの代わりに store を Vue オブジェクトに設置する

var vm = new Vue({
    el: "#app",
    data: data,
    store: store,

当然ながら何も使ってないし、前からの data はそのまま設置しているので、まだ普通に動作する。 基本的に store の state も data も同じモノを指しているのでどちらを操作しても同じはずである。

算出プロパティ

Vue オブジェクトが data に無い項目を data から作り出す時算出プロパティという関数を実装する。

var vm = new Vue({
    el: "#app",
    data: data,
    store: store,
    computed: {
        rootItems: function(){
            return this.$store.state.items;
        }
    },

computed という項目に使いたい名前で関数を登録する。 この名前が template の中では data の中身同様に扱える。

登録した store に関しては Vueオブジェクトには $store という名前でアクセス可能になっている。 これで store 中の state に登録した data にアクセスできる。

作った算出プロパティを使う

<div id="app">
    <ul>
        <li v-for="(item, i) in rootItems" v-bind:class="[item.type, {close:(!item.isOpen && item.type === 'folder')}]">

動かすと、以前と同様に動かせるということがわかる。

フォルダの開閉

開閉は data の isOpen を操作することで行った。ということで state の isOpen を操作すればよい。 しかしこれに少し制約がある。

store の state は mutations という関数から行わないといけないことになっている。

なので、現在 methods に実装されている toggleOpenClose を mutations に移植する。

現状の toggleOpenClose は findById を使っているが、このようなデータから特定のデータを抜き出すような関数は store では getters という関数に実装しなければならない。

なのでまず findById を store の getters へ移植する

getters でパラメータ付きの関数を実装する時は、state と getters を引数に取り、そのパラメータを利用する関数を返す関数を実装することになる。

このように実装する。

var store = new Vuex.Store({
    state: data,
    getters: {
        findById: (state, getters) => (_id, _item) => {
            if(_item == null){
                return getters.findById(_id, state);
            }
            if(_item.id === _id){
                return _item;
            }
            if(_item.type === 'file'){
               return null;
            }
            for(var i = 0; i < _item.items.length; i++){
                var tmp = getters.findById(_id, _item.items[i]);
                if(tmp != null){
                    return tmp;
                }
            }
            return null;
        }                 
    }
});

このような引数で拘束するだけを目的とした入れ子の関数はアロー関数を使うと簡素に書ける。

JavaScript / Basic / 関数 / アロー関数

この記述は

(state, getters) => (_id, _item) => {
    return
}

これと同じになる。

function(state, getters){
    return function(_id, _item){
        return;
    }
}

getters では Vue オブジェクトのように this でアクセスできず、引数でわたされたオブジェクト経由で行う。

使ってみる

console.log(store.getters.findById(5).name); // ddd

動いた。

では、mutations で toggleOpenClose を実装しよう・・・ と思ったらまた問題がある。 mutations は引数に state に加えて値を1個しか取らないのである。

つまりどういうことかというと mutations では getter が使えないのである。

ここで actions というモノが登場する。actions は Vueオブジェクトの methods みたいなモノで結構いろいろ書いていい。 こいつは引数も大量に取るし、行動にも制約が少ないのでこいつに toggleOpenClose を実装する。

この actions は引数に context と呼ばれる store の資産が詰まったオブジェクトとパラメータ1個だけ取る。 そして mutations の toggleOpenClose を呼び出すわけだがここにもまた制約がある。 どんだけ制約があるの mutations。

mutations は commit という専用のメソッド経由で間接的に呼び出さなければならないことになっている このメソッドは context にあるのでそれを使う。

var store = new Vuex.Store({
    state: data,
    // 中略
    actions: {
        toggleOpenClose: function(context, id){
            context.commit('toggleOpenClose', id);
        }
    }
});

しかしこれではまだ getters 使えない問題が解決していないのでこのようにする

var store = new Vuex.Store({
    state: data,
    // 中略
    actions: {
        toggleOpenClose: function(context, id){
            context.commit('toggleOpenClose', {getters: context.getters, id: id});
        }
    }
});

完全に屁理屈でしかないような気がするがこれで mutations で getters が使えるようになった。

ということで全体はこうなる。

var store = new Vuex.Store({
    state: data,
    mutations: {
        toggleOpenClose: function(state, {getters, id}){
            var item = getters.findById(id);
            if(item == null){
                return;
            }
            if(item.type !== 'folder'){
                return;
            }
            item.isOpen = !item.isOpen;
        },
    },
    getters: {
        findById: (state, getters) => (_id, _item) => {
            if(_item == null){
                return getters.findById(_id, state);
            }
            if(_item.id === _id){
                return _item;
            }
            if(_item.type === 'file'){
               return null;
            }
            for(var i = 0; i < _item.items.length; i++){
                var tmp = getters.findById(_id, _item.items[i]);
                if(tmp != null){
                    return tmp;
                }
            }
            return null;
        }                 
    },
    actions: {
        toggleOpenClose: function(context, id){
            context.commit('toggleOpenClose', {getters: context.getters, id: id});
        }
    }
});

このような関数の引数に中括弧が付いている記述は「部分代入」という記法で、オブジェクトが渡されてきたら、そのオブジェクトから選択的にキーと同名の変数にその値を入れるという記述である。

それでは動かしてみよう。 actions も mutations と同様に直接実行はできず dispatch というメソッド経由で実行することになる

console.log(store.getters.findById(7).isOpen);  // false
store.dispatch("toggleOpenClose", 7);
console.log(store.getters.findById(7).isOpen);  // true
store.dispatch("toggleOpenClose", 7);
console.log(store.getters.findById(7).isOpen);  // false

動いているようだ。

ではこいつを Vueオブジェクト内で呼んでいこう。こうなる。

var vm = new Vue({
    el: "#app",
    data: data,
    store: store,
    computed: {
        rootItems: function(){
            return this.$store.state.items;
        }
    },
    methods: {
        toggleOpenClose: function(_id){
            this.$store.dispatch("toggleOpenClose", _id);
        }
    }
});

こうなると、内部で data を使っている部分がまったくなくなるので、こうなる。

var vm = new Vue({
    el: "#app",
    store: store,
    computed: {
        rootItems: function(){
            return this.$store.state.items;
        }
    },
    methods: {
        toggleOpenClose: function(_id){
            this.$store.dispatch("toggleOpenClose", _id);
        }
    }
});

これで、ファイラーっぽいUI に Vuex を導入できたということだ。

まとめ

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Hello</title>
    <script src="https://unpkg.com/vue@latest"></script>
    <script src="https://unpkg.com/vuex@latest"></script>
    <style>
        #app li.folder.close ul{
            display: none;
        }
    </style>
</head>
<body>
    <div id="app">
        <ul>
            <!-- ■■■■■■■■ 今回はココ ■■■■■■■■ --> 
            <li v-for="(item, i) in rootItems" v-bind:class="[item.type, {close:(!item.isOpen && item.type === 'folder')}]">
                <template v-if="item.type === 'folder'">
                    <h2 v-on:click="toggleOpenClose(item.id)">{{ item.name }}</h2>
                    <ul>
                        <li v-for="(item, j) in item.items" v-bind:class="[item.type, {close:(!item.isOpen && item.type === 'folder')}]">
                            <template v-if="item.type === 'folder'">
                                <h2 v-on:click="toggleOpenClose(item.id)">{{ item.name }}</h2>
                                <ul>
                                    <li v-for="(item, k) in item.items" v-bind:class="[item.type, {close:(!item.isOpen && item.type === 'folder')}]">
                                        <template v-if="item.type === 'folder'">
                                            <h2 v-on:click="toggleOpenClose(item.id)">{{ item.name }}</h2>
                                            <ul>
                                                <li v-for="(item, l) in item.items">{{ item.name }}</li>
                                            </ul>                                            
                                        </template>
                                        <template v-else>{{ item.name }}</template>
                                    </li>
                                </ul>
                            </template>
                            <template v-else>{{ item.name }}</template>
                        </li>
                    </ul>
                </template>
                <template v-else>{{ item.name }}</template>
            </li>
        </ul>
    </div>
    <script>
        var data = {
            id: 1, name: "", type: "folder",
            isOpen: true,
            items: [
                {
                    id: 2, name: "aaa", type: "folder",
                    isOpen : false,
                    items: [
                        { id: 3, name: "bbb", type: "file" },
                        { id: 4, name: "ccc", type: "file" },
                        { id: 5, name: "ddd", type: "file" }
                    ]
                },
                { id: 6, name: "eee", type: "file" },
                {
                    id: 7, name: "fff", type: "folder",
                    isOpen: false,
                    items: [
                        { id: 8, name: "ggg", type: "file" },
                        {
                            id: 9, name: "hhh", type: "folder",
                            isOpen: false,
                            items: [
                                { id: 10, name: "iii", type: "file" },
                                { id: 11, name: "jjj", type: "file" }
                            ]
                        }
                    ]
                },
                { id: 12, name: "kkk", type: "file" }
            ]
        };
        // ■■■■■■■■ 今回はココ ■■■■■■■■
        var store = new Vuex.Store({
            state: data,
            mutations: {
                toggleOpenClose: function(state, {getters, id}){
                    var item = getters.findById(id);
                    if(item == null){
                        return;
                    }
                    if(item.type !== 'folder'){
                        return;
                    }
                    item.isOpen = !item.isOpen;
                },
            },
            getters: {
                findById: (state, getters) => (_id, _item) => {
                    if(_item == null){
                        return getters.findById(_id, state);
                    }
                    if(_item.id === _id){
                        return _item;
                    }
                    if(_item.type === 'file'){
                       return null;
                    }
                    for(var i = 0; i < _item.items.length; i++){
                        var tmp = getters.findById(_id, _item.items[i]);
                        if(tmp != null){
                            return tmp;
                        }
                    }
                    return null;
                }                 
            },
            actions: {
                toggleOpenClose: function(context, id){
                    context.commit('toggleOpenClose', {getters: context.getters, id: id});
                }
            }
        });
        
        // ■■■■■■■■ 今回はココ ■■■■■■■■
        console.log(store.getters.findById(5).name);
        console.log(store.getters.findById(7).isOpen);
        store.dispatch("toggleOpenClose", 7);
        console.log(store.getters.findById(7).isOpen);
        store.dispatch("toggleOpenClose", 7);
        console.log(store.getters.findById(7).isOpen);
        
        
        var vm = new Vue({
            el: "#app",
            store: store,  // ■■■■■■■■ 今回はココ ■■■■■■■■
            // ■■■■■■■■ 今回はココ ■■■■■■■■
            computed: {
                rootItems: function(){
                    return this.$store.state.items;
                }
            },
            methods: {
                toggleOpenClose: function(_id){
                    // ■■■■■■■■ 今回はココ ■■■■■■■■
                    this.$store.dispatch("toggleOpenClose", _id);
                }
            }
        });
    </script>
</body>
</html>
javascript/vuejs/create_filer_like_ui/080_use_store.txt · 最終更新: 2019-06-10 12:17 by ore