20200319のvue.jsに関する記事は13件です。

Vueの$routeがどうしてもundefinedになってしまう

反省文

Vueでルーターの設定をしようとしていましたが、なぜか何をやっても設定がVueに反映されません。

下のどちらかのコードには間違いがあります。

a.js
import router from "./router";

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");
b.js
import Router from "./router";

new Vue({
  Router,
  render: h => h(App)
}).$mount("#app");

a.jsの方は動きますが、b.jsの方は動きません。動かそうとするとこんなエラーが出て止まります。

vue.esm.js?efeb:628 [Vue warn]: Error in render: "TypeError: Cannot read property 'meta' of undefined"

found in

---> <App> at src/App.vue
       <Root>

上記の該当箇所はこんなかんじです。

export default {
  computed: {
    layout() {
      console.log(this);
      return this.$route.meta.layout || "default"; // << ココでエラー発生
    }
  }
};

$routeに何も代入されていません。

この様に書き直すと、今までのエラーが嘘の様に動き出しました。

b.js
import Router from "./router";

new Vue({
  router: Router,
  render: h => h(App)
}).$mount("#app");

routerと、オブジェクトのキーをちょいと書き加えてやるだけ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.Draggableの子要素内におけるドラッグ&ドロップで気をつけたいこと

Vue.Draggable大変便利です。いつもお世話になっております。
https://github.com/SortableJS/Vue.Draggable

今日は子要素内におけるドラッグ&ドロップ(以下D&D)時に自分が嵌まってしまった点についての備忘録です。

前置き

  • プロジェクト作成
  • 親子コンポーネントの作成
  • App.vueの修正
  • 実行

プロジェクト作成

さくっとvuecliでプロジェクトを作成します。設定内容はdefaultです。

$ vue create my-vue-draggable

続いて、vuedraggableのインストール

$ npm i -S vuedraggable

親子コンポーネントの作成

componentsディレクトリ下にParent.vueとChild.vueを作成します。

src/components/Parent.vue
<template>
    <div>
        <h3>list1を表示</h3>
        <draggable v-model="list1" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list1" :key="item">{{ item }}</div>
        </draggable>

        <h3>list2を表示</h3>
        <draggable v-model="list2" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list2" :key="item">{{ item }}</div>
        </draggable>

        <h3>listForChildを表示</h3>
        <Child :list-from-parent="listForChild" v-on:update="listForChild = $event;"></Child>
    </div>
</template>

<script>
import draggable from 'vuedraggable';

import Child from '@/components/Child.vue'

export default {
    name: "Parent",
    components: {
        draggable,
        Child
    },
    data: ()=> {
        return {
            list1: ['task1-1', 'task1-2', 'task1-3', 'task1-4'],
            list2: ['task2-1', 'task2-2', 'task2-3', 'task2-4'],
            listForChild: ['task3-1']
        }
    },
}
</script>

<style>
.dev_flex {
    display: flex;
}

.dev_setting-area {
    color: #fff;
    border: 6px double #fff;
    background: #464646;
    border-radius: 10px;
    margin: .5rem;
    padding: .5rem;
    height: 16mm;
}

.dev_back {
    margin: .1em;
    width: 15mm;
    height: 15mm;
    text-decoration: none;
    color: #67c5ff;
    border: solid 1px #67c5ff;
    border-radius: 3px;
    transition: .4s;
}
</style>
src/components/Child.vue
<template>
    <div>
        <h4>list3を表示</h4>
        <draggable v-model="list3" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list3" :key="item">{{ item }}</div>
        </draggable>
    </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
    name: "Child",
    components: {
        draggable,
    },
    props: ['listFromParent'],
    computed: {
        list3: {
            get: function () {
                return this.listFromParent.concat();
            },
            set: function (list) {
                this.$emit('update', list);
            }
        },
    },
}
</script>

App.vueの修正

Parent.vueを表示するように少し修正します。

src/App.vue
<template>
  <div id="app">
    <Parent></Parent>
  </div>
</template>

<script>
import Parent from '@/components/Parent.vue'

export default {
  name: 'App',
  components: {
    Parent
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

実行

ここまでで一旦起動する準備ができたので、実行します。

npm run serve

ローカルホスト( http://localhost:8080/ )へアクセスして下記が表示されればOKです。

スクリーンショット 2020-03-19 19.44.03.png

下記は適当にD&Dを行った結果です。

スクリーンショット 2020-03-19 19.46.05.png

親同士や親子間でのD&Dが問題なく成功します。

本題

続いて、子要素内にもう1つリストを設けます。
このときリストには下記の制限を設けます。

  • 既存のlist3にはtaskの末尾が1or2のものを表示する
  • 新規のlist4にはtaskの末尾が3or4のものを表示する
  • 親が持つlistForChildは増やさない

こちらを踏まえて、Child.vueを変更します。

src/components/Child.vue
<template>
    <div>
        <h4>list3を表示</h4>
        <draggable v-model="list3" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list3" :key="item">{{ item }}</div>
        </draggable>

        <h4>list4を表示</h4>
        <draggable v-model="list4" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list4" :key="item">{{ item }}</div>
        </draggable>
    </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
    name: "Child",
    components: {
        draggable,
    },
    props: ['listFromParent'],
    computed: {
        list3: {
            get: function () {
                return this.listFromParent.concat().filter((task)=>{
                    const lastword = task.slice(-1);
                    return lastword === '1' || lastword === '2';
                });
            },
            set: function (list) {
                this.$emit('update', this.list4.concat(list));
            }
        },
        list4: {
            get: function () {
                return this.listFromParent.concat().filter((task)=>{
                    const lastword = task.slice(-1);
                    return lastword === '3' || lastword === '4';
                });
            },
            set: function (list) {
                this.$emit('update', this.list3.concat(list));
            }
        },
    },
}
</script>

再度実行し、ブラウザで確認します。

スクリーンショット 2020-03-19 19.54.16.png

親から子へ適当にD&Dを行います。このときlist3へ末尾が3,4のものを送っても、list4に表示され、list4へ末尾が1,2のものを送ってもlist3に表示されます。

スクリーンショット 2020-03-19 19.56.28.png

ただ、子要素内にてD&Dを行うと要素が削除されます。
本来であればD&Dしても格納されているlistに変化がないことが期待動作です。

スクリーンショット 2020-03-19 19.58.04.png

原因(憶測)と解決策

親要素へのemitが別々のcomputedから同時に発火されるとこのような現象が発生するようです。根拠となるissueなどが見つからなかったので、憶測ではありますが。。。

Child.vueを修正します。具体的にはcomputedではなくwatchを使います。

src/components/Child.vue
<template>
    <div>
        <h4>list3を表示</h4>
        <draggable v-model="list3" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list3" :key="item">{{ item }}</div>
        </draggable>

        <h4>list4を表示</h4>
        <draggable v-model="list4" :options="{ group: 'tasks' }" class="dev_flex dev_setting-area">
            <div class="dev_back" v-for="(item) in list4" :key="item">{{ item }}</div>
        </draggable>
    </div>
</template>

<script>
import draggable from 'vuedraggable';

export default {
    name: "Child",
    components: {
        draggable,
    },
    props: ['listFromParent'],
    watch: {
        listFromParent: function (newlist, oldlist) {
            if (!(newlist instanceof Array) || !(oldlist instanceof Array)) {
                return;
            }
            this.list3 = this.listFromParent.filter((task) => {
                const lastword = task.slice(-1);
                return lastword === '1' || lastword === '2';
            });
            this.list4 = this.listFromParent.filter((task) => {
                const lastword = task.slice(-1);
                return lastword === '3' || lastword === '4';
            });
        },
        list3: function (newlist, oldlist) {
            if (!(newlist instanceof Array) || !(oldlist instanceof Array)) {
                return;
            }
            if (newlist.length === oldlist.length) {
                return;
            }
            this.$emit('update', newlist.concat(this.list4));
        },
        list4: function (newlist, oldlist) {
            if (!(newlist instanceof Array) || !(oldlist instanceof Array)) {
                return;
            }
            if (newlist.length === oldlist.length) {
                return;
            }
            this.$emit('update', newlist.concat(this.list3));
        },
    },
    data: ()=> {
        return {
            list3: [],
            list4: [],
        }
    },
}
</script>

これで子要素内におけるD&Dが実現できました。

なお、watchを使う際の注意点として、watchのlist3,list4にて次の記述があります。

            if (newlist.length === oldlist.length) {
                return;
            }

list内に変更がない場合はその後の処理を行わない制御となっております。
こちらがないと無限ループに陥るため、お気をつけください。。。(←実際やった人)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラスのバインディング

バインディングとは

データと対象を結びつけること。
ここでいうバインディングとはjavascript(typescript)で定義しているデータをHTMLタグ(CSSや属性)に紐づけること。

クラスのバインディング

Vueではv-bindディレクティブを用いてバインディングすることでクラスを動的に切り替えること(クラスを付与したり外したり)ができる。

基本構文

<div v-bind:"{ クラス名: データプロパティ }"Hello</div>
<div v-bind:"{ active: isActive }">Hello</div>

データプロパティの真偽値によってクラスの有無が決まる
isActiveの値がtrueであればクラスが有効になり、falseであればクラスは無効になる。

プレーンなclass属性との併用も可能
sample.vue
<template>
  <div
    class="static"
    v-bind:class="{ active: isActive }"
  >
  </div>
</template>

isActiveの値がtrueであればactiveクラスが追加され下記のようになる

sample.vue
<template>
  <div class="static active"></div>
</template>

スタイルをつけて動きをみてみる

sample.vue
<template>
  <div id="app">
    <div
      class="static"
      v-bind:"{ active : isActive }"
    >Hello</div>
  </div>
</template>

<script>
  new Vue({
    el: '#app',
    data: {
      isActive : true,
    },
  )}
</script>

<style>
  .active {
    color: red;
  }
</style>

activeクラスが付与されたので「Hello」が赤色の文字で表示される

クラスをバインディングしてモーダルを表示/非表示
sample.vue
<template>
  <div class="SamplePage_message">
    <p>メッセージを表示</p>
    <!-- クリックイベント handleModalOpen関数が呼び出される -->
    <button class="SamplePage_messageButton" @click="handleModalOpen">
      Open
    </button>
  </div>
  <!-- ここからモーダル -->
 <!-- クラスのバインディング -->
 <!-- 論理否定が指定されているので返ってきたisModalVisibleを逆の値で返す -->
  <div
  class="SamplePage_modal"
  :class="{ 'SamplePage_modal--hidden' : !isModalVisible }"
  >
    <div class="SamplePage_modalContent">
      <h1 class="SamplePage_modalContentTitle">
        Hello!
      </h1>
    <!-- クリックイベント handleModalClose関数が呼び出される -->
      <button 
      class="SamplePage_modalContentButton"
      @click="handleModalClose"
      >
        Close
      </button>
    </div>
  </div>
</template>

<script>
export default class SamplePage extends Vue {
 // 初期値をfalseで設定
  public isModalVisible = false;
 // handleModalOpen関数が呼び出されたらisModalVisibleをtrueで返す
  public handleModalOpen() {
    this.isModalVisible = true;
  }
  // handleModalClose関数が呼び出されたらisModalVisibleをfalseで返す
  public handleModalClose() {
    this.isModalVisible = false;
  }
}
</script>

<style lang="scss>
.SamplePage {
  &__message {
    text-align: center;
    margin: 40px 0;
  }
  &__modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.75);
    &--hidden {
       display: none;
    }
    &Content {
      width: 80%;
      margin: 20px auto;
      padding: 16px;
      background-color: #f2f2f2;
      text-align: center;
    }
  }
}
</style>

※ここではv-bind:classを省略して:classと記述している
※クラス名にハイフンやアンダーバーを含む場合、' '(シングルコーテーション)で囲む必要がある

インラインで記述しなくてもOK

v-bind:classの{ active : isActive }の部分はデータプロパティ名を入れて表現することもできる。

<div v-bind:class="classObject">Hello</div>
data.js
data: {
  classObject: {
    active: true,
  },
},

配列構文

v-bind:classに配列を渡してクラスのリストを適用することも可能

<div v-bind:class="[activeClass, errorClass]"></div>
data.js
data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

activeClassとerrorClassどちらも常にtrueということになるので下記のようなコードになる

<div class="activeClass errorClass"></div>

三項演算子式を用いる

配列内のクラスを条件に応じて切り替えたい場合は三項演算式を用いる。

<div v-bind:class="[isActive ? activeClass : '',errorClass]"></div>

この場合、errorclassクラスは常に適用されている状態だが、activeClassクラスはisActiveがtrueの場合にだけ適用される。

三項演算子式

if文の省略形、または代替文。3つの値と式を必要とする特殊な演算子。条件に基づいて2つの値のうち1つを選ぶ。

基本構文

条件式 ? trueの処理 : falseの処理
condition ? active : error

・conditionでtrueかfalseか判断
・trueの場合はactiveの処理が実行
・falseの場合はerrorの処理が実行

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue CLI × Laravel × VuetifyでCSSいらずの爆速開発でTodoリストを作る part9 ~最終回~

前回のパート

前回はtodo削除機能の実装を行いました
今回は残す最後の機能として、todoの更新機能の実装をやっていきます
今回が最後のパートになります
前回のパートはこちら→Vue CLI × Laravel × VuetifyでCSSいらずの爆速開発でTodoリストを作る part8

Laravel側の実装

todoの更新に使うのはTodoController.phpupdate()アクションになります
update()アクションは/todo{id}に対してPUTリクエストで呼び出されます

それではTodoController.phpを開きupdate()アクションに下記を追加してください

TodoController.php
public function update(Request $request, $id)
{
    $todo = Todo::find($id);
    $todo->text = $request->text;
    $todo->save();

    return response('', 200);
}

引数の$idを受け取ってtodosテーブルからそのidに該当するレコードを所得して
$request->textで送られてきたデーターに更新しています
Laravel側の実装はこれで終了です

Vue側の実装

Todo.vueを開き下記のように変更と追加をしてください

Todo.vue
<template>
    <div>
        <Header />
        <div class="main-container">
            {{ user.name }}のTodoリスト

            <v-text-field v-model="text" label="todo" required></v-text-field>
            <v-btn class="btn" @click="create">登録</v-btn>

            <template v-for="item in items">
                <v-card max-width="450" class="mx-auto" style="marign-top: 10px" :key="item.id">
                    <v-list three-line>
                        <v-list-item :key="item.id">
                            <v-list-item-content>
                                <input type="text" v-model="item.text" @keydown.enter="update(item.id, item.text)">    //追加
                                <v-btn small @click="del(item.id)">完了</v-btn>
                            </v-list-item-content>
                        </v-list-item>
                    </v-list>
                </v-card>
            </template>
        </div>

    </div>
</template>

<script>
    import Header from './Header';
    import axios from 'axios';

    export default {
        components: {
            Header
        },
        metaInfo: {
            title: 'Todo',
            htmlAttrs: {
                lang: 'ja'
            }
        },
        created () {
            const user = this.$store.getters['auth/user'];
            if (user === null) {
                this.$router.push('/login');
            }

            axios.get('/api/todo/' + this.user.id)
            .then((res) => {
                this.items = res.data;
            });
        },
        computed: {
            user() {
                return this.$store.getters['auth/user'];
            }
        },
        data () {
            return {
                items: null,
                text: ''
            }
        },
        methods: {
            async create() {
                await axios.post('/api/todo', { user_id: this.user.id, text: this.text });
                this.$router.go({ path: this.$router.currentRoute.path, force: true });
            },
            async del(id) {
                await axios.delete('/api/todo/' + id);
                this.$router.go({ path: this.$router.currentRoute.path, force: true });
            },
            async update(id, text) {    //追加
                if (event.keyCode !== 13) {
                    return
                }

                await axios.put('/api/todo/' + id, { text: text });
                this.flg = false;
                this.$router.go({ path: this.$router.currentRoute.path, force: true });
            }
        }
    }
</script>

<style>
    .main-container {
        width: 500px;
        margin: auto;
    }
    .btn {
        margin-bottom: 20px;
    }
</style>

2箇所の追加を行いました
まずこの部分ですが
<input type="text" v-model="item.text" @keydown.enter="update(item.id, item.text)">
input要素に内容が書き込まれEnterキーを押すことでupdate()メソッドが発火するようにしました
次にこの部分ですが

Todo.vue
            async update(id, text) {   
                if (event.keyCode !== 13) {
                    return
                }

                await axios.put('/api/todo/' + id, { text: text });
                this.$router.go({ path: this.$router.currentRoute.path, force: true });
            }

if文の箇所ですが、この部分がなければtodoの変更でinputに日本語の入力をした際に
変換などでEnterキーを押すとそれで発火してしまいます
そのためif文での処理を追加しています
日本語入力の際の変換でのEnterをKeyCodewを299を返します
日本語入力の変換でのEnterではない(今回の場合はtodoの変更のEnter)の場合はKeyCodeを13を返します
本来発火させたいのは、後者のKeyCode13の場合です
なのでupdate()メソッドの最初でこのEnterキーは13なのかどうかをチェックしています
この処理がなければ、Enterを押すたびに処理が走ってしまうため入力途中のものなどが登録されることになりますので注意してください

次にLaravel側の/todoに対してtodoのidと変更内容をPUTでリクエストしています
これで、先ほど実装したLaravel側でのTodoController.phpupdate()アクションを呼び出すことができます

ここまでの実装で、todoの更新ができるようになっています
表示されているtodoをクリックし、違うものに書き換えてEnterキーを入力してみてください
正常に実装できていれば、todoが編集され画面に出力されているかと思います

以上で本記事での全ての機能の実装が終わりました
会員登録、ログイン、ログアウト、todoの一覧表示、todoの新規作成、todoの削除、todoの更新
この機能を組み合わせればTodoリスト意外の開発でも同じ要領で色々な物が作れるかと思います

最後に

今回は最後のパートとしてtodoの更新機能の実装を行いました
ここまでの9パート最後まで読んでいただきありがとうございました
少しでも、学習者の役に立てればと思いこの記事を作成しました
また、本記事では未熟である私自身が書いた記事のため
まだまだ、改善点やリファクタリング等もできるかと思いますので
このチュートリアルを進めて頂いた方々個人で取り組んで頂ければと思います。
また、間違った点や気になった点アドバイスなど御座いましたら
是非コメント頂ければと思っておりま
ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js (universal) で vuejs-datepicker を使う

import DatePicker from 'vuejs-datepicker';

では mode: universal で動かないので、ひと手間加える

  • nuxt 2.11.0
  • vuejs-datepicker 1.6.2

そのままでは動かない理由

import すると vuejs-datepicker/dist/vuejs-datepicker.js を参照するが、
<head> に css を注入するコードがある

node_modules/vuejs-datepicker/dist/vuejs-datepicker.js
  var HEAD = document.head || document.getElementsByTagName('head')[0];

SSR 時に document は存在しないので、エラーになる

動くようにする

stylus インストール

vuejs-datepicker はスタイリングに stylus を使用しているので扱えるようにする
下記のコマンドだけで大丈夫だった

yarn add --dev stylus stylus-loader

import を変更

vuejs-datepicker/src 内の .vue を直接 import する

ExampleComponent.vue
<template>
  <date-picker />
</template>

<script>
import DatePicker from 'vuejs-datepicker/src/components/Datepicker.vue';

export default {
  components: {
    DatePicker,
  },
};
</script>

おまけ: locale も src から読み込む

ExampleComponent.vue
<template>
  <date-picker :language="ja" />
</template>

<script>
import DatePicker from 'vuejs-datepicker/src/components/Datepicker.vue';
import ja from 'vuejs-datepicker/src/locale/translations/ja';
// 下記は、ja以外の言語もすべてバンドルされてしまうので推奨されない
// import { ja } from 'vuejs-datepicker/src/locale';

export default {
  components: {
    DatePicker,
  },
  data() {
    return {
      ja,
    };
  },
};
</script>

おまけ: IE11 対応

基本的にライブラリ内のコンポーネントはコンパイルされず、IE11 に対応したコードが出力されない
nuxt.config.js の transpile に追加する

nuxt.config.js
export default {
  build: {
    transpile: ['vuejs-datepicker'],
  },
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue CLI × Laravel × VuetifyでCSSいらずの爆速開発でTodoリストを作る part8

前回のパート

前回はtodoの一覧表示機能を作成しました
今回はtodoの完了時に押す削除機能を実装していきます
前回のパートはこちらVue CLI × Laravel × VuetifyでCSSいらずの爆速開発でTodoリストを作る part7

Laravel側の実装

todoの削除を行うのはTodoController.phpdestroy()アクションを利用します
destroy()アクションの中身の実装をやっていきます
TodoController.phpを開きdestroy()アクションに下記を追加してください

TodoController.php
public function destroy($id)
{
    Todo::find($id)->delete();
    return response('', 200);
}

これでdestroy()アクションが呼び出されたら、引数で受け取った該当するtodoの削除を行なえるようになりました
destroy()アクションは/todo/{id}の形でDELETEリクエストをすることで呼び出すことができるので
Vue側でそのtodoのidを付与しリクエストするよに実装します

Vue側の実装

Todo.vueを開き下記を追加してください

Todo.vue
<template>
    <div>
        <Header />
        <div class="main-container">
            {{ user.name }}のTodoリスト

            <v-text-field v-model="text" label="todo" required></v-text-field>
            <v-btn class="btn" @click="create">登録</v-btn>

            <template v-for="item in items">
                <v-card max-width="450" class="mx-auto" style="marign-top: 10px" :key="item.id">
                    <v-list three-line>
                        <v-list-item :key="item.id">
                            <v-list-item-content>
                                <input type="text" v-model="item.text">
                                <v-btn small @click="del(item.id)">完了</v-btn>    //追加
                            </v-list-item-content>
                        </v-list-item>
                    </v-list>
                </v-card>
            </template>
        </div>

    </div>
</template>

<script>
    import Header from './Header';
    import axios from 'axios';

    export default {
        components: {
            Header
        },
        metaInfo: {
            title: 'Todo',
            htmlAttrs: {
                lang: 'ja'
            }
        },
        created () {
            const user = this.$store.getters['auth/user'];
            if (user === null) {
                this.$router.push('/login');
            }

            axios.get('/api/todo/' + this.user.id)
            .then((res) => {
                this.items = res.data;
            });
        },
        computed: {
            user() {
                return this.$store.getters['auth/user'];
            }
        },
        data () {
            return {
                items: null,
                text: ''
            }
        },
        methods: {
            async create() {
                await axios.post('/api/todo', { user_id: this.user.id, text: this.text });
                this.$router.go({ path: this.$router.currentRoute.path, force: true });
            },
            async del(id) {    //追加
                await axios.delete('/api/todo/' + id);
                this.$router.go({ path: this.$router.currentRoute.path, force: true });
            }
        }
    }
</script>

<style>
    .main-container {
        width: 500px;
        margin: auto;
    }
    .btn {
        margin-bottom: 20px;
    }
</style>

完了ボタンにクリックイベントを配置しました
イベントで呼び出されるメソッドの引数にitem.idとして、そのtodoのidを渡しています
後はメソッド側でLaravel側の/todo/{id}としてDELETEリクエストを送信して、store()アクションを呼び出し
削除処理を実装しています

ここまでの実装で実際にtodoの削除ができるようになっています
登録したタスクの完了ボタンをクリックし、そのtodoが消えればOKです
確認してみましょう

これで削除機能の実装は終わりです

終わりに

今回はtodoの削除機能の実装を行いました
次回はtodoの更新機能の実装を行います
次回が最後のパートになります

次回のパート→Vue CLI × Laravel × VuetifyでCSSいらずの爆速開発でTodoリストを作る part9 ~最終回~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue CLI × Laravel × VuetifyでCSSいらずの爆速開発でTodoリストを作る part7

前回のパート

前回は、todoの新規登録機能を実装しました
今回は前回の登録した内容を表示する機能の実装を行っていきます
前回のパートはこちら→Vue CLI × Laravel × VuetifyでCSSいらずの爆速開発でTodoリストを作る part6

Laravel側の実装

TodoController.phpshow()アクションに機能を追加していきます
show()/todo/{id}の形でGETリクエストを送ることで呼び出されます
show()に下記を追加してください

TodoController.php
public function show($id)
{
    $data = Todo::where('user_id', $id)->get();
    return $data;
}

引数で受け取ったuser_idと一致するレコードを所得しているだけの処理になります
Laravel側の処理は以上です

次にVue側でそれを所得し表示させます

Vue側の実装

Todo.vueを開き下記を追加してください

Todo.vue
<template>
    <div>
        <Header />
        <div class="main-container">
            {{ user.name }}のTodoリスト

            <v-text-field v-model="text" label="todo" required></v-text-field>
            <v-btn class="btn" @click="create">登録</v-btn>

            <template v-for="item in items">
                <v-card max-width="450" class="mx-auto" style="marign-top: 10px" :key="item.id">
                    <v-list three-line>
                        <v-list-item :key="item.id">
                            <v-list-item-content>
                                <input type="text" v-model="item.text">
                                <v-btn small>完了</v-btn>
                            </v-list-item-content>
                        </v-list-item>
                    </v-list>
                </v-card>
            </template>
        </div>

    </div>
</template>

<script>
    import Header from './Header';
    import axios from 'axios';

    export default {
        components: {
            Header
        },
        metaInfo: {
            title: 'Todo',
            htmlAttrs: {
                lang: 'ja'
            }
        },
        created () {
            const user = this.$store.getters['auth/user'];
            if (user === null) {
                this.$router.push('/login');
            }

            axios.get('/api/todo/' + this.user.id)    //追加
            .then((res) => {
                this.items = res.data;
            });
        },
        computed: {
            user() {
                return this.$store.getters['auth/user'];
            }
        },
        data () {
            return {
                items: null,
                text: ''
            }
        },
        methods: {
            async create() {
                await axios.post('/api/todo', { user_id: this.user.id, text: this.text });
                this.$router.go({ path: this.$router.currentRoute.path, force: true });
            }
        }
    }
</script>

<style>
    .main-container {
        width: 500px;
        margin: auto;
    }
    .btn {
        margin-bottom: 20px;
    }
</style>

画面がレンダリングされる際に、created()を使い/api/todo/にgetリクエストを行い
Laravel側のルートで定義された/todostore()アクションを呼び出すようにしています
store()では、その指定されたユーザーのIDから該当するデーターを所得してreturnするように実装したので
Vue側ではそれを受け取り、dataのitemsに格納しています
後は、そのitemsの中身をv-forでループさせて表示させています

ここまでの実装で、前回登録した内容のデーターが表示されていれば
正常にできていることになります

また、一度todoの登録を行い新規で登録したものが追加されて表示されているか
確認してみてください

終わりに

今回はtodoの一覧表示機能を実装しました
残る機能は、todoの削除とtodoの更新になります
次回はtodoの削除(完了)を実装していこうと思います

次回のパートはこちら→Vue CLI × Laravel × VuetifyでCSSいらずの爆速開発でTodoリストを作る part8

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js入門5(私的メモ)

Vue Routerの活用

公式プラグインのVue Routerを使うと、SPAを始めURL遷移を伴う動作を簡単に実現できる。

Vue RouterによるSPA

SPA・・・最初に一つのHTMLをロードし、以後はユーザーのアクションに応じてAjax(非同期通信)で情報を取得し、動的にページを更新するアプリケーションのこと

通常は、ページ遷移時に指定するURLに応じてサーバーにアクセスし、HTMLをロードする。

SPAでは、ページ遷移をクライアントサイドで行う。その際に、Ajaxで必要なデータを取得しビューを更新する。

SPAの実装で考慮すべき点

  • クライアントサイドでの履歴管理などを含めたページ遷移
  • 非同期によるデータの取得
  • Viewのレンダリング
  • モジュール化されたコードの管理

だが、ルーター、ルーティングライブラリと呼ばれるモジュールを用いれば、これらを担ってくれる。

Vue Routerとは

Vue Routerとは、Vue.jsの公式プラグインとして提供されているSPA構築のためのルーティングライブラリである。

URLにアクセスした時やリンクにクリックした時に、対応するVue.jsのコンポーネントがアクティブになる、という仕組みで動作する。

ページ遷移以外にも

  • ネストしたルーティング
  • リダイレクトとエイリアス
  • HTML5 History APIとURL Hashによる履歴管理
  • 自動的にCSSクラスがアクティブになるリンクの仕組み
  • Vue.jsのトランジションの仕組みを使用した遷移時のトランジション
  • スクロールの振る舞いのカスタマイズ

などの高度な機能も提供されている。

ルーティングの基礎

ルーターのインストール

CDNで読み込む。

インストール | Vue Router

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<script src="./main.js"></script>

ルーティング設計

ルーターコンストラクタにルートを定義することでルーティングが実装できる。

???

ルート定義・・・パスとそのパスにアクセスした際に表示させるコンポーネントを指定したオブジェクト

const topPage = {
  path: "/top", //URLを指定
  component: {   //表示させるコンポーネントを指定
    template: "<p>トップページ</p>"
  }
};

const Login = {
  template: "<p>ログインページ</p>",
};

const loginPage = {
  path: "/login",
  component: Login,
};

ルーターコンストラクタ・・・ルート定義を配列に渡して仕事をしてもらえるような状態にする

const router = new VueRouter({
  routes: [ //ルート定義を配列で渡す
    topPage,
    loginPage,
  ],
});

ルーターコンストラクタで作成したインスタンスをrootとなるVueインスタンスに登録することで、アプリケーション全体がルーターを認知できるようになる。

let vm = new Vue({
  router: router,
});

ルート定義で指定したコンポーネントを反映させる要素を記述する。

実際にページ遷移させるためのリンクはrouter-link要素に定義し、マッピングしたコンポーネントがrouter-view要素にレンダリングされる。

<router-link to="/top">トップページ</router-link>
<router-link to="/login">ログイン</router-link>
<router-view></router-view>

実践的なルーティングのための機能

Vue Routerに備わっている機能を確認。

URLパラメータとパターンマッチング

アクセスするURLのパラメータを/user/:userIdというような形で受け付け、パラメータの値に応じて表示を切り替えたい場合。

pathのURLに : を使用してパターンを記述する。
マッチしたパラメータはコンポーネント内の$route.paramsから受け取れる。

const Login = {
  template: "<p>ユーザーID{{ $route.params.userId }}がログインしました</p>",
};

const loginPage = {
  path: "/login/user/:userId",
  component: Login,
};

.../login/user/21 にアクセス。

スクリーンショット 2020-03-19 13.54.52.png

名前付きルート

パラメータ付きURLにページ遷移したい場合、静的にHTML側に記述することはできない。
このようなとき、ルートに名前を付与することで解決する。

名前付きルートを呼び出す場合は、router-link要素のtoパラメータ(:to)に指定する。

const Login = {
  template: "<p>ユーザーID{{ $route.params.userId }}がログインしました</p>",
};

const loginPage = {
  path: "/login/user/:userId",
  name: "loginUser",
  component: Login,
};
<section class="chapter">
  <router-link to="/top">トップページ</router-link>
  <router-link :to="{ name: 'loginUser', params: { userId:55 } }">ログイン</router-link>
  <router-view></router-view>
</section>

router.pushを使用した遷移

router-link要素はHTML側での宣言的な書き方であるが、router.pushを使用すればjs側での遷移も可能。
引数にはrouter-link要素に渡したオブジェクトと同じものを渡す。

router.push({ name: "userId", params: { userId:33 } });

フック関数

ページ遷移が実行される前後に処理を追加することができる関数をフック関数と呼ぶ。

リダイレクトや遷移前の確認などに利用される。

グローバルのフック関数、ルート単位のフック関数、コンポーネント内のフック関数が存在する。

●グローバルのフック関数

全てのページ遷移に対して設定できるフック関数。
router.beforeEachに関数をセットすると、ページ遷移が起こる直前に関数が実行される。

引数に渡す関数の引数には、to(遷移先のルート)とfrom(遷移元のルート), next(フック処理)が渡される。

toとfromに格納されるルートは、マッチしたルートのパスやコンポーネントの情報を持つ。

●ルート単位のフック関数

特定のルート単位でフックを追加するには、Vue Router初期化時のルート定義の際に個別に設定する。

const loginPage = {
  path: "/login/user/:userId",
  name: "loginUser",
  component: Login,
  beforeEnter: function(to, from, next) {
    //遷移前の処理
  },
};

●コンポーネント内のフック関数

コンポーネント側でもオプションとしてbeforeRouteEnterを使用してフックを追加することができる。

他にも、次の遷移の発生でコンポーネントが去って行くタイミングで発火するフック関数beforeRouteLeaveも利用できる。
例えば、保存していない変更がある場合にページ遷移するとき、確認画面を実装するなど。

詳しくは公式サイトを見る。

その他

APIによるデータ通信

SPAでは、ページ遷移をした際にAPIを通じて取得したデータを表示することが頻繁にある。

このような場合、コンポーネントのcreatedとwatchを使用して実装するのが一般的。

watchで$routeを監視して、変更があるたびに処理を呼び出す。

高階関数のサンプルを読むのに手こずった。。。
コールバック関数と合わせて勉強。

ルート定義の順番について(ユーザー登録ページの実装)

ユーザー詳細ページのURLを/users/:userId、ユーザー登録ページのURLを/users/newとしたとき、登録ページのルート定義を先にしてしまうと/users/newでアクセスした際に/users/:userIdのパターンと合致してしまい詳細ページへのルーティングとなってしまう。
そのため、ルート定義の順番には気をつけなければならない。

RouterインスタンスとRouteオブジェクト

名前は似ているが別物。

●$router

$routerはRouterインスタンスを指す。

RouterインスタンスはWebアプリケーション全体に対して一つ存在し、全般的なRouter機能を管理する。

アプリケーション全体としての履歴を管理する、プログラム上でページ遷移を実行する場合などにRouterインスタンスを使用する。

●$route

$routeはRouteオブジェクトを指す。

ページ遷移によってルーティングが発生するごとに生成される。
アクティブなルートの状態を保持している。

詳しくは公式サイトを見る。

ネストしたルーティング

公式サイトを見る。

リダイレクトとエイリアス

●リダイレクト

ルート定義の際に、redirectプロパティを使用する。
* を用いることで、定義している全てのルートにマッチしなかった場合のリダイレクト先を設定できるためNot Foundページを作る際に便利。

routes: [
  ...
  ...
  ...
  { path: "*", redirect: "/notfound" },
]

●エイリアス

複数の名称(URL)で特定のページへ遷移させたい場合に使用する。

routes: [
  ...
  ...
  ...
  //pocketmonstersでもpokemonでもsatoshiでも、ちゃんとページ遷移する
  { path: "/pocketmonsters", component: 表示させたいコンポーネント, alias: ["pokemon", "satoshi"] }
]

履歴の管理

SPAではサーバー側のルーティングを介していないため、クライアント側で履歴操作を管理しなければならない。

●URL Hash
●HTML5 History API

この二つを調べる。


最後投げやりになったけど、使うべき時がきたら ちゃんとここに戻って勉強し直そう

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js: i18nで多言語化

多言語化案件に携わることになり、Nuxt.js + i18nの導入を経験できたので備忘録的に残します。

ドキュメント
https://nuxt-community.github.io/nuxt-i18n/

要件

今回の要件は以下
1. locale: en, ja の2言語
2. url: /en, /ja ( /へのアクセスはcookie制御、デフォルト /en )
3. localeファイルはページごとに管理
4. メッセージはテンプレート方式で引数を与えられるようにする

導入手順

  1. nuxt-i18n のインストール
  2. nuxt-i18n.config.js の作成
  3. 言語ファイルの作成
  4. nuxt.config.js の設定
  5. Template側での呼び出し

nuxt-i18n のインストール

これはドキュメントの通り。

yarn add nuxt-i18n

or

npm i -S nuxt-i18n

nuxt-i18n.config.js の作成

今回は、 ページごとにメッセージファイルを管理したいので、locales[].file ではなく、 vueI18n.message を使用します。

nuxt-i18n.config.js

import messages from './locales/message' <- これは次項で説明

export default {
  strategy: 'prefix',
  locales: [
    { code: 'en', iso: 'en_US' }, <- fileプロパティは使用しない
    { code: 'ja', iso: 'ja_JP' }
  ],
  defaultLocale: 'en',
  vueI18n: {
    fallbackLocale: 'en',
    messages
  },
  rootRedirect: 'en',
  detectBrowserLanguage: {
    useCookie: true,
    cookieKey: 'i18n_locale'
  }
}

言語ファイルの作成

要件#3のページごとにlocaleファイルを分けるために、このようなディレクトリ構造で管理します。
各localeごとにページ単位のjsファイルを用意して、message.jsでそれらをがっちんこしてexport。
それを nuxt-i18n.config.js でimport。

./locales
├── en
│   ├── top.js
│   └── sub.js
├── ja
│   ├── top.js
│   └── sub.js
└── message.js

./locales/ja/top.js

export default {
    top_title: 'TOPページへようこそ',
    top_title_service: 'サービス概要',
    top_label_service: 'サービス {index}: {name}',
    top_service_price: '{0} 円',
    top_service_description: 'こんなサービスです。詳しくは<a herf="{0}">こちら</a>までどうぞ!'
}

./locales/en/top.js

export default {
    top_title: 'Welcome to TOP page',
    top_title_service: 'Services'
    top_label_service: 'Service {index}: {name}',
    top_service_price: 'USD {0}',
    top_service_description: 'This is the service description. For more information, please reach out ot us <a herf="{0}">here</a>.'
}

./locales/message.js

import localeENTop from './en/top'
import localeENSub from './en/sub'

import localeJATop from './ja/top'
import localeJASub from './ja/sub'

export default {
  en: {
    ...localeENTop,
    ...localeENASub
  },
  ja: {
    ...localeJATop,
    ...localeJASub
  }
}

これでページが量産されても管理しやすくなります。

テンプレートフォーマットの利用

各メッセージをテンプレート形式で引数を与えて利用できるようにすることから、pluginを準備します。
arrow functionでは arguments オブジェクトが利用できないので、ES5の function() を利用しています。
これにより、template側で $message($t('top_label_service'), { index: i, title: item.title }) のような利用が可能になります。

./plugins/message.js

/* eslint-disable no-extend-native */
import Vue from 'vue'

Vue.prototype.$message = function(template, replacement) {
  if (typeof replacement !== 'object') {
    replacement = Array.prototype.slice.call(arguments, 1)
  }

  return template.replace(/\{(.+?)\}/g, function(m, c) {
    return replacement[c] != null ? replacement[c] : m
  })
}

nuxt.config.js の設定

作成した i18n.config.js を利用して、nuxt.config.js のi18n設定を行います。

import i18n from './nuxt-i18n.config'

export default {
  ...
  plugins: [
    ...
    '~/plugins/message'
  ],
  modules: [
    ...
    [ 'nuxt-i18n, i18n ]
  ]
}

Template側での呼び出し

こんな感じで利用します。$message() の引数にはオブジェクトの指定も可能。

<template>
  <div>
    <h2>{{ $t('top_title') }}</h2>

    <section>
      <h3>{{ $t('top_title_service') }}</h3>
      <div v-for="(item, i) in services" :key="item.id">
        <h4>{{ $message($t('top_label_service'), { index: i + 1, name: item.name }) }}<h4>
        ...
        <p>{{ $message($t('top_service_price'), item.price.toLocaleString()) }}</p>
        <p v-html="$message($t('top_service_description'), localePath({ path: '/contact' }))" />
      </div>
    </section>
  </div>
</template>
...

Cookie制御とか自前で実装しようとしていたけど、これ使えばバッチリでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js: i18nで多言語化 (localeファイルを分割する)

多言語化案件に携わることになり、Nuxt.js + i18nの導入を経験できたので備忘録的に残します。

ドキュメント
https://nuxt-community.github.io/nuxt-i18n/

要件

今回の要件は以下
1. locale: en, ja の2言語
2. url: /en, /ja ( /へのアクセスはcookie制御、デフォルト /en )
3. localeファイルはpage(component)ごとに管理
4. メッセージはテンプレート方式で引数を与えられるようにする

導入手順

  1. nuxt-i18n のインストール
  2. nuxt-i18n.config.js の作成
  3. 言語ファイルの作成
  4. nuxt.config.js の設定
  5. Template側での呼び出し

nuxt-i18n のインストール

これはドキュメントの通り。

yarn add nuxt-i18n

or

npm i -S nuxt-i18n

nuxt-i18n.config.js の作成

今回は、 ページごとにメッセージファイルを管理したいので、locales[].file ではなく、 vueI18n.message を使用します。

nuxt-i18n.config.js

import messages from './locales/message' <- これは次項で説明

export default {
  strategy: 'prefix',
  locales: [
    { code: 'en', iso: 'en_US' }, <- fileプロパティは使用しない
    { code: 'ja', iso: 'ja_JP' }
  ],
  defaultLocale: 'en',
  vueI18n: {
    fallbackLocale: 'en',
    messages
  },
  rootRedirect: 'en',
  detectBrowserLanguage: {
    useCookie: true,
    cookieKey: 'i18n_locale'
  }
}

言語ファイルの作成

要件#3のページごとにlocaleファイルを分けるために、このようなディレクトリ構造で管理します。
各localeごとにページ単位のjsファイルを用意して、message.jsでそれらをがっちんこしてexport。
それを nuxt-i18n.config.js でimport。

./locales
├── en
│   ├── top.js
│   └── sub.js
├── ja
│   ├── top.js
│   └── sub.js
└── message.js

./locales/ja/top.js

export default {
    top_title: 'TOPページへようこそ',
    top_title_service: 'サービス概要',
    top_label_service: 'サービス {index}: {name}',
    top_service_price: '{0} 円',
    top_service_description: 'こんなサービスです。詳しくは<a herf="{0}">こちら</a>までどうぞ!'
}

./locales/en/top.js

export default {
    top_title: 'Welcome to TOP page',
    top_title_service: 'Services'
    top_label_service: 'Service {index}: {name}',
    top_service_price: 'USD {0}',
    top_service_description: 'This is the service description. For more information, please reach out ot us <a herf="{0}">here</a>.'
}

./locales/message.js

import localeENTop from './en/top'
import localeENSub from './en/sub'

import localeJATop from './ja/top'
import localeJASub from './ja/sub'

export default {
  en: {
    ...localeENTop,
    ...localeENASub
  },
  ja: {
    ...localeJATop,
    ...localeJASub
  }
}

これでページが量産されても管理しやすくなります。

テンプレートフォーマットの利用

各メッセージをテンプレート形式で引数を与えて利用できるようにすることから、pluginを準備します。
arrow functionでは arguments オブジェクトが利用できないので、ES5の function() を利用しています。
これにより、template側で $message($t('top_label_service'), { index: i, title: item.title }) のような利用が可能になります。

./plugins/message.js

/* eslint-disable no-extend-native */
import Vue from 'vue'

Vue.prototype.$message = function(template, replacement) {
  if (typeof replacement !== 'object') {
    replacement = Array.prototype.slice.call(arguments, 1)
  }

  return template.replace(/\{(.+?)\}/g, function(m, c) {
    return replacement[c] != null ? replacement[c] : m
  })
}

nuxt.config.js の設定

作成した i18n.config.js を利用して、nuxt.config.js のi18n設定を行います。

import i18n from './nuxt-i18n.config'

export default {
  ...
  plugins: [
    ...
    '~/plugins/message'
  ],
  modules: [
    ...
    [ 'nuxt-i18n, i18n ]
  ]
}

Template側での呼び出し

こんな感じで利用します。$message() の引数にはオブジェクトの指定も可能。

<template>
  <div>
    <h2>{{ $t('top_title') }}</h2>

    <section>
      <h3>{{ $t('top_title_service') }}</h3>
      <div v-for="(item, i) in services" :key="item.id">
        <h4>{{ $message($t('top_label_service'), { index: i + 1, name: item.name }) }}<h4>
        ...
        <p>{{ $message($t('top_service_price'), item.price.toLocaleString()) }}</p>
        <p v-html="$message($t('top_service_description'), localePath({ path: '/contact' }))" />
      </div>
    </section>
  </div>
</template>
...

Cookie制御とか自前で実装しようとしていたけど、これ使えばバッチリでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueのRouterでURLのパラメータの型をstringから変更する

例えばユーザーデータ更新APIリクエストでURLのIDとか使って引数に渡す時に,

this.$route.params.userId

↑みたいに取得するとstringでしか受け取れず
毎回number型に変更するのが冗長な気がしてたので
router側で変更する方法ないかなぁって調べたけど
なかなか出てこなかったけどやっと見つけたのでメモ

修正前

修正前のrouterでの設定が↓だとして,

{
  path: 'userEdit/:userId',
  name: 'userEdit',
  component: () => import('./views/UserEdit.vue'),
},

修正後

propsという項目でパラメータの型を変更することができる
routerから先にparamsを取り出して,キャストしてpropsにセットするというやり方

{
  path: 'companyList/:companyId',
  name: 'companyEdit',
  component: () => import('./views/CompanyEdit.vue'),
  props: (route) => ({userId: Number(route.params.userId)}),
},

コンポーネントでの受け取り方

後はコンポーネントでPropで受け取るだけ
自分の環境だと↓で受け取れる

@Prop()
private userId!: number;

意外にもクソ簡単だった。。。。。

おわり

参考資料

VueRouter基本ルーティング記法まとめ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

備忘録集[Vue.js+Laravel+docker]

一つの記事に書く程ではないけれども、
ハマった所を今後同じ所でハマった同志のため、
また物忘れが最近酷いので再度自分がハマらないため
自分が躓いた所を備忘録集として残します。
基本的だけれども見落としやすい所が中心です。

備忘録なので書きなぐりの文章が多いです。
また間違い等があるかもしれないので、
都度修正していきます。

Javascript関連:VueJS,typescript,axios等

1.axiosのdeleteの方法

postとは異なる方法で設定する必要がある。

axios.delete('url',
             params: { foo: 'bar' }
             ).then(
             //以下省略
             );

2.[Vue.js]async,awaitでreturnの結果としてaxiosでgetしたデータを取得する。

下の例はupdateDate関数でaxiosを使っている。
このupdateDate関数は外部から呼び出され、this.Dataの値を更新している。
しかしthisを使用しているので、thisに依存している。
依存を解消するなら、axiosの結果をupdateDataの外で受け取るべき。
しかしaxiosはPromise型なので、thenの内部でreturnを書いても値を返さない。

改善前
methods:{
    updateData(url) {
      //データ取得
      axios
        .get(
          url
        )
        .then(response => {
          this.Data=response;
          //下のように書いてもresponseを返さない
          //return respose;
        })
        .catch(error => {
          console.log(error);
        })
    },
}

そこでasync,awaitを利用し以下のように書き直す。
then以降まで書かないでもaxios.get(url)までで,
結果を返してくれる。

参考:https://github.com/axios/axios

例1.thisをupdateDataの外部に出すことができた.
  mounted: async function() {
    let url = "どこかのurl";
    try {
      this.Data = await this.updateTable(url);
    } catch (err) {
      console.log(err);
    }
  },
  methods: {
    async updateData(url) {
      //axiosでデータ取得.get以降にthenを書かない。
      return axios.get(url);
    }
例2
    async function updateData2(url) {
        try {
          const response = await axios.get(url);
          return response;
        } catch (error) {
          console.error(error);
          return error;
        }
      }

注意:axiosをVue.jsのmounted内部で使っている。
Axiosは例外処理も含めて多用する可能性が高いので、
慣れてきたらaxios等の非同期処理を担うクラスを作成したほうがコードが散乱しないので良い。

3.tr等に普通にtransition-groupしてもアニメーションはつかない

参考:https://jp.vuejs.org/v2/guide/components.html#%E5%8B%95%E7%9A%84%E3%81%AA%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88

htmlの仕様によりliやtr等のタグはtableタグ直下等の特殊な条件でしか処理されない。
なので、tableとtrの間に普通にtransition-groupしてもtr以下が表示されなくなる。

失敗例
<tbody name="table-row">
<transition-group>
<tr v-for="data in showData" class="table-row-item" :key="data.id">  
<!--ここは表示されない.transition-groupをdiv等にしても同じ挙動になる.-->
</tr>
</transition-group>
</tbody>

対策の一例は、tbodyにis="transition-group"をつけること。

tbodyにisをつける回避策
<tbody name="table-row" is="transition-group">
<tr v-for="data in showData.data.data" class="table-row-item" :key="data.title">  

他の対策も複数あるので、必要な時は参考urlを参照。

4.アップロードされた画像ファイルのプレビュー表示方法 + URL.createObjectURLの仕様

画像の出力
    <div class="col-sm-1">
      <img class="icon-image border border-dark" :src="iconimg" />
    </div>

ファイルのアップロードを入力するhtmlタグは省略。
ファイルが入力された後、下のselectedFile関数を通して画像を格納する。

    selectedFile(event) {
      //必要に応じてkeyは変更
      this.iconfile = event.target.files[0];
      //プレビュー用のイメージ格納
      if (this.iconfile.type.startsWith("image/")) {
      //ObjectURLを生成
      this.iconimg = window.URL.createObjectURL(this.iconfile);
      }
    },

以下はjavascriptのcreateObjectURLの仕様の話。

createObjectURL(object)は、
与えられたobjectに対してアクセス可能となるURLを生成している。
このURLはクライアントのブラウザのメモリ上に保存されているblob(いわゆる生のデータ)を参照する。
アップロードされたファイルはクライアント上ではFileオブジェクトで保存されているが、
File自体がblobを継承しているため、createObjectURLでのURL生成が可能となる。
なので、createObjectURL関数の引数となるobjectはfile,blob,MediaSourceのどれかでなくてはならない。
生成されたブラウザを閉じるまでURLは有効。
ただ一度ブラウザに読み込まれればURLは必要ないので、可能ならば削除したほうが良い。
window.URL.revokeObjectURL(file);
でURLの削除が可能。

参考:
https://developer.mozilla.org/ja/docs/Web/API/File
https://qiita.com/azu369yu/items/8998e1e1536a5acfb7b3
https://qiita.com/iLLviA/items/c24f385ca3334c05a682

5.[typescript+Vue.js]子コンポーネントにて、propsを定義する際にはundefinedに注意

参考:https://qiita.com/kyokoshimizu/items/ee6c6e6b905b8aa101fa
参考https://tech-up.hatenablog.com/entry/2019/03/15/152258

前提:vue-property-decoratorをimportしている。

@Prop() public name:string;

のようにVue.jsのpropsをtypescriptで書くと

Property '----' has no initializer and is not definitely assigned in the constructor.

というエラーが出て怒られ、コンパイルができないケースがある。
しかし

@Prop() public name:string = "";

と初期化したらしたで、コンパイルも稼働はするが、コンソールには

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "propsValue"

とエラーが出てしまう。
この場合、前者はtypescriptのエラーで後者がVue.jsのエラーになる。

原因
typescriptの設定でstrictPropertyInitializationという設定がオンになっているためである。
strictPropertyInitializationは、「undefinedの許容されてない型を初期化させる」という設定になる。
要するに変数に初期値設定しろって設定。
最初に定義したPropsはstring型であるが、何も定義されていないのでundefinedが代入される。
string型にはundefinedが許容されていないため、strictPropertyInitializationによりエラーが発生した。
2回目のPropsのように初期化すればstrictPropertyInitializationによるエラーは解決しコンパイルは可能になるが、
これは子コンポーネントでpropsを書き換えているので、今後はVue.jsがエラーを発生させた。

対策
四苦八苦した過程が残るけど自分のお勧めは4番

1.tsconfig.jsonに以下を記述しstrictPropertyInitializationの設定をオフにする

strictPropertyInitialization:false

但し全体に影響する。

2.any型やstringならばUnion型に変更しundefinedで対応できるようにする。

    @Prop() public name:string|undefined;

ただし、number型の場合はundefinedまたはnullとunion型にできないので、
この解決はできない。

    @Prop() public name:any;

anyはどの型でも機能する。但しanyになるので型の意味があまりなくなる。
stringやnumberのような複雑じゃない型の時までanyにするのは悲しい。

3.そもそも@component以下に書く

@Component(
    {
        props:{
            placeholder:String,
        }
    }
)

但しexport以下で呼び出せなくなる。

4.型宣言の際に!をつける、または?をつける
参考:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#strict-class-initialization
参考:https://dev.classmethod.jp/server-side/typescript-assertions/

    @Prop() public name!:any;

!はコンパイラにこの型はnon-nullですよと伝える意味がある。
初期化されてないが実際には外部で変数が代入されている(丁度今回みたいな)場合に使える。
ちなみに @Prop() の中身にはoptionが設定できる。

ソースより
export interface PropOptions<T=any> {
  type?: PropType<T>;
  required?: boolean;
  default?: T | null | undefined | (() => T | null | undefined);
  validator?(value: T): boolean;
}

defaultとか設定したいときは次のような感じ

@Prop({default:"text"}) public type!:string;

ちなみに上のソースコードでも出現しているが、!の代わりに?をつけても解決する。
?は代入されてなくても構わない、という意味になる。

6.[Vue.js]vue-routerのto属性に対してparamを設定しpropを渡したい時、pathではなくnameでurlを呼び出さないといけない

<router-link
:to="{name:'hoge',params:{huga: 'hage'}}"
>

でないといけない。router.jsの方でnameで設定しないとparamsが有効にならない。
pathを設定するとurlに直接paramsを組み込まなければならない。

Laravel

1.モデルでSELECTする時にidを取得しないと、そのインスタンスにsaveもupdateもできない

大ハマり
参考:https://qiita.com/moimoinon/items/5adb4b179e3c25d189a2

悪い例
$data = Article::select('title','updated_at')->with('hashtags:name')->get();

例のようにモデルのidを取得していないインスタンスにsaveメソッドを使用して更新しようとした場合、
saveの結果としてtrueが返されてもデータベース上では更新されない。
この後の例等、予期せぬ挙動を生むことが多いので、selectする時は基本的にidは取得したほうが良い。

2.EagerLoadを使う場合、SELECTでidを取らないとwithで参照してくれない

参考:https://mseeeen.msen.jp/laravel-5-5-get-specified-column-with-with/

relationのwithも、idを軸にリレーション先のデータを取得している。
よって、もしSELECT元でidが選択されていない場合、withの参照先のデータも取得されない。

良い例
//もしidのカラムを取得していない場合はhashtagsが空になってしまう
$data = Article::select('title','updated_at','id')->with('hashtags:name')->get();

3.belongsToの場合のモデル名は単数。データ取得の際も関数でなくプロパティを通して取得。

HasManyと同じつもりで複数形にすると、参照先モデルの取得が正しくされない。

悪い例
    public function admins(){
        return $this->belongsTo(Admin::class);
    }

belongsToの参照先は1対多の1側なので、単数にしなければならない。

良い例
    public function admin(){
        return $this->belongsTo(Admin::class);
    }

リレーション先のデータを取得する際も単数形で指示する。
プロパティでアクセスできるのが大きな違い。

    //adminに対して()をつけずにプロパティでアクセスできる。
    public function scopeGetAdminName($query,$id){
        return $query->find($id)->admin->name;
    }

    //EagerLoad,withのadminが単数形になるのがhasManyの時との違い
    public function scopeWithAdmin($query){
        return $query->with('admin:id,name');
    }

その他

1,[docker] php:7.4.2-apacheのdockerイメージでドキュメントルートを変更する方法

/etc/apache2/sites-available/000-default.confのドキュメントルートを変更

000-default.conf
<VirtualHost *:80>
    # The ServerName directive sets the request scheme, hostname and port that
    # the server uses to identify itself. This is used when creating
    # redirection URLs. In the context of virtual hosts, the ServerName
    # specifies what hostname must appear in the request's Host: header to
    # match this virtual host. For the default virtual host (this file) this
    # value is not decisive as it is used as a last resort host regardless.
    # However, you must set it for any further virtual host explicitly.
    #ServerName www.example.com

    ServerAdmin webmaster@localhost
        #ここを変更
    DocumentRoot /var/www/html/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでブラーエフェクト付きの画像遅延ローディング

概要

  1. lqip-loaderを使って低解像度版の画像を生成する
  2. vue-lazyloadで、1で生成した低解像度の画像をローディング時の画像として用いる
  3. 遷移時のアニメーションはcssのtranstionを使う

やりたいこと

example.gif

こんな感じで画像を遅延ローディングするときのプレースホルダーにブラーがかかった画像を使いたい!

方法

lqip-loaderを導入する

lqip-loaderは低解像度版の画像を出力するためのwebpackのローダーです。まずはこのツールを導入します。

npm install --save-dev lqip-loader

vue-cliを使ってプロジェクトを作成した場合、vue.config.jsに次のように記述します。

vue.config.js
module.exports = {
  chainWebpack: config => {
    const imagesRule = config.module.rule('images');

    imagesRule.uses.clear();

    imagesRule
      .use('lqip-loader')
      .options({
        path: '/media',
        base64: true,
        palette: false
      })
      .loader('lqip-loader');
  }
};

webpackをそのまま使っている場合は、公式のREADMEを参考に設定してください。

Vue-Lazyloadを導入する

Vue-Lazyloadはリソースの遅延ロードを行うためのVue.js用モジュールです。

公式のREADMEの通りに導入しましょう。

画像を利用する

例として、画像を遅延ロードするコンポーネントLazyImageを実装します。

LazyImage.vue
<template>
    <div class="lazy-image" v-lazy-container="{ selector: 'img' }">
        <img :data-src="img.src"
             :data-loading="img.preSrc"
        >
    </div>
</template>

<script>
  export default {
    name: 'LazyImage',
    props: {
      img: {
        type: Object,
        required: true,
      },
    },
  }
</script>

3,4行目に書いたように、lqip-loaderが出力した低解像度の画像はpreSrcプロパティ1、本来の画像のパスはsrcプロパティで取得できます。

このコンポーネントにimgを渡すときは、次のように記述します。

App.vue
<lazy-image :img="require('./img/example.png')" />

スムーズに遷移する

遷移のアニメーションにはcssが使えます。

cssのfilterでblurをかけ、画像のロードが終了したときにfilterを外せば良いです。あとは好きにtransitionをかけましょう。

lazy-img > img {
  filter: blur(25px);
  transition: all .2s;

  &[lazy=loaded] {
    filter: none
  }
}

参考


  1. base64エンコードされた画像そのものが入っています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む