山崎屋の技術メモ

IT業界で働く中で、気になること、メモしておきたいことを書いていきます。

【Vue】おれおれチュートリアル Ⅶ

第7弾です。

前回までの記事はこちら。
【Vue】おれおれチュートリアル Ⅰ - 山崎屋の技術メモ
【Vue】おれおれチュートリアル Ⅱ - 山崎屋の技術メモ
【Vue】おれおれチュートリアル Ⅲ - 山崎屋の技術メモ
【Vue】おれおれチュートリアル Ⅳ - 山崎屋の技術メモ
【Vue】おれおれチュートリアル Ⅴ - 山崎屋の技術メモ
【Vue】おれおれチュートリアル Ⅵ - 山崎屋の技術メモ

今回は見た目の改善を行います。

Vue.js入門 基礎から実践アプリケーション開発まで

Vue.js入門 基礎から実践アプリケーション開発まで

  • 作者: 川口和也,喜多啓介,野田陽平,手島拓也,片山真也
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/09/22
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る
基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

Todo リストの行の高さを調節する

第5回のチュートリアルで Element のストライプテーブルを使用して見た目をおしゃれにしましたが、行の高さが固定されていて調節の仕方が分からないと書きました。

↓↓↓ ちょっと1行1行が高いですよね。
f:id:yyama1556:20181014120006p:plain

最終的な HTML を確認すると、<table> タグ内の <td> に padding 12px のスタイルが定義されており、これが変更できなくて困っていました。

調べたら解決方法がありました!
ElementUI に Scoped CSS が適用されない問題対応策
スコープ付き CSS · vue-loader

Vue の ディープセレクタという機能を使うようです。

今回は「.list」クラス内の <td> タグの padding を 5px にします。Todo.vue の <style> タグ内の最後に以下の記述を追加します。

.list >>> td {
    padding: 5px;
}

これで子コンポーネントのスタイルを調整することができます。sass の場合は「>>>」の変わりに「/deep/」と記載します。
 
行間がいい感じになりました。
f:id:yyama1556:20181014121341p:plain

削除時にアニメーションを追加

現状、削除ボタンを押すと瞬時に対象の行が消えて、下の行が上に詰まってきます。

どの行が消えたのか分かりやすいようにゆっくりフェードアウトするようなアニメーションを付けたいと思います。


 
 
うーん、結構めんどくさいことになってしまいました。Element のコンポーネントに細かい動作を設定するのは結構はまります。ここは Element 使うべきではなかったかも知れません。

まず、<el-table> タグに row-class-name を追加します。生成される <tr> タグに class 属性が追加されます。JavaScript で DOM を取得するときに使います。

        <el-table
            :data="list"
            stripe
            class="list"
            row-class-name="todo-row"> <!-- 追加 -->

削除時に呼び出すメソッドの引数は scope.row ではなく、 scope 自体を渡すようにします。JavaScript で scope の $index プロパティにアクセスしたいので。
変更前:

                    <el-button @click="deleteTask(scope.row)" type="danger" plain size="small">削除</el-button>


変更後:

                    <el-button @click="deleteTask(scope)" type="danger" plain size="small">削除</el-button>


 
そして deleteTask メソッドは大幅に変更し、生の js でゴリゴリやっていきます。コメントを細かく入れたので、何の処理をしているのか確認してください。

        deleteTask(scope) {
            // TR タグを全て取得
            const els = document.getElementsByClassName('todo-row')
            
            // 削除ボタンが押された行に deleting クラスを追加
            els[scope.$index].classList.add('deleting')

            // this を別変数で保管
            const self = this

            // setTimeout に渡す関数を定義
            const f = function() { 
                // list から 削除する要素を削除
                self.list = self.list.filter(e => e !== scope.row)
                
                // 全ての TR タグから deleting クラスを削除
                for(var i=0; i < els.length; i++){
                    els[i].classList.remove('deleting');
                }
            }

            // 0.5 秒後に削除を実施する
            setTimeout(f, 500);
        }



最後に css で .delete クラスのアニメーション定義を追加します。0.5 秒をかけて、要素を透明にします。

@keyframes fadeout {
  0% { opacity: 1; }
  100% { opacity: 0; }
}
.list >>> .deleting {
    animation: fadeout 0.5s ease 0s forwards;
}

 
これで削除時にゆっくり消えてなくなるアニメーションが実装できました。
f:id:yyama1556:20181014152819p:plain

まとめ

今回はデザインの修正を行いました。

ここまでのコードは GitHub にあります。「oreore7」でタグが打ってあります。
GitHub - yyama694/ore-todo: 【vue 】おれおれチュートリアル

最後に Todo.vue の全量を乗せておきます。

<template>
    <div>
        <h1>Todo List</h1>
        <el-input ref="new_task" class="task-input" placeholder="追加するタスクを入力してください。" v-model="newTask" @keydown.enter.native="addTask"></el-input>
        <el-button type="primary" plain @click="addTask">追加</el-button>
        <el-table
            :data="list"
            stripe
            class="list"
            row-class-name="todo-row">
            <el-table-column style="padding: 8px">
                <template slot-scope="scope">
                    <span :class="{ complete: scope.row.isComplete }">{{ scope.row.value }}</span>
                </template>
            </el-table-column>
            <el-table-column>
                <template slot-scope="scope">
                    <el-button @click="scope.row.isComplete=true" type="primary" plain size="small" v-if="!scope.row.isComplete">達成</el-button>
                    <el-button @click="scope.row.isComplete=false" type="primary" plain size="small" v-if="scope.row.isComplete">戻す</el-button>
                    <el-button @click="deleteTask(scope)" type="danger" plain size="small">削除</el-button>
                </template>
            </el-table-column>
        </el-table>
    </div>
</template>
    
<script>
export default {
    data() {
        return {
            list: [
               { id:1, value: "たまご買う", isComplete: false }, 
               { id:2, value: "図書館に本を返す", isComplete: false },
               { id:3, value: "宅急便を受け取る", isComplete: false }
            ],
            newTask: "",
            nextId: 4
        }
    },
    methods: {
        addTask() {
            if(!this.newTask.trim()) {
                return
            }
            this.list.push({
                id: this.nextId++,
                value: this.newTask,
                isComplete: false 
            })
            this.newTask = ""
        },
        deleteTask(scope) {
            // TR タグを全て取得
            const els = document.getElementsByClassName('todo-row')
            
            // 削除ボタンが押された行に deleting クラスを追加
            els[scope.$index].classList.add('deleting')

            // this を別変数で保管
            const self = this

            // setTimeout に渡す関数を定義
            const f = function() { 
                // list から 削除する要素を削除
                self.list = self.list.filter(e => e !== scope.row)
                
                // 全ての TR タグから deleting クラスを削除
                for(var i=0; i < els.length; i++){
                    els[i].classList.remove('deleting');
                }
            }

            // 0.5 秒後に削除を実施する
            setTimeout(f, 500);
        }
    },
    mounted: function() {
        this.$refs.new_task.focus()
    } 
}
</script>

<style scoped>
.list {
    width: 80%;
    margin: auto;
    text-align: left;
}
.complete {
    text-decoration: line-through;
}
h1 {
    position: relative;
    line-height: 1.4;
}
h1:before { 
    font-family: "Font Awesome 5 Free";
    content: "\f00c";
    font-size: 0.7em;
    left: 0;
    top: 0;
    color: #5ab9ff;
}
.task-input {
    width: 60%;
}
.list >>> td {
    padding: 5px;
}
@keyframes fadeout {
  0% { opacity: 1; }
  100% { opacity: 0; }
}
.list >>> .deleting {
    animation: fadeout 0.5s ease 0s forwards;
}
</style>

それでは今日はここまで。ヴァ~イ。
 

Vue.js入門 基礎から実践アプリケーション開発まで

Vue.js入門 基礎から実践アプリケーション開発まで

  • 作者: 川口和也,喜多啓介,野田陽平,手島拓也,片山真也
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/09/22
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る
基礎から学ぶ Vue.js

基礎から学ぶ Vue.js