Vue.jsでファイルアップロードの実装--ついでにプレビュー機能も実装した話--


![画像選択画面録画.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119773/ca38abaf-b4b4-d3fa-5dd2-ab8faeb0ac9e.gif)


## ファイルアップロードのコンポーネントを作成
スタートから悩み悩みやってみた結果、一応目標物はできたのでアウトプット。


## ☆Special thanks☆

・<a href="http://proengineer.internous.co.jp/content/columnfeature/7605">input type="file"のボタンデザインを変更する方法</a>
・<a href="http://d.hatena.ne.jp/m_ogawa/20100322/1269263282">javascriptで文字列をバイト(bytes)単位で省略する方法</a>


### 初手


```index.vue
<template>
  <div>
    <label>
      <input type="file" placeholder="何でやねん" />
    </label>
  </div>
</template>
```

これ実行してみると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119773/af00d43d-6950-d977-cdf7-ba63b1e53fa8.png)

## デフォルトのプレイスフォルダーを変更したい
`ファイルを選択`という箇所。
何でやねん
が表示されてないようだ。
これは自由の変えられないのかな？と悩んでいたら。書く場所を間違っていました。

placeholderは`input`へ書くものじゃないらしい。
デフォルトのスタイルを変更するには、

```index.vue
<label>画像を選択
  <input />
</label>
```

このように`label`タグへ追加することで表示されるようになる。
（クリックするとファイルを選択と同様に動くからこれで問題ない）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119773/5dd691a1-2c6a-75ea-9414-a1609d7061c4.png)

あれ、`ファイルを選択する`が消えてないけど？
と思って調べていたら、プレイスホルダーを変更するというよりは、
元々のデザインをなかったことにして新たに作成するというイメージらしい。

`ファイル選択`選択されていません
という不要な部分を削除するには、スタイルを当てるしかない。


### display: none;でスタイルを削除し、labelに対して新たにスタイルを当てる

```index.vue
<template>
  <div class="input-item">
    <label class="input-item__label">
      画像を選択
      <input type="file" />
    </label>
  </div>
</template>


<style>
label > input {
  display: none;
}

label {
  padding: 0 1rem;
  border: solid 1px #888;
} 

label::after {
  content: '+';
  font-size: 1rem;
  color: #888;
  padding-left: 1rem;
}
</style>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119773/adebe874-290c-c0ac-a49f-9c5ccea32b07.png)


若干不恰好だけどできました。

## せっかくだし選択した画像をプレビューできる機能をつける

画像を選択すると下の画像のように、選択した画像&画像ファイル名が表示されるようなプレビュー機能も実装してみます。

<img width="311" alt="プレビュー機能実装" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119773/1b8f6381-9349-0a2d-edcf-f052885a1f47.png">


やりたいことは5つ
・選択したら画像が表示される
・選択した画像ファイル名が表示される
・画像が表示されたら画像選択のボタンが消える
・画像ファイル名が表示される
・画像ファイルの`close`をクリックするともう一度`画像を選択`ボタンが表示される



### 選択した画像表示する場所と表示methodsを実装する

```index.vue
<template>
  <div class="contents">
    <label v-show="!uploadedImage" class="input-item__label"
      >画像を選択
      <input type="file" @change="onFileChange" />
    </label>
    <div class="preview-item">
      <img
        v-show="uploadedImage"
        class="preview-item-file"
        :src="uploadedImage"
        alt=""
      />
      <div v-show="uploadedImage" class="preview-item-btn" @click="remove">
        <p class="preview-item-name">{{ img_name }}</p>
        <e-icon class="preview-item-icon">close</e-icon>
      </div>
    </div>
  </div>
</template>

<script>
import EIcon from '../components/EIcon.vue';

export default {
  components: {
    EIcon,
  },
  data() {
    return {
      uploadedImage: '',
      img_name: '',
    };
  },
  methods: {
    onFileChange(e) {
      const files = e.target.files || e.dataTransfer.files;
      this.createImage(files[0]);
      this.img_name = files[0].name;
    },
    // アップロードした画像を表示
    createImage(file) {
      const reader = new FileReader();
      reader.onload = e => {
        this.uploadedImage = e.target.result;
      };
      reader.readAsDataURL(file);
    },
    remove() {
      this.uploadedImage = false;
    },
  },
};
</script>
```

これで、
・選択したら画像が表示される
・選択した画像ファイル名が表示される
・画像が表示されたら画像選択のボタンが消える
・画像ファイル名が表示される
・画像ファイルの`close`をクリックするともう一度`画像を選択`ボタンが表示される
実はこれで5つは実装できましたが、現状では長いファイル名の場合、折り返してしまいデザインが崩れてしまいました。


<img width="313" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119773/e1138ea0-2a03-f640-1f24-955ec213b9b4.png">

この通り、ファイル名が長い時デザインが崩れてしまうため、短くカットしてデザイン崩れしないようにしたいと思います。

### 3点リーダー、拡張子残し、全角、半角の判別を組み合わせてファイル名表示を工夫する


```js

onFileChange(e) {
      const files = e.target.files || e.dataTransfer.files;
      this.createImage(files[0]);
      // 拡張子で分ける（※.が1つの想定です）
      const imgNameExe = files[0].name.split('.');

      // 拡張子から前
      let imgName = imgNameExe[0];

      // 拡張子から後ろ
      const imgExe = imgNameExe[1];

      // 表示したいMaxのByte数（全角10文字、半角20文字）
      const maxBytes = 20;
      const imgNameBytes = encodeURIComponent(imgName).replace(/%../g, 'x').length;

      // 画像ファイルとMax Byte数の比較
      if (imgNameBytes > maxBytes) {
        const zenkaku = imgNameBytes - imgName.length;
        if (zenkaku > 0) {
          imgName = imgName.slice(0, maxBytes / 2 - imgName.length) + '..';
        } else {
          imgName = imgName.slice(0, maxBytes - imgNameBytes) + '..';
        }
      }

      // 短くカットしたものと.と拡張子の文字列の連結
      imgName = imgName + '.' + imgExe;
      this.img_name = imgName;
    },
```

こうして実装ができました。

![画像選択画面録画.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119773/ca38abaf-b4b4-d3fa-5dd2-ab8faeb0ac9e.gif)



現在、長いファイル名の表示が、XXX...png などの表示になっているけれど、おそらくXXX...XXX.pngのようにファイル名の先頭と末尾が見えるような工夫が必要そうです。


ですが、今回はここまで！