「Vue.jsで簡単なメモアプリを実装する」の備忘録

# 概要

この記事は、以前に投稿した「[Vue.jsで簡単なメモアプリを実装する] (https://qiita.com/rubytomato@github/items/294ca52abc2be4ae6131)」で、実際にメモアプリを実装して得られたちょっとしたノウハウや躓いたポイントの備忘録です。

**環境**

* 前記事と同じ

# 算出プロパティ(Computed Properties)の使い方を間違えていた

算出プロパティは名前の通りプロパティとして扱いますが、実装が関数なので当初メソッドのように使おうとしてエラーになりました。
また、methodsに定義する関数は引数を取ることができますが、computedに定義する関数は引数を取ることができません。

```vue
<script>
export default {
  //...省略...
  data () {
    return {
      test: 'TEST'
    }
  },
  methods: {
    method1 () {
      return `method1: ${this.test}`
    }
  },
  computed: {
    method2 () {
      return `method2: ${this.test}`
    }
  }
}
</script>
```

```html
<div class="row">
  <div class="col">
    <p>{{ method1() }}</p>
    <!-- <p>{{ method2() }}</p> この呼び出しはエラー -->
    <p>{{ method2 }}</p>        <!-- この呼び出しはOK -->
  </div>
</div>
```

上記の算出プロパティの記述は省略したgetメソッド(getter関数)の記述の仕方で、以下のようにgetメソッドとsetメソッド(setter関数)に分けて記述できます。

```vue
computed: {
  method2: {
    get () {
      return `method2: ${this.test}`
    },
    set (val) {
      this.test = val
    }
  }
}
```

setter関数を呼び出すには以下のようにプロパティに代入するようにします。

```html
<div class="row">
  <div class="col">
    <p>{{ method1() }}</p>
    <!-- <p>{{ method2('テスト') }}</p> この呼び出しはエラー -->
    <p>{{ method2='テスト' }}</p>        <!-- setter関数を呼び出し -->
  </div>
</div>
```


# ブラウザバックしたときに前の画面位置に戻らない

vue-routerにはブラウザバックしたときに、スクロールの位置を前画面の元の位置へ戻す機能があります。
しかし実装して動作確認したところブラウザバックすると画面トップにしか戻らないという現象が起きました。

ルーターの実装は以下の通りです。

```js:router/index.js
const router = new Router({
  routes: [
    //...省略...
  ],
  scrollBehavior: (to, from, savedPosition) => {
    return savedPosition || { x: 0, y: 0 }
  }
})
```

調べた結果、画面遷移にアニメーションを使用している場合、アニメーションが終わるまで待機しないといけないようです。
（実際に検証してはいないので正確ではありません。）

変更前は以下のようにrouter-viewコンポーネントをtransitionで囲み、画面遷移時にフェードするアニメーションを適用していました。
transitionをコメントアウトしアニメーションを無効化したところ、ブラウザバックで前の画面位置に戻るようになりました。

```html
<transition name="fade" mode="out-in">
  <router-view>

    <!-- 省略 -->

  </router-view>
</transition>
```

# アドレスバーで直接動的セグメントを書き換えたときに期待するデータが取得できない

以下のような動的セグメント(`:id`のこと)を含むpathの定義で、ブラウザのアドレスバーから直接動的セグメントを書き換えて表示するデータを変えようとしたときに、期待するデータが取得できないという現象がありました。

例）メモID1のデータを表示後、idを書き換えてメモID2のデータを表示しようとするとデータが見つからないというエラーが起きる。

```text
http://localhost:8080/memo/1
  ↓
http://localhost:8080/memo/2
```

ルーターの実装

```js:router/index.js
{
  path: '/memo/:id',
  name: 'MemoDetails',
  component: MemoDetails,
  props: true,
  meta: {
    title: 'details of memo'
  }
}
```

メモ詳細を表示するコンポーネントの実装

```vue:MemoDetails.vue
<script>
export default {
  name: 'MemoDetails',
  data () {
    return {
      targetId: this.id
    }
  },
  // routeの動的セグメント
  props: ['id'],
  // pathの:idを直接書き換えたときの対応
  beforeRouteUpdate (to, from, next) {
    // ここで参照するidは文字列型
    this.targetId = to.params.id
    next()
  },

  //...省略...
}
</script>
```

原因は、恥ずかしい話`to.params.id`が文字列型だったというオチです。
通常の遷移時にはpropsプロパティのidに動的セグメントの:idがバインドされますが、動的セグメントを直接書き換えた場合はこのidプロパティが変更されないため、beforeRouteUpdateに実装したコールバック関数の中で変更されたidを参照するように実装していました。

# フィルターではthisは参照できない

当初、日付のフォーマットを下記のようにフィルターで実装しようとしていましたが、動作確認すると期待する結果にならず調べたところフィルター内ではthisは参照できないことがわかりました。

```vue
filters: {
  formatedDate (value) {
    if (!value) {
      return ''
    }
    return this.$moment(value).format('YYYY/MM/DD a hh:mm:ss')
  }
}
```

```html
<small>Release Date. {{ memo.releasedAt | formatedDate }}</small>
```

修正後はフィルターではなく算出プロパティとして実装しました。リファレンスにもきちんと書かれていたのでリファレンスをよく読んでおけばよかったのですが、汎用性を考えるとフィルターとして実装するのが適当かとおもいそのまま実装していました。

```vue
computed: {
  formatedReleasedAt () {
    if (!this.memo.releasedAt) {
      return ''
    }
    return this.$moment(this.memo.releasedAt).format('YYYY/MM/DD')
   }
}
```

```html
<small>Release Date. {{ formatedReleasedAt }}</small>
```

# コンポーネントのpropsで勘違いした点について

わざわざ書き残すほどの内容ではなかったのですがせっかくの備忘録なので残しておきます。

htmlは要素(element)と属性(attribute)を下記のように記述します。

```text
<要素 属性1="属性値1" 属性2="属性値2">テキスト</要素>
```

Vue.jsのコンポーネントはhtmlの記述の仕方に似ていますが、違うのは定義するものが属性(attribute)ではなくプロパティ(property)であり、コンポーネントへデータを渡すときに使用するという点です。

```text
<コンポーネント プロパティ1="プロパティ値1" プロパティ2="プロパティ値2" />
```

プロパティ値を受け取るには、コンポーネントのprops（というカスタム属性）にプロパティ名を定義します。

```vue:ChildComponent.vue
<script>
export default {
  name: 'ChildComponent',
  props: [
    'プロパティ1',
    'プロパティ2'
  ],

  //...省略...
}
</script>
```

ここで勘違いしていた点ですが、コンポーネントもhtmlと同じように属性(attribute)を定義しているのだと思い、コンポーネントのpropsのプロパティと関係しているとすぐには理解できませんでした。

```text:勘違い
<コンポーネント 属性1="属性値1" 属性2="属性値2" />
```

つまるところ、コンポーネントに定義するプロパティ(property)は、コンポーネントのprops(properties?)で定義したプロパティで受け取ることができるというように理解しました。

ちなみに、Reactでは"props"は"properties"のことだと説明されています。

> [Components and Props] (https://reactjs.org/docs/components-and-props.html)
> 
> This function is a valid React component because it accepts a single “props” (which stands for properties) object argument with data and returns a React element. 

propsで定義したプロパティはdataオブジェクトと同じようにテンプレート内で利用できます。

```html
<h1>{{ プロパティ1 }}</h1>
<h2>{{ プロパティ2 }}</h2>
```

親コンポーネントが監視するdataオブジェクトを渡す場合はv-bindディレクティブを使用します。

```vue
<script>
export default {
  name: 'ParentComponent',
  data () {
    return {
      title: 'blog title'
    }
  },

  //...省略...
}
</script>
```

単なる文字列を渡しているようでここも勘違いしましたが、渡しているのはプロパティ名です。逆に文字列リテラルを渡すときにv-bindを使うとそのようなプロパティが存在しなければエラーになります。

```html
<child-component v-bind:title="title" />
```

次のように記述するとtitleというpropsへ"title"という文字列を渡します。

```html
<child-component title="title" />
```

propsのプロパティへ値を渡すもう1つの方法は、routeの定義でpropsを有効にしてrouter-linkコンポーネントのparamsに指定する方法です。

```js
const router = new Router({
  routes: [
    {
      path: '/child/:id',
      name: 'ChildComponent',
      component: ChildComponent,
      props: true                    // ← ここをtrue
  ]
})
```

親コンポーネント

```vue
<template>

  <!-- propsがtrueの場合、paramsに指定した値はpropsプロパティにバインドされる -->
  <router-link
    v-bind:to="{name: 'ChildComponent', params:{ id: child.id, name: child.name }}">
    {{ child.name }}
  </router-link>

</template>
```

子コンポーネント

```vue
<template>
  <div class="child">
    {{ id }}, {{ name }}
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  props: [
    'id',
    'name'
  ],

  // ...省略...

}
</script>
```

* この方法には注意点があり、ブラウザの戻る、進むボタンで画面遷移するとpropsのプロパティは更新されません。

## Vue.jsでのpropsの説明

> [プロパティを使用した子コンポーネントへのデータの受け渡し] (https://jp.vuejs.org/v2/guide/components.html#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E5%AD%90%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%B8%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%8F%97%E3%81%91%E6%B8%A1%E3%81%97)
> 
> プロパティはコンポーネントに登録できるカスタム属性です。値がプロパティ属性に渡されると、そのコンポーネントインスタンスのプロパティになります。


# まとめ

まとめというか反省になりますが、リファレンスはよく読んでおいた方がよいと思いました。
いまのところ4点だけしか書いていませんが、他にも躓いていた点があったので時間があれば追記しようと思います。

* 5点目追記しました。