20200619のvue.jsに関する記事は11件です。

【Vue.js 】 watch データの変更を監視

はじめに

watchは特定のデータや算出プロパティの状態の変化を監視して、変化があったときに登録した処理を実行するプロパティ。
データや算出プロパティの変更が処理のトリガー(きっかけ)となる。

使い方

watchオプションに監視するデータの名前変化した時に呼び出されるハンドラ(関数)を定義する。
データの比較が必要な場合は、第1引数として新しい値、第2引数として古い値が受け取れる。
※ここではインスタンスが生成されると同時にwatchが呼び出されている。

基本的な書き方

sample.js
new Vue({
  watch: {
    監視するデータ: function(新しい値, 古い値) {
      // データが変化した時に行いたい処理
    },
    // ↓ オブジェクトのプロパティも監視できる
    'item.value': function(新しい値, 古い値) {
      // 処理
    }
  }
})

↓ データ等を入れて記述してみる

sample.js
new Vue({
  el: 'app',
  data: {
    flag: true
  },
  methods: {
    changeFlag: function() {
      this.flag = !this.flag
    }
  },
  // データflagをwatchオプションに定義
  watch: {
    flag: function(newVal, oldVal) {
      // データの値が変化した時にコンソールに新しい値と古い値を出力
      console.log(newVal, oldVal)
    }
  }
});

watchのオプション

下記のオプションを任意で設定できる(デフォルトではfalse)

プロパティ 説明
deep Boolean ネストされたオブジェクトも監視する
immediate Boolean 初期読み込み時にも呼び出す

オプションを使用する際はオブジェクトにまとめ、handlerプロパティにハンドラを定義する。

sample.js
new Vue({
  el: 'app',
  data: {
    list: [
      { name: '鉛筆', price: 98 },
      { name: '消しゴム', price: 100 }
  ]
  },
  watch: {
    // listもしくはlist内のプロパティに変更があった場合に処理が実行される
    list: {
      handler: function(newVal, oldVal) {
        console.log(newVal)
      },
      // deepがfalseだとオブジェクト内の値が変更されても処理が実行されない
      deep : true,
      immediate: true
    }
  }
});

watchオプションとcomputedオプションの違い

同じようなプロパティとしてデータ変更時にのみ処理を実行するcomputedオプションがある。
computedオプションについてこちら

まずはwatchプロパティとcomputedプロパティで同じ処理を記述して比較してみる。
↓ firstNum と secondNum を掛けてresultNumを表示する

sample.js
// watchの場合
new Vue({
  el: 'app',
  data: {
    firstNum: 2,
    secondNum: 3,
    resultNum: 6
  },
  watch: {
    firstNum: function(newVal) {
      this.resultNum = newVal * this.secondNum;
    },
    secondNum: function(newVal) {
      this.resultNum = this.secondNum * newVal;
    },
  }

})
sample.js
// computedの場合
new Vue({
  el: 'app',
  data: {
    firstNum: 2,
    secondNum: 3
  },
  computed: {
    resultNum: function() {
      return this.firstNum * secondNum;
    }
  }

})

このようにcomputedオプションの方が簡潔で可読性の高いコードが記述できる。そのため、computedオプションで記述できる場合はcomputedオプションを使用するのが望ましい。

watchオプションは下記のような場面で使う。

◎ 非同期通信(APIアクセス)などの複雑な処理を行う場合
 (computedオプションでは処理できない)
◎ 更新前と更新後の値を使う場合
◎ 処理を実行してもデータを返さない場合

メソッド内でwatchを登録

インスタンスメソッドとしてthis.$watchと定義することでメソッド内でもwatchが登録できる。
今までの書き方とは少し異なり、下記のように引数を渡してあげる。

sample.js
// 書き方
this.$watch('監視したいデータ',ハンドラ(関数),オプション)

// ライフサイクルのcreatedにwatchを登録してみる
created() {
  this.$watch('value', function(newVal, oldVal) {
    // 処理
  }, {
    deep: true,
    immediate: true
  })
}

監視の解除

返り値を使って監視を解除することができる。

sample.js
var unwatch = this.$watch('value' function() {})

unwatch()

一度だけ監視

一度しか監視しないwatchをunwatch()を使って登録する。

sample.js
new Vue({
  el: 'app',
  data: {
    edited: false,
    list: [
      { name: '鉛筆', price: 98 },
      { name: '消しゴム', price: 100 }
    ]
  },
  created() {
    var unwatch = this.$watch('list', function() {
      // データlistが変更されたらeditedをtrueにする
      this.edited = true
      // データが変更されたら監視を解除
      unwatch()
    }, {
      deep: true
    })
  }
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ビデオチャット作るときの逆引きTipsまとめ

リモートワークのため社内用ビデオチャットツールを先日作ったのですが、せっかくなので一部をオープンソースにしてGitHubにあげておきました。
ukkz/v-sky: Single page video chat appplication
※リファクタしてないので見辛いです

SPAなのでGiHub Pages上でそのまま利用もできます。
https://ukkz.github.io/v-sky

で、一旦開発が落ち着いたので、作業時のメモを簡単に整理して公開しておくことにします。

構成

  • Vue.js (@vue/cli v4.3.1)
  • Vuetify @2.3.1
  • Vuex @3.4.0
  • Vue-Router @3.3.4
  • SkyWay

まとめ一覧

ビデオストリームの情報を知りたい

使ったところ:実際の映像アスペクト比とブラウザ画面のアスペクト比から複数映像配置時の自動最適化 

VideoTrack.getSettings()でいろいろ情報がとれました。

// メディアストリームオブジェクト
const my stream = new MediaStream;
// ビデオトラックの配列(ないこともある)
const video_tracks = mystream.getVideoTracks();
// 少なくとも1つのビデオトラックがあるとき、そのトラックの情報を見る(空配列はtrue判定になるので必ずインデックス0を確認する)
if (video_tracks[0]) {
  const track_settings = video_tracks[0].getSettings();
  console.log( track_settings );
  /* 検出例(640x480の映像のとき)
    aspectRatio: 1.3333333333333333,
    deviceId: "1d3fa9…",
    frameRate: 30.000030517578125,
    groupId: “66fa41…",
    height: 480,
    resizeMode: "none",
    width: 640,
  */
}

アス比は循環小数になることがあるためあまり使い勝手がよくなさそうです。
ストリームやトラックに関しては以下がたいへん詳しく書かれておりました。
getUserMedia()の設定項目|npaka|note

ビデオタグに関して幅と高さを知りたい・設定したい

使ったところ:映像にcanvasをオーバーレイさせるときのcanvasサイズの指定 

<video id="my-video" :srcObject.prop="my_stream" width="160px" height="120px"></video>
<style>
#my-video {
  width: 320px;
  height: 240px;
  background-color: red;
}
</style>

上記のDOMの場合のビデオタグの大きさは、インライン指定よりもCSS指定のほうが優先されます。
実際のビデオトラック(映像)はCSSで指定されたサイズ(CSSしていなければインライン指定サイズ)の枠内に収まるように配置されます。
上記で枠内の余白部分はbackground-colorで指定された色になります。
CSSもインラインもどちらも指定されていなければ、ビデオトラックのオリジナルサイズで配置されます。

const video_element = document.getElementById('my-video’);
// 以下、取得できる様々なサイズの例
console.log( video_element.width );        // 160px(インライン指定の幅・未指定ならundefined)
console.log( video_element.height );       // 120px(インライン指定の高さ・未指定ならundefined)
console.log( video_element.offsetWidth );  // 320px(CSS指定の幅・mounted前ならundefined)
console.log( video_element.offsetHeight ); // 240px(CSS指定の高さ・mounted前ならundefined)
console.log( video_element.videoWidth );   // 640px(ビデオトラックの実際の幅・メタデータ受信前ならundefined)
console.log( video_element.videoHeight );  // 480px(ビデオトラックの実際の高さ・メタデータ受信前ならundefined)

メディアストリームをビデオタグに動的に追加したい [Vue.js]

使ったところ:相手からストリームを受信する前にvideoタグをDOMにマウントしたいとき 

<video :srcObject.prop="my_stream"></video>

:srcObject.propdataで定義した変数を指定しておいて、相手からストリーム受信したり自分がgetUserMediaしたりしたタイミングでメディアストリームオブジェクト(空の場合new MediaStream()で生成)を入れればよいです。
Vue歴2ヶ月ですが、リアクティブっていいなあと思いました。

ビデオタグのプロパティについて

使ったところ:ビデオタグぜんぶ(ほぼテンプレ構文) 

<video :srcObject.prop="my_stream" muted autoplay playsinline></video>

muted:音声再生しません。自身のストリームだとハウリング起こすので必須の属性です。
autoplay:mutedとともに有効のとき自動再生します。カメラからのストリームだとこれがないとそもそもデータが流れないので表示されません。インライン(videoタグ内)で指定しない場合は、スクリプト内のどこかで明示的にplay()を実行しないと再生されません。
playsinline:インライン(Webページ上の表示そのまま)で再生します。これがないとiOSでは自動的に映像がフルスクリーンになってしまうのでほぼ必須です。

ボタンの背景色にあわせて文字色の白黒を変えたい [Vuetify]

使ったところ:ボタンのデザイン全般 

Vuetifyのv-btnでは基本的にdarkプロパティをつけておくと良さそうです。
背景色にあわせて文字色も自動で変えてくれる唯一のコンポーネントのようです。

<v-btn rounded dark color="blue darken-4" @click=“any">
  <v-icon>mdi-message-text</v-icon>
  チャット
</v-btn>

v-dialogを使った子コンポーネントを親コンポーネント側から開閉させたいが反応しない [Vuetify, Vue.js:v-model]

使ったところ:ボタンクリックでテキストチャットダイアログを開くとき 

v-dialogv-ifなどではなくv-modelで開閉します。
(開いているときにダイアログ外側をクリックorタップで閉じれるようにするため)
このv-dialogを用いた子コンポーネントを親コンポーネントから制御するときは、computedにおいてゲッターとセッターを利用するとうまくいきます。
Vue.jsのv-modelを正しく使う - Qiita

親コンポーネント
<template>
  <v-container>
    <!-- v-dialogを開くボタン -->
    <v-btn @click.stop="chat_open = true">チャットを開く</v-btn>
    <!-- :showで開く・toggleイベント受信で閉じる -->
    <ChatWindow :show="chat_open" @toggle="chat_open = $event" />
  </v-container>
</template>

<script>
  data() {
    return {
      chat_open: false,
    }
  }
</script>
子コンポーネント
<template>
  <v-dialog v-model="toggle">
    // 略
  </v-dialog>
</template>

<script>
  name: 'ChatWindow',
  props: {
    show: {
      type: Boolean,
      required: true,
      default: false,
    },
  },
  computed: {
    toggle: {
      get() { return this.$props.show },
      set(v) { this.$emit('toggle', v) }
    },
  },
</script>

ゲッターはprops変数を受け取り、セッターは$emitで親コンポーネントに変化を通知します。
親側では変化を受信してpropsに与えた変数を子側から受け取った$event値に変更します。

チェックボックスなど設定値の変化でVuexの値を変えたい [Vue.js:v-model]

使ったところ:設定ダイアログで特定機能をスイッチボタンでON/OFFするとき 

v-dialogの例と同様で、入力系コンポーネントでv-modelを使うときはゲッターとセッターを使います。
ゲッターは$store.stateから取り出し、セッターでは$store.commitでVuexにセットします。

<template>
  <v-container>
    <v-switch v-model="speech_onoff" :label="` 音声認識:${(speech_onoff)?'有効':'無効'} `"></v-switch>
  </v-container>
</template>

<script>
  computed: {
    speech_onoff: {
      get() { return this.$store.state.config.speech_recognition },
      set(onoff) { this.$store.commit('speechConfig', onoff) },
    },
  },
</script>

テキストチャットで長文入力したときに折り返しできない・overflowやword-breakが効かない [Vuetify]

使ったところ:テキストチャット内の吹き出し部分(個別の発言) 

pタグのようなブロック要素で囲みtext-wrapクラスを適用させましょう。
(Vuetifyのpタグはデフォルトでmargin-bottomが入ってしまうのでmb-0クラスと組み合わせるとよい)
text-align: left;にしておくと折り返しても左寄せで表示してくれます。

文字起こししたい

使ったところ:そのまま(喋った内容を文字列にしてテキストチャットに逐次送信) 

SpeechRecognition(Web Speech API)が便利です。すごく簡単。
Webページでブラウザの音声認識機能を使おう - Web Speech API Speech Recognition - Qiita

音声入力を明示的に指定できるわけではない(というか選べない)みたいなので、マイクミュート時は手動でabortすべきですね。
2020年6月現在もChromiumベースのブラウザしか対応していないので注意です。
Samsung, Baidu, QQといった中韓系ブラウザは利用者層が音声入力を好むためか対応しておりなかなか興味深いです。

背景切り抜きなどの加工をしたい

使ったところ:画面共有とカメラ映像の合成(公開してないコンポーネントで利用) 

今回オープン版には実装してませんが内部向けで消したり合成したりの用途にはOpenCV.jsのパターンを使いました。
findContoursで輪郭抽出後、一番外の階層のある程度の大きさの輪郭をアルファで塗りつぶすだけで、クロマキー色を指定したりしなくても消したり背景合成したりすることができました。
上記参考記事では画像に対して利用されていますが、映像に対しても15fps程度の速さで処理できたのでそのうち別途記事かきます。

スクリーン共有でストリームを置き換えたら音声が送られなくなった

使ったところ:画面共有とカメラ映像の切替時 

getUserMediaで音声取得 + getDisplayMediaで映像取得 を組み合わせたメディアストリームを作ります。
MediaStream APIで画面キャプチャとマイクからの音声を同時に収録する - エムスリーテックブログ

const display_stream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: false
});
const camera_stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
});
// ディスプレイ映像と外部音声のトラックから新しくメディアストリームを作成
const merged_stream = new MediaStream([ display_stream.getVideoTracks()[0], camera_stream.getAudioTracks()[0]  ]);
// または、既存のカメラからのストリームに画面キャプチャ用のトラック追加してもいい(受信側で区分する必要あり)
camera_stream.addTrack( display_stream.getVideoTracks()[0] );

スマホとPCでエレメントのサイズ・デザインを簡単に切り替えたい [Vutify]

使ったところ:レスポンシブデザインとして全体 

v-if$vuetify.breakpoint.smAndDownを組み合わせましょう。
ブレイクポイントはほかにもたくさんあります。
Breakpoints — Vuetify.js

v-ifv-elseで同一機能を異なるデザインで作成して並列に配置すると画面幅によって表示切り替えが可能です。
以下はボタンの例で、PCではアイコン+文字だが、smサイズ以下だとアイコンのみのボタンが表示されます。

<v-list-item-action>
  <v-btn v-if="$vuetify.breakpoint.smAndDown" fab depressed small @click="open = true"><v-icon>mdi-open</v-icon></v-btn>
  <v-btn v-else rounded depressed @click="open = true"><v-icon>mdi-open</v-icon>開く</v-btn>
</v-list-item-action>

ビデオを上下中央センタリングしたい

使ったところ(トリミングなし):自分の映像の確認用・チャットルーム内で話者の映像を拡大する用
使ったところ(トリミングあり):チャットルーム内での基本的な映像表示 

単純なCSSだけどメモ
一番外側の枠となるdivはあらかじめサイズが決まっている(100%指定とかでもよい)ものとし、その中にビデオを配置するときにどうしましょ?という話です。

<div class="video-frame">
  <video :srcObject.prop="local_media_stream" muted autoplay playsinline></video>
</div>

上記DOMが基本構造とします。

トリミングなし(上下または左右に余白を作る)

div.video-frame {
  position: relative;
  background-color: black;
  width: 100%;  // 枠幅:固定値でもよい
  height: 100%; // 枠高さ:固定値でもよい
}

div.video-frame video {
  position: absolute;
  top: 50%; left: 50%;
  transform: translateY(-50%) translateX(-50%) scale(-1, 1);
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
}

ビデオ側でwidthheightautoにしつつも最大サイズを親要素の100%にしておくとアスペクト比を変えずにフィットさせられます。
このままだと左上に寄ってしまうので、topleftを使って親要素の上左からそれぞれ半分(つまり真ん中)に寄せつつ、translateで自要素の幅と高さ半分ずつ戻すことでセンタリングさせています。

また、センタリングとは関係ないですが、scale(-1, 1)は左右フリップして鏡状態にしてくれるので自分のカメラ映像の時のみ使います。transformは多重に適用できないため、別のクラスなどでtransform: scale(-1, 1);を指定してもうまく反映されません。そのためここでまとめて指定しています。

余白は外枠であるvideo-frameクラスで指定した背景色になります。

トリミングあり(上下をフィットさせ左右をカットする)

div.video-frame {
  position: relative;
  background-color: black;
  width: 100%;  // 枠幅:固定値でもよい
  height: 100%; // 枠高さ:固定値でもよい
  overflow: hidden; // はみ出ても表示させない
}
div.video-frame video {
  position: absolute;  
  left: 50%;
  transform: translateX(-50%);
  width: auto;
  height: 100%;
}

width: auto;height: 100%;で、アスペクト比を維持したまま高さが外枠に合わせられます。
そのあと左右方向に対してleft: 50%;translateX(-50%)でセンタリングさせています。

トリミングあり(左右をフィットさせ上下をカットする)

div.video-frame {
  position: relative;
  background-color: black;
  width: 100%;  // 枠幅:固定値でもよい
  height: 100%; // 枠高さ:固定値でもよい
  overflow: hidden; // はみ出ても表示させない
}
div.video-frame video {
  position: absolute;  
  top: 50%;
  transform: translateY(-50%);
  width: 100%;
  height: auto;
}

上下と左右、widthheightを入れ替えると左右方向のフィッティングおよび上下センタリングにも対応できます。

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

Firestoreに今月と前月のデータがあるか調べる処理を実装した

Nuxt.js+Firebaseを勉強しています。

やりたいこと

月ごとのデータを登録しているのですが、月初にログインしたとき今月分データがないときに前月分データをコピーして今月分データとする機能を実装したいと考えました。
ログインしてすぐにこの判定をしたいので、処理タイミングは mounted とします。

環境

  • Firebase 8.3.0
  • Vue CLI 4.0.5
  • Nuxt.js 2.11.1
  • VuexFire 3.2.0

課題1

Firestoreのドキュメント の「クエリの制限事項」にある通り、Firestoreでは論理 OR クエリを利用できません。

今月分と前月分をWHERE句のOR条件で検索するつもりでしたが、これができないことが分かりました。今月と前月の2回に分けてデータベースにアクセスすることもできますが、できればアクセスは1回で済ませたいです。

課題2

VuexFireを使っています。 computed でマッピングされた情報を mounted で使うことができればいいのですが、これもできませんでした。

this.$store.dispatch をするタイミングは mounted です。この dispath の後に続けてコピー処理を実装すると、dispath 完了よりも先にコピー処理が実行されます。データベースアクセスは非同期だからです。

実装したこと

Firestoreのデータベース構造を以下のようにしました。

コレクション ドキュメント サブコレクション ドキュメント フィールド
notes (userID) pages (年月) (各データ)
notes (userID) index YM (年月)

データを登録するときに、pagesサブコレクションの年月ドキュメントにデータを登録します。これに加えて、indexサブコレクションのYMドキュメントに登録した年月を記録することにしました。
mounted でindexサブコレクションのYMドキュメントを取得することで、どの年月がpagesサブコレクションに登録されているか判断できます。

pages/index.vue
// 省略

<script>
  // 省略

  mounted () {
    if (this.$store.getters['auth/isLoggedIn']) { // ログインしている場合は以下の処理を続行
      let userID = this.$store.getters['auth/getUid'];

      let docRef = db.collection('notes').doc(userID).collection('index').doc('YM');
      let self = this;
      docRef.get().then( function(doc) {
        if (doc.exists) {
          // ドキュメントを取得できたら、ここでチェック&コピー処理
        }
      });
    }
  },

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

Vue.js コンポーネント作成

コンポーネントを作成する

グローバル登録、ローカル登録、シングルファイルコンポーネントの作成方法がある

    <div id="app">
        <paragraph></paragraph>
    </div>
    <script>
        Vue.component('paragraph',{
            data:function(){
                return {
                    counter: 0
                }
            },
            template:'<div><button @click="counter++">{{counter}}</button></div>'
        })
    </script>

<paragraph message="hello"></paragraph>
messageプロパティを受け取り表示させる

    <div id="app">
        <paragraph message="hello"></paragraph>
        <paragraph message="world"></paragraph>
    </div>
    <script>
        Vue.component('paragraph',{
            props:['message'],
            template:'<div>{{ message }}</div>'
        })

プロパティの値が長い場合、<slot></slot>で同様の動きが可能

    <div id="app">
        <paragraph>hellohellohello</paragraph>
    </div>
    <script>
        Vue.component('paragraph',{
            template:'<div><slot></slot></div>'
        })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

el-uploadを使ったファイル添付機能

概要

laravel+vueで作成したWebサイトにて添付ファイル機能を実装したのでメモ。
フロント側のUIはelement-uiのel-uploadで作った。

コード

vue側

<el-row :gutter="24" class="attachment-row">
  <el-col :span="24">
    <span class="item-label"><i class="fas fa-paperclip icon"></i>添付ファイル</span>
    <el-upload
      action="#"
      list-type="text"
      class="attachments-upload"
      :before-remove="beforeAttachmentsRemove"
      :on-change="addAttachments"
      :file-list="attachmentsList"
      :auto-upload="true"
      :show-file-list="true"
      :multiple="false"
      :drag="true"
      :limit="attachments_limit"
      :http-request="AttachmentsHttpRequest"
    >
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">
        ここにドラッグするか <em>ここをクリックして</em>アップロードしてください。
      </div>
      <div slot="file" slot-scope="{ file }">
        <div class="contents" :class="{ delete: file.isDelete }">
          <span v-if="file.isDelete" class="el-upload-list__item-deleted">
            <span class="el-upload-list__item-label">
              <span class="each-row">削除は更新実行後に反映されます。</span>
            </span>
          </span>
          <li>
            <div v-if="!file.isUploaded">
              <i class="el-icon-document icon"></i>
              <span v-if="!file.isUploaded" class="el-upload-list__item-noactions">
                <span class="el-upload-list__item-label">
                  <span class="each-row">アップロード</span>
                  <span class="each-row">待ち</span>
                </span>
              </span>
              <span class="filename">{{ file.name }}</span>
              <i
                class="el-icon-close"
                :disabled="file.isDelete"
                @click="deleteAttachments(file)"
              ></i>
            </div>
            <div v-else class="available-download">
              <i class="el-icon-document icon" @click="downloadAttachment(file)"></i>
              <span class="filename" @click="downloadAttachment(file)">{{
                file.name
              }}</span>
              <i
                class="el-icon-close close-icon"
                :disabled="file.isDelete"
                @click="deleteAttachments(file)"
              ></i>
            </div>
          </li>
        </div>
      </div>
    </el-upload>
  </el-col>
</el-row>

(略)
private attachments_limit: number = 10;
private attachmentsList: AttachementData[] = [];
private fileReader: FileReader = new FileReader();

//添付ファイル追加(AttachmentsHttpRequestより後で呼ばれる)
addAttachments(file: any, fileList: any) {
  // File size limitation
  const isLt5M = file.size / 1024 / 1024 < 100;
  if (!isLt5M) {
    this.$message.error("アップロードできるファイルサイズは100Mまでです。");
    this.deleteAttachments(file);
    return false;
  }
  // Exceed limit
  if (this.attachmentsList.length >= this.attachments_limit) {
    this.$message.error("アップロードできるファイルは" + this.attachments_limit + "つまでです。");
    this.deleteAttachments(file);
    return false;
  }

  file.id = file.uid;
  file.isUploaded = false;
  file.isDelete = false;
  this.attachmentsList.push(file);
}

//添付ファイル削除
deleteAttachments(file: any) {
  if (file.isUploaded) {
    //アップロード済み

    //画面表示リストの削除フラグ立てる
    let t = this.attachmentsList.filter(function(e) {
      return e === file;
    });
    t[0].isDelete = true;

    //サーバへ送るリストの削除フラグを立てる
    let t2 = this.minute.attachments_base64.filter(function(e) {
      return e.id == file.uid;
    });
    t2[0].isDelete = true;
  } else {
    //未アップロード

    //画面表示リストから消す
    this.attachmentsList = this.attachmentsList.filter(function(e) {
      return e !== file;
    });

    //サーバへ送るリストから消す
    this.minute.attachments_base64 = this.minute.attachments_base64.filter(function(e) {
      return e.id != file.uid;
    });
  }
}

//添付ファイル追加時のアップロード
AttachmentsHttpRequest(options: any) {
  let file = options.file;
  let filename = file.name;
  if (file) {
    this.fileReader.readAsDataURL(file);
  }
  this.fileReader.onload = () => {
    if (!this.minute.attachments_base64.length) {
      this.minute.attachments_base64 = [];
    }
    this.minute.attachments_base64.push({
      id: file.uid,
      filetype: file.type,
      original_filename: filename,
      comment: "", //アップロード時は空
      base64data: this.fileReader.result,
      isUploaded: false,
      isDelete: false
    });
  };
}

//画面初期表示時に既存添付ファイルを反映する
defaultAttachmentsListLoad(data: Attachement[]) {
  this.attachmentsList = [];
  data.forEach((value, index) => {
    if (this.attachmentsList.length < this.attachments_limit) {
      this.attachmentsList.push({
        id: value.id,
        name: value.original_filename,
        comment: value.comment,
        isUploaded: true,
        isDelete: false,
        uid: value.id,
        url: ""
      });
    }
  });
}

//添付ファイルの属性入力時にdeleteキーやBackSpaceキーでリスト削除が走らないようにするための処理
beforeAttachmentsRemove(file, fileList) {
  return false;
}

/**
 * 添付ファイルダウンロード
 */
downloadAttachment(file: any) {
  const _this = this;
  axios({
    url: "/api/minute/download/attachment",
    params: { minute_id: this.minute.id, attachment_id: file.uid },
    method: "GET",
    responseType: "blob" // これがないと文字化けする
  })
    .then(res => {
      const blob = new Blob([res.data], {
        type: res.data.type
      });

      //レスポンスヘッダからファイル名を取得します
      var fileName = res.headers["download_filename"];
      fileName = decodeURI(fileName).replace(/\+/g, " ");

      //ダウンロードします
      saveAs(blob, fileName);
    })
    .catch(error => {
      var errorMsg = "不明";

      // 通知
      _this.$message({
        message: "実行に失敗しました。[" + errorMsg + "]",
        type: "error",
        duration: 0,
        showClose: true
      });
    });
}

バックエンド側

[画面表示時のメタデータ取得]
//添付ファイル(メタ情報だけ返す)
$attachments_base64 = [];
if ($minute->attachments) {
  $attachment_paths = json_decode($minute->attachments);
  foreach ($attachment_paths as $file) {
    $attachments_base64[] = [
      'id' => $file->id,
      'original_filename' => $file->original_filename,
      'isUploaded' => true,
      'isDelete' => false
    ];
  }
}
$minute['attachments_base64'] = $attachments_base64;
[保存時]
//添付ファイルをファイルとして保存する
$attachment_json = [];
foreach ($request->attachments_base64 as $file) {
  $path = str_pad($minute->id, 10, '0', 0) . '_' . $file['id'];
  if ($file['isUploaded'] && !$file['isDelete']) {
    //アップロード済みで削除でない->何もしない
    $attachment_json[] = ['id' => $file['id'], 'path' => $path, 'original_filename' => $file['original_filename']];
  } elseif ($file['isUploaded'] && $file['isDelete']) {
    //アップロード済みで削除する->実体ファイルを削除しJSONに書き込まない
    if (Storage::disk('local')->exists('/minute/' . $path)) {
      Storage::disk('local')->delete('/minute/' . $path);
    }
  } elseif (!$file['isUploaded']) {
    //アップロード済されていない->アップロードする
    $fileData = $file['base64data'];
    if (count(explode(';', $fileData)) > 1) {
      list(, $fileData) = explode(';', $fileData);
    }
    if (count(explode(',', $fileData)) > 1) {
      list(, $fileData) = explode(',', $fileData);
    }
    $fileData = base64_decode($fileData);
    Storage::disk('local')->put('/minute/' . $path, $fileData);
    $attachment_json[] = ['id' => $file['id'], 'path' => $path, 'original_filename' => $file['original_filename']];
  }
}
$minute->attachments = json_encode($attachment_json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
$minute->save(); //DBに添付ファイルパスを更新
[ダウンロード時]
public function attachmentDownload(Request $request)
{
  $minute = Minutes:: firstOrNew(['id' => $request->minute_id]);

  //添付ファイル
  if ($minute->attachments) {
    $attachment_paths = json_decode($minute->attachments);
    foreach ($attachment_paths as $file) {
      if ($file->id == $request->attachment_id) {
        $mimeType = Storage::mimeType('/minute/' . $file->path);
        $headers = [['Content-Type' => $mimeType], 'download_filename' => urlencode($file->original_filename)];
        return Storage::response('/minute/' . $file->path, $file->original_filename, $headers, 'attachment');
      }
    }
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue CLI+ Laravel 5.8 で、チャット作成 firebase Cloud Messaging対応

概要

前の 、クロスドメイン構成の
Vue-CLI + Laravel 関連となり。チャット機能の作成です

・会員制となり、Google認証で
 ログインに対応しています。
・PWAも対応しています
・ 前のLaravel版チャットを、Vue CLIに移植した形です

構成

PWA
firebase Cloud Messaging / FCM
Notification API
Vue CLI
vue-router
vue/cli-service : 4.4.0
・API サービス:
Laravel 5.8
nginx
mysql
・フロント設置ドメイン ,Vue-CLI:
netlify / ホスティングサービス


画面

・web push 受信時の、通知API で。タスクバー表示

ss-chat-0619-noti.png

・チャット画面、詳細

ss-chat-0619-show.png

・一覧
左側に、参加チャットの表示
ss-chat-0619a.png


Vue components

・index
https://github.com/kuc-arc-f/vue_spa3a_4chat/blob/master/src/components/Chats/Index.vue

・create
https://github.com/kuc-arc-f/vue_spa3a_4chat/blob/master/src/components/Chats/new.vue

・show
https://github.com/kuc-arc-f/vue_spa3a_4chat/blob/master/src/components/Chats/show.vue

package.json

https://github.com/kuc-arc-f/vue_spa3a_4chat/blob/master/package.json


参考のページ

https://knaka0209.hatenablog.com/entry/lara58_31cross_chat

・Vue CLI+ Laravel 5.8で、クロスドメイン SPA構成/PWA対応のCRUD作成する。
https://qiita.com/knakaqi/items/1bae7a540aa13ce8233b


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

Nuxt.js で Slack のようなリアクション機能を実装する

Prologue

Slack のようなリアクション機能をNuxt.js(Vue.js) を使って実装してみました。
CSSの実装やカウント、リアクションをつけたユーザーの表示等に関しては割愛して、今回は Picker と絵文字の表示部分についてのみ説明します。
公式のドキュメントがかなりしっかり作られているので、そちらも確認してみてください。

参考:

環境

  • macOS: 10.15.4
  • terminal: iTerm
  • node.js: 10.16.0
  • エディタ: VS Code
  • 使用言語: TypeScript, Nuxt.js, CompositionAPI(v0.5.0)

  • Project概要

    • パッケージマネージャ: yarn

インストール

emoji-mart-vue をインストールします。

yarn add emoji-mart-vue

セットアップ

挙動を見たかった為、 Nuxt のプロジェクトを新しく作成し、必要最低限の code を書いていきます。

<template>
  <div class="section">
    <div>
      <Picker />
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from '@vue/composition-api'
// @ts-ignore
import { Picker } from 'emoji-mart-vue'

export default defineComponent({
  name: 'EmojiPicker',
  components: {
    Picker,
  },
})
</script>

最低限これだけでも emoji-picker は表示されました。

プロパティをセット

emoji-mart-vue は様々なプロパティを用意してくれているので、必要なものを確認していきます。
<Picker /> component にプロパティを渡していきます。

参考: https://github.com/jm-david/emoji-mart-vue

<template>
  <div class="section">
    <div>
      <Picker
        title="pick emoji"
        emoji="grinning"
        set="twitter"
        color="pink"
      />
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from '@vue/composition-api'
// @ts-ignore
import { Emoji, Picker } from 'emoji-mart-vue'

export default defineComponent({
  name: 'EmojiPicker',
  components: {
    Picker
  },
})
</script>

内容:

  • title: フッターに表示されるテキスト
  • emoji: フッターに表示される初期値の絵文字
  • set: pickerに使われる絵文字のデザイン
  • color: ヘッダーに使われるカテゴリ hover 時のカラー

個人的には Color を標準で簡単に切り替えられるのはすごい助かりました。CSSをいじればいいだけですが、バージョンアップ等を考えるといじらないに越したことはないので...

表示する内容(カテゴリ)を限定する

emoji-mart-vue はデフォルトでたくさんの絵文字を提供してくれています。
ただ、プロジェクトによっては不要だったり制限したりする可能性もあるので、カテゴリの制限の仕方を確認しました。
余談ですが、実はここがなかなかうまくいかず、一番てこずりました。ドキュメントをよく読まなかったのが敗因です。

include というプロパティが用意されています。
そこに l18n と同じキーをセットします。複数使用する場合には配列で書くだけです。

<template>
  <div class="section">
    <div>
      <Picker
        title="pick emoji"
        emoji="grinning"
        set="twitter"
        color="pink"
        :include="cat"
      />
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from '@vue/composition-api'
// @ts-ignore
import { Emoji, Picker } from 'emoji-mart-vue'

export default defineComponent({
  name: 'EmojiPicker',
  components: {
    Picker
  },
  setup(){
    const cat = ['search', 'recent', 'people', 'objects']
    return { cat }
  }
})
</script>

選択した絵文字を任意の場所に表示する

  • Emoji という component が用意されているので import します。
  • Pickerselect という event が用意されているため、そこから emoji object を取得します。
  • Emoji component の emoji プロパティ に bind します。
<template>
  <div class="section">
    <div v-for="(item, index) in state.selectedItem" :key="index" class="emoji-wrapper">
      <div>
        <Emoji :emoji="item" :size="20" class="emoji-image" />
      </div>
    </div>
    <div>
      <Picker
        title="pick emoji"
        emoji="grinning"
        set="twitter"
        color="pink"
        :include="cat"
        @select="selectEmoji"
      />
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive } from '@vue/composition-api'
// @ts-ignore
import { Emoji, Picker } from 'emoji-mart-vue'

export default defineComponent({
  name: 'EmojiPicker',
  components: {
    Picker,
    Emoji
  },
  setup(props, context) {
    const state = reactive({
      selectedItem: ['grinning'],
    })

    const cat = ['search', 'recent', 'people', 'objects']

    function selectEmoji(item: any) {
      if (!state.selectedItem) {
        state.selectedItem = item.unified
      }
      state.selectedItem.push(item)
    }

    return {
      selectEmoji,
      state,
      cat,
    }
  }
})
</script>

これで表示されるはずです。

Emoji component を使わないパターンを検討する

ここまでで特に問題なく実装ができました。ただ今回はこの内容をデータとして、なるべくシンプルな形で保存しておく必要がありました。
Emoji Component を使わず、 string だけでやりとりができないものかと方法を探していたら、以下の記事を見つけました。

参考: https://medium.com/@allegra9/add-emoji-picker-to-your-react-chat-app-30d8cbe8d9a6

select event で取得できる object の中にある unified に接頭辞 0x をつけて Unicode とし、それを String.fromCodePoint で絵文字として表示します。

以下の code は 抜粋になります。
state.emoji を template に置くと表示されます。

function selectEmoji(item: any) {
  let splitCode = item.unified.split('-');
  let codeArr = new Array()
    splitCode.forEach((c, index) => {
      splitCode[index] = `0x${c}`
    })
    state.emoji = String.fromCodePoint(...splitCode)
}

注意したいのは、 emoji-mart-vue は肌の色なども変えることができるため、取得した unified の中身は - 区切りの string になる場合があります。
その string 一つ一つの頭に 0x をつけないと、望む形の絵文字は表示されません。
最終的にはこの形で渡せればOKです。

String.fromCodePoint('0x1f44d', '0x1f3fd')

unified の中に - がいくつ存在しうるのか、肌の色以外にもどのパターンでそうなるのか、は一つ一つ検証していないのですが、この形で大丈夫だと思います。
何かあればフィードバックして頂けると助かります。^^

Epilogue

絵文字を表示する、という一見するとたくさんやることがありそうな内容ですが、今回見つけた emoji-martVue.js 用に作られたもの(emoji-mart-vue) があり、助かりました。
ドキュメントもわかりやすく、進めるのが楽しかったです。

最後の unified の部分だけ、実装段階まで知らず、気付かずで焦りましたが... JSが素敵な関数用意してくれていてよかった^^ というJSの勉強にもなりました。

今回言及していませんが、この他にもSlack風にするためにカウントを増やしたり tooltip を実装したりしています。また、絵文字のサイズも任意で変えられたりするので、ぜひ実装する際には公式ドキュメントを見ながら進めてみてください。

何かおかしなところや、こういう方法もある、というものがありましたらお気軽にメンションしてください。

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

Quasar × VSCodeでのデバッグ(ブレークポイント張ったりなど)

1 概要

Quasar FrameworkをVSCodeでデバッグするときの手順
公式サイト

サクッと3ポイント

①Debugger for Chromeをインストールしましょう
.vscode\launch.jsonを作成してこれをコピペ!

launch.json
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
          "type": "chrome",
          "request": "launch",
          "name": "Quasar App: chrome",
          "url": "http://localhost:8080",
          "webRoot": "${workspaceFolder}/src",
          "breakOnLoad": true,
          "sourceMapPathOverrides": {
            "webpack:///./src/*": "${webRoot}/*"
          }
        }
    ]
}

②quasar.conf.jsにsource-mapの設定を追加

quasar.conf.js
    build: {
      //他の設定とか
      devtool: 'source-map',   //←追加
      extendWebpack(cfg) {
      }

③quasar devコマンドを実行後、VSCode側からデバック実行をスタート!
キャプチャ.PNG

終わり

後はブレークポイント張るなり、お好きにどうぞ
quasar devで立ち上げた環境にVSCode側からアタッチするような感じの動きになっているようです

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

Vue.jsからaxiosでGoにパラメータ渡せなくてつまり続けたが、Echo導入したら即解決した

Vueで入力された値をJson形式でGoに渡して処理をさせたかった。
うまい感じに渡せず、詰まったがEcho導入したら即解決した。

やりたいこと

  • VueからPost(非同期)でパラメータを渡して、Goで処理をさせたい。
    • とりあえず、Axiosで送信したparamsがGoで受け取れていることを確認したい。

VueとAxios

あるボタン押下時にtestを実行して、data1とdata2をjson形式でpostする。
Goの処理結果をconsoleに表示する。

axiosはpost時、以下のように渡せば自動的にjson形式にしてくれるらしい。

vue.js(一部抜粋)
<script>

export default {
  name: 'SqlGenerater',
  data() {
    return {
      data1:'1',
      data2:'2',
    }
  },
  methods:{
    test(){
      let params= {
        data1: this.data1,
        data2: this.data2,
      }
      this.$axios.get('/api',{ params: params })
      .then(response => {
        console.log(response.data)
      })
    }
  }
}
</script>

Go(Echo)

Echo公式のrequestにかなりわかりやすく書いてある。
下記のようにすれば自動でバインドできるみたい。すごく便利ぃ

main.go
package main

import (
    "net/http"

    "github.com/labstack/echo"
)

type Test struct {
    Data1  string `json:"data1" form:"data1" query:"data1"`
    Data2  string `json:"data2" form:"data2" query:"data2"`
}

func main() {
    e := echo.New()
    e.GET("/", index)
    e.Logger.Fatal(e.Start(":8081"))
}

func index(c echo.Context) (err error) {
    t := new(Test)
    if err = c.Bind(t); err != nil {
        return
    }
    return c.JSON(http.StatusOK, t)
}

実行したら、ちゃんと取れてることが確認できた。

console
{data1: "1", data2: "2"}

ありがとうEcho

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

Vue.jsで複数モーダルを表示した時に背景(オーバーレイ)が重ならないようにする

こんな感じのモーダルコンポーネントがあるとします。

<template>
    <div class="background" @click.self="closeModal()">
        <div class="Modal--form__medium">
            <div class="Modal__header">
                <div v-show="title" class="Modal__title--center">
                  {{ title }}
                </div>
                <slot name="header_button" />
            </div>
            <slot name="subtile" />
            <div class="Modal__body--dialog">
                <slot name="main" />
            </div>
            <div class="Modal__footer">
                <slot name="footer" />
            </div>
            <slot name="modal" />
        </div>
    </div>
</template>

backgroundクラスにモーダルのオーバーレイにbackground-color:rgba(0,0,0,0.75);みたいなスタイルが設定することが多いと思います。
しかし、そうした場合、複数のモーダルを表示した場合にbackground-colorが重なってしまって濃い色になってしまう場合があります。
つまりは、一つのモーダルの背景のみbackground-colorをつけたいです。

そこで以下のようなmixinを書くことで複数モーダルが出ていても必ず一つのモーダルのみ背景を当てることができます。
document.querySelectorは一番最初に見つけた要素しかとってこないためその仕組みを利用して#backgroundにスタイルを当てています。

   import { mapGetters } from 'vuex';

    export default {
        computed: {
            ...mapGetters({ modals: 'modal/modals' }),
        },
        watch: {
            modals: {
                immediate: true,
                handler(newValue) {
                    this.$nextTick(() => {
                        const background = document.querySelector('#background');
                        if (background) background.style.background = 'rgba(0, 0, 0, 0.5)';
                    });
                }
            }
        }
    };

これを使えば複数のモーダルの管理しやすくなると思いますので是非お試しください。

もっといい方法あるがあればコメントいただければと思います!

そろそろvue3もでるし、勉強熱が高まりますね。

ここまで見ていただきありがとうございました。

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

Vue.js 軽くおさわり

はじめに

フロントエンドのフレームワークを勉強してみようと思った。
ReactVue.jsどちらにしようか悩んだのでとりあえずどちらも軽く触れてみてそこから勉強する対象を決定しようと思います。

軽く触るついでに軽くアウトプットも、ただ軽く触る程度なので内容は薄っぺらいし間違ったことを書いている可能性があります。
公式ドキュメントに従っておさわりしてます。

環境、バージョン

  • OS Windows 10 Home
  • Vue.js 2.x

導入~軽く触る

ここからが内容

導入

軽く程度なのでライブラリはダウンロードしていません。
以下のように定義して導入。

donyu.html
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

組んでみる

Hello World的な

こんなんでできました。

helloVue.js
<div id="helloDiv">
    {{ message }}
</div>

<script type="text/javascript">
    let app = new Vue({el: '#helloDiv', data: {message: 'Hello Vue'}})
</script>

elで対象のタグを取得、data連想配列で定義した情報を{{ <key> }}に設定してる感じですね。
ブラウザで表示するとこのdiv中にはDello Vueが表示されます。

※ちゃんとdivタグより下にscriptタグを入れないと動かないのでご注意を!
フロントエンドを普段触られてる方は大丈夫でしょうが、私は普段フロントエンドを触らないので少しはまりました!

チュートリアルに「ブラウザの JavaScript コンソールを開いて・・・」と記載があります。
ブラウザの開発者ツール画面を開き、コンソールでapp.message = '<変更値>を打つとブラウザに反映されます。
この機能初めて知った・・・。

条件分岐

タグ内にv-if属性を付与することで条件分岐が可能となります。
以下のように組み込む。

vuewif.html
<div id="ifDiv" v-if="see">
    表示するかしないかのやーつ
</div>

<script type="text/javascript">
    let ifApp = new Vue({el: "#ifDiv", data: {see: true}});
</script>

data.seetrueが入ってるのでdiv要素が表示されます。
逆にfalseを設定すると非表示になります。

繰り返し

タグ内にv-for属性を付与することで繰り返しが可能となります。
以下のように組み込む。

vuefor.html
<div id="forDiv">
    <div v-for="itemObj in itemList">
        {{ itemObj.txt }}
    </div>
</div>    

<script type="text/javascript">
    let forApp = new Vue({el: "#forDiv", data: {itemList: [
        {txt: 'アイテムその1'},
        {txt: 'アイテムその2'},
        {txt: 'アイテムその3'},
    ]}})
</script>

itemListに格納されているデータ分繰り返してv-forが設定されているタグを追加します。

※Vue.js公式ドキュメントではリストタグで紹介してます。そちらの方がわかりやすいですね。

※注意事項※
以下のように組み込むとエラーとなります。

errfor.html
<div id="forDiv" v-for="itemObj in itemList">
    {{ itemObj.txt }}
</div>

<!-- ※<script>の中は「vuefor.html」と同じとします。 -->

TODO 上記の説明考える

コンポーネント

htmlのタグごと定義してテンプレート要素を作成するもの。的な感じかな?
違う気がするので実装参照↓

component.html
<ol id="cust">
    <cust-compo v-for="item in liItemList" v-bind:liitem="item" v-bind:key="item.id"></cust-compo>
</ol>

<script type="text/javascript">
    // コンポーネントを定義
    Vue.component('cust-compo', 
    {
        props: ['liitem'],
        template: '<li>{{ liitem.txt }}</li>'
    });

    // 定義したコンポーネントをインスタンス化
    let custCompo = new Vue({el: '#cust', 
        data: {
            liItemList: [
                {id: "001", txt: "あいてむ1"},
                {id: "002", txt: "あいてむ2"},
                {id: "003", txt: "あいてむ3"},
            ]
        }
    });
</script>

少し複雑ですね。
例のようにタグを含んだ情報をテンプレートとして定義すると他の機能で使いまわせる独自タグになる感じでしょうか?
もうちょっと調べたり触ってみないと真相はつかめないかなと。

さいごに

Vue.jsの公式ドキュメントの「はじめに」で紹介されている機能を組んで動かしてみました。
この記事はここまでですが、もう少し触ってみようかなと思います。

以上、
Vue.js軽くおさわりでした。:scream_cat:

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