Vue.jsを使用してcontenteditableの値をバインドする (改良)

## はじめに

この記事を全面的に参照していきます。
https://qiita.com/zaru/items/baacf5eb490094aaa150

任意の要素に```contenteditable```をつけると入力フィールド化出来ます。
当たり前と言えば当たり前ですが、```contenteditable```要素に入力した際、改行するとその位置で改行され、EdgeやChromeでは**要素の高さが自動的に伸びていきます。**
従来の```input```や```textarea```だと、入力内容によって高さを自動的に変えるのには少し工夫がいるのですが、この方法なら属性一発で解決できます。(EdgeとChrome以外では試していませんが…)

ただ、```Vue```を用いて```contenteditable```の要素に値をバインドして、後からバインドする要素を変更するような実装に少し手間取ったので、記録として残しておきます。

## TL;DR;

最終的にこんなコードのコンポーネントになりました。

```vue
<template lang="pug">

    .text-input(
        :contenteditable = "true"
        v-text = "text"
        @input = "update"
        @focus = "focus"
        @blur = "blur"
    )

</template>
<script lang="ts">
import { Component, Vue, Prop, Emit, Watch } from 'vue-property-decorator';

@Component
export default class TextInput extends Vue {

    @Prop()
    public value!: string;

    private text: string = '';
    private focusIn: boolean = false;

    @Emit()
    private input(str: string): void {/* */}

    @Watch('value')
    private valueChanged(n: string): void {
        // value が変更され次第 text へコピー
        // ただし入力中は無視
        if (this.focusIn) return;
        this.text = this.value;
    }

    private update(e: Event): void {
        const target: HTMLElement = e.target as HTMLElement;
        this.input(target.innerText);
    }

    private focus(): void {
        this.focusIn = true;
    }

    private blur(): void {
        this.focusIn = false;
    }

}
</script>
```

## 何が問題だったか

参考記事の内容だと、```mounted```時に```content```を```innerContent```に値をコピーしているので、```mounted```後に```content```が変更された時にその値が反映されません。
かといって、```content```を```value```に、```sync```を```input```に変更して```v-model```を使えるようにすると、参考記事冒頭にあるようにキャレットが文字列先頭に飛んでいく現象が起こります。

最初は、以下のように```value```の値を```watch```して、変更次第コピーしていました。

```typescript
    @Watch('value')
    private valueChanged(n: string): void {
        // value が変更され次第 text へコピー
        this.text = this.value;
    }
```

これだと、結局常に1つの値(```value```)を見ているだけになってしまい、入力の度にレンダリングが走ってキャレットが先頭に戻ります。


## 解決策

問題は入力中に入力内容が変更される事なので、```focus```と```blur```を使って、入力エリアがフォーカスされている時は値を変更しないようにしました。

```typescript
    @Watch('value')
    private valueChanged(n: string): void {
        // value が変更され次第 text へコピー
        // ただし入力中は無視
        if (this.focusIn) return;
        this.text = this.value;
    }
```

## おわりに

色々なケースでテストしたわけではないので、この方法ではうまくいかないケースがあるかもしれませんが、とりあえず一つの解決策として参考になれば幸いです。


## おまけ(改行を復元する)

入力中に改行するとバインドされた値にも```\n```が入ります。
その値をデータベース等に保存して、その後読み込んで改めてバインドすると、改行が無視されます。

どうやら```contenteditable```に入力した値は随時```innerHTML```に置き換わっているようで、改行はその中で実現されているようです。

```html
<!-- 「sample<リターン>text」と入力すると… -->
<div contenteditable="true">
    sample
    <div>text</div>
</div>
<!-- …となる -->
```

という事で、改行も復元する場合のコードがこちら。もはやデータのバインドはしておらず、```innerHTML```を直で書き換えています。

```vue
<template lang="pug">

    .text-input(
        ref = "input"
        :contenteditable = "true"
        @input = "update"
        @focus = "focus"
        @blur = "blur"
    )

</template>
<script lang="ts">
import { Component, Vue, Prop, Emit, Watch } from 'vue-property-decorator';

@Component
export default class TextInput extends Vue {

    @Prop()
    public value!: string;

    private focusIn: boolean = false;

    @Emit()
    private input(str: string): void {/* */}

    @Watch('value')
    private valueChanged(n: string): void {
        if (!this.focusIn) {
            const i: HTMLElement = this.$refs.input as HTMLElement;
            const elem: string[] = this.value.split('\n');
            let t: string = '';
            for (const e of elem) {
                t += '<div>' + e + '</div>';
            }
            i.innerHTML = t;
        }
    }

    private update(e: Event): void {
        const target: HTMLElement = e.target as HTMLElement;
        this.input(target.innerText);
    }

    private focus(): void {
        this.focusIn = true;
    }

    private blur(): void {
        this.focusIn = false;
    }

}
</script>
```