Vueでその場編集をコンポーネント化


[[Vue.js]クリックすると編集可能に切り替わるテキスト](https://qiita.com/huigo/items/5a1eb5ade1bce13c098e)を参考にしつつNuxt.jsでその場編集を実装していたのですが、

- ディレクティブ外出しにしたい
- その場編集はcomponent化したい
- さらにその場編集をいくつか持ってるformのcomponent作りたい

等々あってところどころで詰まったのでメモ

### 環境
Nuxt.js

### ディレクティブの外出し
#### plugins以下にjsファイル作成
下記ファイルをplugins以下に作成

```plugins/autoFocusDirective.js
import Vue from 'vue'

Vue.directive('auto-focus', {
  bind: function(el) {
    var self = el
    Vue.nextTick(function() {
      self.focus()
    })
  }
})
```

#### nuxt.config.jsに記述
作成したカスタムディレクトリをnuxt.config.jsのpluginsに追記

```nuxt.config.js
...
  plugins: [
    '~/plugins/autoFocusDirective'
  ],
...
```

これで使えるようになります！

```input.vue
<template>
  <input
    v-model="newValue"
    v-auto-focus
    type="text"
  /> 
<template/>
```

### その場編集
#### 最初の実装

フォームに最初に入力しておく初期値は親コンポーネントからpropsで渡されるものとします。
クリック→編集→フォーカスアウトで親に編集後の値を返すという仕様にしたい。
propsで渡った値はimmutableとし直接v-modelでバインディングするのはご法度なので、computedで置き換えの値のgetter, setterを定義します。
ということで最初に実装したのが↓

```EditInPlaceTextForm.vue

<template>
  <section>
    <div v-if="!editable" @click="editable = true" v-text="newValue"></div>
    <input
      v-if="editable"
      v-model="newValue"
      v-auto-focus
      type="text"
      @blur="onFocusOut"
    />
  </section>
</template>

<script>
export default {
  name: 'EditInPlaceTextForm',
  props: {
    value: {
      type: String,
      required: true
    }
  },
  data: function() {
    return {
      editable: false,
      newValue: ''
    }
  },
  computed: {
    // フォーカスアウトした際に初期値に戻ってしまう
    newValue: {
      get() {
        return this.value
      },
      set(val) {
        console.log(val)
      }
    }
  },
  methods: {
    onFocusOut() {
      // 値に変更がなくnewValueが空なら渡された値をそのまま入れる
      this.editable = false
      this.submit()
    },
    submit() {
      this.$emit('update', this.newValue)
    }
  }
}
</script> 
```

#### フォーカスアウトで初期値に戻ってしまう
上手く動きそうですが、フォーカス→適当に編集→フォーカスアウトすると表示された値が編集後のものではなく初期値に戻ってしまう。
getterでpropsの初期値を返しているのでそりゃそうですね。
~~どうしよう〜〜〜と悩んだ結果、propsを初期値、newValueを最終値として、modelにバインディングする動的な中間値を挟むことにしました。~~
~~多分すごくとっても大変にスマートじゃない。~~
【10/20追記】
これ単にdataにv-model用の変数を定義して初期値にpropsの値渡してやればいいんじゃないかとようやく思い当たりおれは　おれは

```javascript

  data() {
    return {
     newValue: this.value
    }
  },

```
ちょっと↓の記事書き直せてないんですがこの定義を入れれば意図通りのことができるのではないかと思っています
[プロパティ](https://jp.vuejs.org/v2/guide/components-props.html)

#### 解決TRY
divとinputには中間値`tmpValue`をバインディングし、`tmpValue`のgetterでは変更がない初期値の場合のみpropsの初期値を返し、setterで`newValue`に値を入れ直します。
おめーまじかって感じですね。私もそう思います。すごい回りくどい。
他にいい方法ないのかなあと思ったのですがVueにまだ疎くてどうにも思いつきませんでした。
ともあれこれで編集後にフォーカスアウトしても表示されている値が初期値に戻ることはなくなりました。

``` EditInPlaceTextForm.vue

<template>
  <section>
    <div v-if="!editable" @click="editable = true" v-text="tmpValue"></div>
    <input
      v-if="editable"
      v-model="tmpValue"
      v-auto-focus
      type="text"
      @blur="onFocusOut"
    />
  </section>
</template>

<style scoped></style>

<script> 
export default {
  name: 'EditInPlaceTextForm',
  props: {
    value: {
      type: String,
      required: true
    }
  },
  data: function() {
    return {
      editable: false,
      newValue: ''
    }
  },
  computed: {
    // newValueをcomputedで定義してv-modelでバインディングし、
    // get() { return this.value } で定義すると
    // フォーカスアウトした際に初期値に戻ってしまうので中間値を挟む
    tmpValue: {
      get() {
        if (!this.newValue) {
          return this.value
        }
        return this.newValue
      },
      set(val) {
        this.newValue = val
      }
    }
  },
  methods: {
    onFocusOut() {
      // 値に変更がなくnewValueが空なら渡された値をそのまま入れる
      if (!this.newValue) {
        this.newValue = this.value
      }
      this.editable = false
      this.submit()
    },
    submit() {
      console.log('submit')
      console.log(this.newValue)
      this.$emit('update', this.newValue)
    }
  }
}
</script>
```

### 全体
formですが今回は関係ないのでsumbit処理とかは割愛で

親(page vue)

```edit.vue
<template>
  <section>
    <edit-form :value="val"></edit-form>
  </section>
</template>

<style scoped></style>

<script>
import EditForm from '~/components/EditForm'
export default {
  components: {
    'edit-form': EditForm
  },
  data: function() {
    return {
      val: 'hoge',
    }
  }
}
</script>
```

子:その場編集コンポーネントを呼び出すフォームコンポーネント(form component)

```EditForm.vue
<template>
  <section>
    <form>
      <edit-in-place-text-form
        :value="value"
        @update="updateFormValue"
      ></edit-in-place-text-form>
  </form>
  </section>
</template>

<style scoped></style>

<script>
import EditInPlaceTextForm from '~/components/EditInPlaceTextForm'
export default {
  name: 'EditForm',
  components: {
    'edit-in-place-text-form': EditInPlaceTextForm
  },
  props: {
    value: {
      type: String,
      required: true
    }
  },
  methods: {
    updateFormValue(newValue) {
      console.log(newValue)
    }
  }
}
</script>
```

孫:その場編集のコンポーネント(input component)

``` EditInPlaceTextForm.vue
<template>
  <section>
    <div v-if="!editable" @click="editable = true" v-text="tmpValue"></div>
    <input
      v-if="editable"
      v-model="tmpValue"
      v-auto-focus
      type="text"
      @blur="onFocusOut"
    />
  </section>
</template>

<style scoped></style>

<script> 
export default {
  name: 'EditInPlaceTextForm',
  props: {
    value: {
      type: String,
      required: true
    }
  },
  data: function() {
    return {
      editable: false,
      newValue: ''
    }
  },
  computed: {
    // newValueをcomputedで定義してv-modelでバインディングし、
    // get() { return this.value } で定義すると
    // フォーカスアウトした際に初期値に戻ってしまうので中間値を挟む
    tmpValue: {
      get() {
        if (!this.newValue) {
          return this.value
        }
        return this.newValue
      },
      set(val) {
        this.newValue = val
      }
    }
  },
  methods: {
    onFocusOut() {
      // 値に変更がなくnewValueが空なら渡された値をそのまま入れる
      if (!this.newValue) {
        this.newValue = this.value
      }
      this.editable = false
      this.submit()
    },
    submit() {
      console.log('submit')
      console.log(this.newValue)
      this.$emit('update', this.newValue)
    }
  }
}
</script>
```

### thanks
- https://imatomix.com/imatomix/notes/1516891080
- https://qiita.com/huigo/items/5a1eb5ade1bce13c098e
- https://qiita.com/clomie/items/7a69ee850a304595142e