あいつの日誌β

働きながら旅しています。

Meguro.es # 21 @ ビザスク で Vue.draggable について話をしてきた

というわけで行ってまいりました。今回は目黒駅ではなく目黒区という広いくくりでの開催です。

meguroes.connpass.com

スポンサー

株式会社ビザスクさんです。ありがとうございます。前回開催時よりもちゃんとしたビールが置いてあったので私の中ではとても好感度が高いスポンサーです。といっても私は健康診断前日だったのでのめませんでしたけど。

ちなみに8/22にクラフトビールを飲みながらなんか話をする会があるそうです。

visasq.connpass.com

会場が常に足りてない問題

運営の人たちも色々と工夫して「同じ場所での連続開催を避けるように」しているので基本的に開催場所を次回どこにするのか悩ましい問題がいつも発生しているようです。この状況は「自分の会社のスペース貸したら業務終了後にすぐに勉強会に行ける」チャンスです。そこそこの人数を収容できる会社にお勤めの方は是非運営の人に声かけてあげてください。meguro.es に限らず。

LT

というわけで LT です。

ArrayBuffer と binary: @sasurau4

仕事で Bluetooth である機器をつなごうとしたら Binary が現れた。どうする?みたいなお話でした。得体の知れないものを見つけてこれはなんだ?ということでバイナリを調べて触ってたらコンピューターの気持ちになれたっていうお話でした。

speakerdeck.com

私はあんまりバイナリデータに直面する機会がないのですが、実際に局面した人の話が聞けて良かったです。ちなみにあんまりバイナリの事深く調べなくてもやりたいことはできたそうです*1

LitElementとWeb Componentsの比較: @tiwu (Taguchi Wataru)

LitElement の LT。最近 JS 界隈の勉強会で LitElement というワードを聞くのでそろそろ触ってみた方がいいのかもしれない。LitElement の求人増えるのでしょうか?

speakerdeck.com

javascript のメモリと WeakRef: @brn0227

名前の由来は青野さんだから。変数作るのにもコストが掛かっていて、空いているメモリのスペースを裏側でどうやって管理しているのかを紹介。ガベージコレクションは知ってたけどヤング領域とオールド領域の話は知らなかった。難しい話ですけど分かりやすく説明してくれてました。まあそれでもだいぶ分かってないですけど。

speakerdeck.com

ヤング、オールドは簡単に言うと「いつまでも生き残っている変数はもう消される可能性低いから別の場所に移動させておけば空き領域の管理楽じゃね?」みたいな話です。たぶん。

サンプルで学ぶ THE Elm Architecture @y_taka_23 チェシャ猫

どうやら勉強会でいつも猫耳をつける人らしいです。そういえばいつもテンガロンハットかぶっているITジャーナリストさんがいるけどそういう事なのかな*2

speakerdeck.com

Elm に関しては随分前に触った記憶があるのですが、今回のトークで Elm は Web アプリ開発に特化しているがサーバーサイドの実装を行う想定はされてない、という事を知りました。

okamuuu.hatenablog.com

Google I/O ’19から見る新しいJavaScript @toshi-toma

Google I/0 で紹介されていたプロポーザルで気になった内容について紹介。

  • Private filed
  • Numeric Separators
  • Array flat
  • Promise.allSettled
  • Promise.any
  • top-level await

speakerdeck.com

個人的には Promise.allSettledPromise.any は頭の片隅に置いてこうかな、でもたぶん私が実装するとしたら一個でも失敗したらやり直し、みたいな富豪的な事するんだろうな...

top-level await はあったら便利だけどなくてもそんなに困らないから無理にプロポーサルしなくてもいいんじゃないかな、とか思ったりします。

Vue.draggable

私のターン。英語の勉強をしたくてアプリ作りました。英語の勉強はしてないです。そして資料のなかで draggable のつづりを間違えている。英語力はそのうちなんとかします。

ちなみにこの記事の一つ前に Vue.draggable の記事を投稿していますが、これはLTの時間が10分間あったにも関わらず間違えて5分だと勘違いしていたためです*3

Promiseの復習と予習 あらや

話そうとしていた内容が前の登壇者にだいぶ話されてしまったのでちょっとやりづらそうでした。聞いている方は違う角度から話きけるので全く気にならないのですが...まあ話す方はやりづらいですよね。

そして Promise.try のインターフェースがたしかに謎。

speakerdeck.com

スポンサーセッション: 株式会社ビザスク

会社の事業内容を説明。スキルの共有・マッチングサイトを運営していて、かなり好調な事業らしいです。エンジニア募集していてカジュアル面談を行なっているそうです。

懇親会

今回は参加せずに帰りました。

今回の勉強会で得た知見

  • ヤング領域とオールド領域
  • Elm はサーバーサイドには関わらない
  • Promise.allSettled, Promise.any

まとめ

というわけで今回も学びが多い勉強会になりました。このような機会を頂けるのも運営の方々のおかげだと思っておりますので今後もよろしくお願いします。

こちらからは以上です。

*1:それが聞きたかった

*2:認知してもらいやすいというメリットがある

*3:詳しい説明はこちらの記事をご覧ください、で5分に収める予定だった

Vue.draggable を Swappable にしたい

どうした?

英単語やら英会話を覚えたいので WEB 単語帳を作ってみました*1

https://flash-cards-marvelous.firebaseapp.com

その時に単語を並べ換える機能が必要だったのですがそこで Vue.draggable を使ってみました。これは Sorable をベースに Vue component 化したものです。

使ってみたところなかなか便利で、すごい、かんたん、気持ちいい Component だなと思いました。

なんですが DB に保存しようとした時に、一つのアイテムを並べ替えたときに変更されるアイテムがたくさんある事に気づきました。

以下の例のように、先頭のアイテムを一番最後に持っていった場合、以下のように全てのアイテムの位置が変わってしまう。

// sort
Before: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After:  [2, 3, 4, 5, 6, 7, 8, 9, 10, 1]

これだと DB に保存する時にちょっと悩ましい。上記のサービスは Firestore を使っているので配列を丸ごと保存しても良いと思いますが、一つの単語帳にたくさんの単語を登録した場合の事も考えないといけない。

将来的に特定の単語を fav したりする機能をつけようとすると コレクション(もしくはサブコレクション)にして ID を持たせて置いた方が良いのですがそうすると order を各行が持つので上記の例だと並び替えが行われる度に複数のドキュメントの order を update しないといけなかったりします。

どうしたい?

というわけで以下のように Swap したいという気分になりました。

// swap
Before: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After:  [10, 2, 3, 4, 5, 6, 7, 8, 9, 1]

なんですけどその機能は正当な理由により Vue.draggable にはありません。

github.com

どうする?

ソースコードを読んでいたら以下のようにする事によって Swap ぽい挙動にする事に成功しました。

以下が通常の使い方です

<template>
  <table>
    <draggable v-model="items" tag="tbody" @end="handleDragEnd">
      <tr v-for="item in items" :key="item.id">
        <td>{{ item.id }}</td><td>{{ item.name }}</td><td>{{ item.age }}</td>
      </tr>
    </draggable>
  </table>
</template>

<script>
import draggable from "vuedraggable";

export default {
  components: {
    draggable,
  },
  data() {
    const items = [
      { id: 1, name: "Bianka Effertz", age: 37 },
      { id: 2, name: "Mckayla Bogan", age: 20 },
      { id: 3, name: "Estevan Mann", age: 17 },
      { id: 4, name: "Cloyd Ziemann", age: 55 }
    ]
    return { items }
  },
  methods: {
    handleDragEnd() {
      this.$toast.show('dragEnd')
      // update database here
    }
  }
}
</script>

これを以下のように修正しました。

<template>
  <table>
    <draggable v-model="items" tag="tbody" :move="handleMove" @end="handleDragEnd" :options="{animation:500}">
      <tr class="movable" v-for="item in items" :key="item.id">
        <td>{{ item.id }}</td><td>{{ item.name }}</td><td>{{ item.age }}</td>
      </tr>
    </draggable>
  </table>
</template>

<script>
import draggable from "vuedraggable";

export default {
  components: {
    draggable,
  },
  data() {
    const items = [
      { id: 1, name: "Bianka Effertz", age: 37 },
      { id: 2, name: "Mckayla Bogan", age: 20 },
      { id: 3, name: "Estevan Mann", age: 17 },
      { id: 4, name: "Cloyd Ziemann", age: 55 }
    ]
    return { items }
  },

  methods: {
    handleDragEnd() {
      this.$toast.show('dragEnd')

      this.futureItem = this.items[this.futureIndex]
      this.movingItem = this.items[this.movingIndex]
      const _items = Object.assign([], this.items)
      _items[this.futureIndex] = this.movingItem
      _items[this.movingIndex] = this.futureItem

      this.items = _items
    },
    handleMove(e) {
      const { index, futureIndex } = e.draggedContext
      this.movingIndex = index
      this.futureIndex = futureIndex
      return false // disable sort
    }
}
</script>

サンプルはこちら:

fir-vue-draggable.firebaseapp.com

解説

draggable の :move プロパティはそのまま Sortable に渡されます。ここに関数をセットすることができるのですが false を返すと sort の挙動をキャンセルすることができます。

sort の挙動はキャンセルしつつ、ドラッグした状態で drag している item と drop されるであろう場所にある item を保持します。

sort の挙動は抑制されているので this.items は変化しないのですが、@end イベントで this.items を保持していた item を swap させてつじつまを合わせています。

というわけで

無事(?) Vue.draggable を Swap させる挙動に変更できました。とはいえそのそも Soratable が Swap する事を前提にした設計ではないのでべき論でいうと他の Vue component を使ったほうが良いのかもしれません。

というわけで何か良さげなライブラリーがあったらどなたか教えてください。

こちらからは以上です。

*1:肝心の英語は勉強していない

福岡の勉強会「俺の話を聞け!!LT大会 #14」に参加してきた 【宿とご飯編】

ということで今回の博多で宿泊した施設と立ち飲み屋について

宿泊所

f:id:okamuuu:20190523160624j:plain
お洒落な入り口。夜はライトアップされてより一層いい感じでした。

www.thegatehostel.com

私は旅行する時の宿泊所はホステル(簡易宿泊所)にする事が多いです。私は旅行中も大体作業しているので昼間はノマドして、仕事が終わったら立ち飲み屋に行って酔っ払って帰ってくるので清潔なベッドとシャワールームがあればそれでOKだからです。

そんなわけで私が宿泊所を選ぶ基準は以下の2点が重要になってきます。

  • ノマドスペースが充実している
  • 繁華街の近く

今回は5/24(金)の勉強会への参加が主目的だったのですがちょうど3代目 J SOUL BROTHERS のコンサートと日程が重なっていたせいか、宿泊所が割と予約できる場所が少なかったです。

f:id:okamuuu:20190602221317j:plain
セミダブルドミトリー

そんな中なぜかすんなり予約できたホステルに宿泊してきました。場所はちょっと繁華街からは離れた場所だったのですが、勉強会の会場まで歩ける距離だったので booking.com でなんとなく予約しました。そしてセミダブルドミトリー秘密基地風にに泊まることになりました。これは半個室のセミダブルベッドで、2名まで宿泊できるようです。私はそんな事知らずに普通に予約して一泊3000円程度でした。シングルベッドだったらもっと安かったのかもしれませんが、セミダブルだからすんなり予約できたんだと思います。

私以外の宿泊者はだいたい海外の人で、カップルだったりお友達同士で宿泊していたようです。あくまで私の感想ですが正直2名で泊まるとかなり狭いと思います...

f:id:okamuuu:20190524091019j:plain
1Fフロント前のラウンジ

ノマドスペースに関しては1Fと5Fが使えるのですが1Fのほうがお洒落だったので私はこちらを使っていました。立地に関しては少し繁華街から離れた場所なのでバスで天神まで移動したり、帰りはタクシーを使ったりしていました。

こちらの宿泊所に関してですが、私はそこまで不満を感じることはありませんでした。アニメティが有料だったり、トイレの数が少ないと感じたり、ちょっとおしいな、と感じる程度です。海外からの宿泊客や働いている人も若い人が多かったり、ちょっと非日常的な感覚を味わえたので割と満足しています。

とはいえ観光するには立地が少し不便だったのと、他にも気になる宿泊所がたくさんあるのでしばらく利用する機会はないのかも。でも今回のように勉強会で訪れる場合は割と穴場だと思います。

立ち飲み屋とご飯処

今回博多に来た主目的は勉強会への参加だったのですが、博多の立ち飲み屋を巡りたいという理由もありました。そんなわけで以下視察した立ち飲み屋です

  • ちょい立ち酒場 にどね
  • 峠の玄氣屋xMEGUSTA
  • 立ち呑みとうどん みのり
  • STAND BY ME
  • 立呑 okuto

ちょい立ち酒場 にどね

f:id:okamuuu:20190602221958j:plain

博多はドリンクもフードもかなり安いお店が多いです。その中でもかなり安い部類に入ると思いますが食べ物もお酒も美味しかったです。場所が少し天神から離れた場所なので分かりづらいのですがとてもいいお店でした。この日は遅い時間に訪れたのですがスタッフが一人で頑張っていました。たまたま今日は一人で切り盛りしていたそうですが、空いた時間に話しかけてくれたり、注文も調理も滞りなくこなしていたのでやたらと気配りできる人だなあと感心しました。そんなわけで良い印象が残ったのでまた博多に来たら行きたいと思います。

峠の玄氣屋xMEGUSTA

f:id:okamuuu:20190525234017j:plain

昼はおにぎり屋、夜は立ち飲み屋、「峠の玄氣屋」と「MEGUSTA(メグスタ)」がコレボレーションしたお店らしいです。ここで食べた鯖は美味しかったです。博多駅の筑紫口にあるのですが、便利な場所にあるのでおすすめします。

博多の中心地は天神ではあるものの、最近は博多駅周辺がかなり人気が高いらしく、近年マンションはたくさん立っているが博多駅周辺に住もうと思っても物件に空きが無い状態らしいです。そんな訳でたぶんこの近辺にはまだまだ美味しいお店がたくさんありそうです。

立ち呑みとうどん みのり

f:id:okamuuu:20190602222309j:plain

かなり混雑している時間帯に訪問したのですが、お店のスタッフがすごく気さくな人たちでした。常連さん同士も仲が良さそうで、みなさん盛り上がっていらっしゃいました。ちなみに場所は ちょい立ち酒場 にどね の近くです。

うどんは立ち飲み屋でしたけど上品な味でした。だしがかなり美味しかったです。うどん食べた感じだとおそらく他の料理も美味しいじゃないかと思います。他の立ち飲み屋にも行きたかったので長居しなかったのですが、またうどんを食べに行きたいです。

STAND BY ME

f:id:okamuuu:20190602222402j:plain

博多の人気ホステルが運営している BAR です。今まで博多に来たら必ず立ち寄っているお店です。立地がビジネス街にあるので繁華街から少し離れた場所にあるのですが、お洒落な雰囲気の立ち飲み屋が好きなので毎回来てうまかっちゃんを食べています。

STAND BY ME は宿泊してみたいと思いながら、いつも希望する日程に空きが無くて残念。利用したい方は早めの予約をおすすめします。

stand-by-me.jp

立呑 okuto

f:id:okamuuu:20190523213056j:plain

西中洲という中洲ではないスポットがあって、そこにちょっと良いお値段の飲食店がいくつもあるエリアがあるのですが、そこの立ち飲み屋です。私が訪れた時は割と静かで店内は落ち着いた雰囲気でした。寝る前に少しだけ、静かにお酒呑みたい、という人にはちょうどいいかもしれません。

その他のごはん処

  • 元祖博多めんたい重
  • 博多天ぷら たかお
  • 平四郎

f:id:okamuuu:20190602222740j:plain
インスタ映え抜群の明太子

f:id:okamuuu:20190602222922j:plain
午前中は3代目J Soul Brothers 目当てに博多に来たファンの方々がライブ前に多数いらっしゃったので夕方出直しました。

元祖博多めんたい重はインスタ映えする写真が撮れるのでそういうのがお好きな方にはおすすめです。味に関してはたしかにおいしいんですけど一度食べたらもういいかな、ぐらいでした。というのも博多ではめんたいこが無料で食べられる店がいくつかあるので明太子だけ食べたいっていう気分にそこまでならないです。でもこのめんたい重はみんなのリアクションいいので SNS 好きな方はぜひ。ちなみに午前中は長蛇の列でしたが夕方は並ばなくてもすぐに入店できました。まあ日によるのでいつ食べに行くのが空いているのかはわかっていません。

f:id:okamuuu:20190602223331j:plain

博多天ぷら たかおは私がお気に入りの天ぷら屋さんで、揚げたてのてんぷらをお寿司やさんのように小出ししてくれるスタイルのお店です。キャナルシティにあって、今回勉強会の会場を提供してくれた株式会社ベガコーポーレーションの近所にあります。

f:id:okamuuu:20190523164001j:plain

平四郎はいわゆる回転寿司なのですが割と手頃な値段で美味しかったです。今回宿泊所がキャナルシティから徒歩でいける距離だったのでたかおと平四郎はよく利用させていただきました。

まとめ

というわけで博多で飲み歩きを楽しんできました。今回宿泊した施設は飲み屋さんからは遠いのですが博多で勉強会があるときには意外と重宝するかもしれません。博多には他にも泊まってみたいホステルや行ってみたい飲み屋さんがあるのでまた訪れたいと思います。

福岡の勉強会「俺の話を聞け!!LT大会 #14」に参加してきた

f:id:okamuuu:20190529122312j:plain

というわけで福岡に行ってきました。

https://cdg.connpass.com/event/129574/

経緯

福岡の勉強会を探していたところ、mya-ake 氏が主催する勉強会を見つけました。Nuxt.js あたりで彼の書いた記事に助けられた事があったので興味を持ったので参加してきました。

https://mya-ake.com/

勉強会の雰囲気について

私が普段都内で参加している勉強会と基本的には大きな差は感じませんでした。といっても私が参加している勉強会はだいたい酒を飲みながらスタイルばかりなのでもしかしたら参考になってないかもしれません。終始リラックスした雰囲気で途中で話してもOKみたいな雰囲気だったので、たぶん「俺にも何か喋らせてくれてれる勉強会が好きだ」という人には向いている勉強会だと思います。

ちなみに発表内容はどれもかなり良かったので一度福岡に行ってみると良いと思います。ごはんも美味しいですし。

会場の提供

株式会社ベガコーポレーション様が提供してくれました。ありがとうございます。この勉強会にも多数同社のエンジニアが参加してました。家具のEコマースで Vue.js を使っているそうです。

LT順番について

司会者が「次のLTやりたい人」と言ったあと最初に手を上げた人から順番にLTするシステムでした。途中何回か手をあげたのですがいつも出遅れてしまいだんだん面倒になってきたので結局順番は最後になりました。

ちなみにこの方式、初参戦の私にとって「今話している人誰?」状態になってしまいました。というわけで登壇者とタイトルが間違っている可能性が高いです。というかいくつか間違っているのと話の内容を思い出せない人が何人かいる...

LT

OSSのあれこれ話すイベントやりたいと思った チャンカツ

ベガコーポレーションのフロントエンド。イベントに関わっていると色々な知見があるので共有したい、という話だった気がします。

https://speakerdeck.com/tyankatsu/ossfalsearekorehua-suibentoyaritaitosi-tuta

gatsby + netlify でサイトを作った話

ベガコーポレーションの人。ゆるきゃんの話が好きだそうです。Gatsby.js と Netlify は便利ですよね。私も最近まで使っていました。

福岡でガチエンジニアがブートキャンプをはじめる話

GMOペパポの人。スプラトゥーンが好きだそうです。大名エンジニアカレッジを始めたそうです。

https://speakerdeck.com/udzura/bootcamp-by-engineer-for-engineer https://daimyo-college.pepabo.com

Fukuoka Growth Nextの話 or postalk.ioの話 新5 Fukuoka Growth Next

最近福岡に帰ってきたそうです。カード型チャットサービスの postalk.io で複雑な話し合いをわかりやすくしたい。あと Fukuoka Growth Next で 6/1 にイベントやるそうです。

https://growth-next.com/ja/event/14669

海の住所の話

umeebe の人。地球の7割は海なんだけどその海には住所が割り当てられてないんだけどそれが我々の作っているサービスにおいて問題がおきるので解決を図ったお話。

GANった話 10 Kenta K. Tanaka

ベガコーポレーションのエンジニアさん。姿を消すマント = 物体検出 + GAN(ギャン) というお話です。多分デモ画面見ないと伝わらないと思いますがコマ送りで写っている自分の姿をけすことに成功(?)していました。まあ完全に消えていないので「ここになんかいるよね」というのはバレるんでしょうけど。

AbemaTVのコメビュを作る話

AbemaTV のソースコードを読むと色々仕掛けがあって楽しいそうです。そして福岡新着ITイベント のボットを作ったそうです。

https://speakerdeck.com/loftkun/abematvfalsekomentobiyuawozuo-ruhua

セキュリティ対策してますか?

とある会社の情報システムで働いているそうです。セキュリティで起きる問題、原因は主に外部よりも内部。人はミスする、悪いことを考える、ゆえに100%の対策は無理。テクニカルじゃないほうのセキュリティ対策しましょう。とのことでした。

イラスト普及

就活生がエンジニア向けにイラストの書き方についてふんわりと説明。最初はペンタブで大丈夫 。ウォームアップすると線がきれいにかけるそうです。30秒ドローイングで練習しましょう。 水晶は塗りの基礎。だそうです。

動画でドローイングの様子を再生したのを見せてもらいましたが思った以上に手間かかってるのでイラストレーターってすごいんだな、と思いました。

けんてぃが考えるWebサイトの品質

品質の定義は色々あるけど基礎価値と付加価値を考えた場合、基礎価値はコンテンツとアクセシビリティでは?建物の価値観に通ずるものがあるのではないかとのこと。そして転職活動中だそうです。なぜかLT終わったあとベガコーポレーションのエンジニア達からプチ面接が行われました。

showtime のご紹介 TakeruNarita

思い立って一週間でスライド共有サービスを作った話。実は今回東京から私以外のエンジニアが来ていました。お話を聞いたところ「どうしても話をしたかったが直近のイベントで枠がなかったから新幹線で来た」そうです。

インフラエンジニアがジョブチェンジした話

東京から福岡へ。スタートアップはスピードと結果が大事。話を聞いている限りジョブチェンジというよりもフルスタック化している気がしました。

英単語を覚えたいので単語帳をつくりました。サイト名がまだ決まっていません。

https://flash-cards-marvelous.firebaseapp.com/

というわけで

初めて地方の勉強会に参加してきました。都内の勉強会と全く遜色のないイベントで発表内容もとても面白かったです。福岡でも東京と同じ様に頻繁に勉強会が行われているので時々地方に出向いて勉強会に参加すると楽しいと思います。

というわけで博多はご飯もお酒も美味しかったのでまた勉強をしに行きたいと思います。

Nuxt.js@2.6.3 と core-js@3 で Can't resolve 'core-js/modules/es7.promise.finally' というエラーが表示される場合の対処

core-js@2 を使うと良いでしょう

Vue.js で Carousel な component を再開発して色々な学びを得たお話

f:id:okamuuu:20190504160726j:plain

あらすじ

Vue.js でシンプルな Carousel を作ります。すでにそういった component は一杯あるのですが、車輪の再開発を通じてお勉強しました、という趣旨の記事です。すでに vue-carousel という定番モジュールがあるのでこれを真似て作成しています。

Just Do it

以下のようにファイルを作成します。

mkdir vue-tiny-carousel && cd $_
mkdir src example
touch src/{index.js,Carousel.js,Slide.vue, Navigation.vue}
touch example/App.vue
yarn init -y
yarn add lodash.debounce
yarn add -D @vue/cli

touch したファイルを以下のように修正します。

src/index.js

import Carousel from "./Carousel.vue";
import Slide from "./Slide.vue";

const install = Vue => {
  Vue.component("carousel", Carousel);
  Vue.component("slide", Slide);
};

export default {
  install
};

export { Carousel, Slide };

src/Carousel.Vue

<template>
  <div class="carousel">
    <div ref="carousel-wrapper" class="carousel-wrapper">
      <div
        ref="carousel-inner"
        class="carousel-inner"
        :style="{
          'transform': `translate(${offset}px, 0)`,
          'transition': dragging ? 'none' : `0.4s ease transform`,
          'flex-basis': `${carouselWidth}px`,
        }"
      >
        <slot></slot>
      </div>
    </div>

    <navigation
      :clickTargetSize=8
      :nextLabel="`>`"
      :prevLabel="`<`"
      @navigationclick="handleNavigation"
    />

  </div>
</template>

<script>
/* eslint-disable no-console  */
import debounce from "lodash.debounce"
import Navigation from "./Navigation.vue";
export default {
  name: "carousel",
  components: {
    Navigation
  },
  mounted() {
    window.addEventListener(
      "resize",
      debounce(this.onResize, this.refreshRate)
    );
    this.setSlideCount()
    this.computeCarouselWidth();
  },
  beforeUpdate() {
    // this.computeCarouselWidth();
  },
  provide() {
    return {
      carousel: this
    };
  },
  data() {
    return {
      slideCount: 0,
      currentPage: 1,
      carouselWidth: 0,
      dragging: false,
      refreshRate: 16,
    };
  },
  computed: {
    offset() {
      return this.carouselWidth * (this.currentPage - 1) * -1
    },
    canAdvanceForward() {
      return this.slideCount - this.currentPage > 0
    },
    canAdvanceBackward() {
      return this.currentPage > 1;
    }
  },
  methods: {
    setSlideCount() {
      const slots = this.$slots
      const tagName = "slide"
      const isSlide = slot => slot.tag && slot.tag.match(`^vue-component-\\d+-${tagName}$`) !== null
      this.slideCount = (slots && slots.default && slots.default.filter(isSlide)).length || 0
    },
    computeCarouselWidth() {
      let carouselInnerElements = this.$el.getElementsByClassName("carousel-inner");
      this.carouselWidth = carouselInnerElements[0].clientWidth || 0;
    },
    handleNavigation(direction) {
      if (direction && direction === "backward") {
        this.goPreviousPage()
      } else if (direction && direction === "forward") {
        this.goNextPage()
      }
    },
    onResize() {
      this.computeCarouselWidth();
      this.dragging = true; // force a dragging to disable animation

      // clear dragging after refresh rate
      const refreshRate = 16
      setTimeout(() => {
        this.dragging = false;
      }, refreshRate);
    },
    goToPage(page) {
      this.currentPage = page
    },
    getNextPage() {
      return this.currentPage < this.slideCount ? this.currentPage + 1 : this.currentPage;
    },
    getPreviousPage() {
      return this.currentPage > 1 ? this.currentPage - 1 : this.currentPage
    },
    goNextPage() {
      this.goToPage(this.getNextPage())
    },
    goPreviousPage() {
      this.goToPage(this.getPreviousPage())
    }
  }
}
</script>

<style scoped>
.carousel {
  height: 100%;
  display: flex;
  flex-direction: column;
  position: relative;
}
.carousel-wrapper {
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
}
.carousel-inner {
  height: 100%;
  display: flex;
  flex-direction: row;
  backface-visibility: hidden;
}
</style>

src/Slide.Vue

<template>
    <div class="slide">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "slide",
}
</script>

<style scoped>
.slide {
  flex-basis: inherit;
  flex-grow: 0;
  flex-shrink: 0;
  user-select: none;
  backface-visibility: hidden;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  outline: none;
}
</style>

src/Navigation.Vue

<template>
  <div class="carousel-navigation">
    <button
      type="button"
      aria-label="Previous page"
      class="carousel-navigation-button carousel-navigation-prev"
      v-on:click.prevent="triggerPageAdvance('backward')"
      v-bind:class="{ 'carousel-navigation--disabled': !canAdvanceBackward }"
      v-bind:style="`padding: ${clickTargetSize}px; margin-right: -${clickTargetSize}px;`"
      v-html="prevLabel"></button>
    <button
      type="button"
      aria-label="Next page"
      class="carousel-navigation-button carousel-navigation-next"
      v-on:click.prevent="triggerPageAdvance('forward')"
      v-bind:class="{ 'carousel-navigation--disabled': !canAdvanceForward }"
      v-bind:style="`padding: ${clickTargetSize}px; margin-left: -${clickTargetSize}px;`"
      v-html="nextLabel"></button>
  </div>
</template>

<script>
/* eslint-disable no-console  */
export default {
  name: "navigation",
  inject: ["carousel"],
  props: {
    clickTargetSize: {
      type: Number,
      default: 8
    },
    nextLabel: {
      type: String,
      default: "&#9654"
    },
    prevLabel: {
      type: String,
      default: "&#9664"
    }
  },
  computed: {
    canAdvanceForward() {
      return this.carousel.canAdvanceForward || false;
    },
    canAdvanceBackward() {
      return this.carousel.canAdvanceBackward || false;
    }
  },
  methods: {
    triggerPageAdvance(direction) {
      this.$emit("navigationclick", direction);
    }
  }
};
</script>

<style scoped>
.carousel-navigation-button {
  position: absolute;
  top: 50%;
  box-sizing: border-box;
  color: #000;
  text-decoration: none;
  appearance: none;
  border: none;
  background-color: transparent;
  padding: 0;
  cursor: pointer;
  outline: none;
}
.carousel-navigation-button:focus {
  outline: 1px solid lightblue;
}
.carousel-navigation-next {
  right: 45px ;
  font-size: 20px;
  transform: translateY(-50%) translateX(100%);
  font-family: "system";
}
.carousel-navigation-prev {
  left: 45px;
  font-size: 20px;
  transform: translateY(-50%) translateX(-100%);
  font-family: "system";
}
.carousel-navigation--disabled {
  opacity: 0.2;
  cursor: default;
}
/* Define the "system" font family */
@font-face {
  font-family: system;
  font-style: normal;
  font-weight: 300;
  src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"),
    local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Symbol"),
    local("Roboto-Light"), local("DroidSans"), local("Tahoma");
}
</style>

example/App.vue

<template>
  <div style="background: #eee;">
    <Carousel style="width:100%; height: 80vh;">
      <Slide>
        <div class="sample bg-primary">
          <h1>slide 1</h1>
          <p>test test test</p>
        </div>
      </Slide>
      <Slide>
        <div class="sample bg-warning">
          <h1>slide 2</h1>
          <p>test test test</p>
        </div>
      </Slide>
      <Slide>
        <div class="sample bg-danger">
          <h1>slide 3</h1>
          <p>test test test</p>
        </div>
      </Slide>
    </Carousel>
  </div>
</template>

<script>
import { Carousel, Slide } from '../src/index';
export default {
  components: {
    Carousel,
    Slide
  }
}
</script>

<style>
.sample {
  box-sizing: border-box;
  margin: 0;
  padding: 30px 60px;
  height: 100%;
}
.bg-primary { background: hsl(171, 100%, 41%) }
.bg-warning { background: hsl( 48, 100%, 67%) }
.bg-danger  { background: hsl(348, 100%, 61%) }
h1 {
  margin: 0;
  padding: 0;
}
</style>

動作確認

yarn vue serve example/App.vue で動作確認をします。

解説

この Carousel では以下の事を行なっています。おそらく世の中に出回っている Carousel と同じ挙動だと思います。

  • 画面に Carousel を配備する
  • Carousel の内部に Slide を横一列に並べる
  • Slide ひとつの横幅は親要素である Carousel と同じサイズにする
  • 横一列に並んだ Slide は Carousel に対して Slide の数だけ大きくはみ出す。
  • はみ出した部分は非表示にする
  • 次のスライドに遷移させた場合はスライドを一つ分左にずらす

ただし、この vue-tiny-carousel はあくまで初学者向けの教材として利用することを目的としているので細かい調整を行えるように設計していません*1。あらかじめご了承ください。

setSlideCount()

Carousel の中に Slide を複数配備して、その Slide の数をカウントします。この数字を元に offset の数などを計算します。Slide の tagName が vue-component-2-slide という形式なので、それにマッチしたものを Slide だとカウントします。

setSlideCount() {
  const slots = this.$slots
  const tagName = "slide"
  const isSlide = slot => slot.tag && slot.tag.match(`^vue-component-\\d+-${tagName}$`) !== null
  this.slideCount = (slots && slots.default && slots.default.filter(isSlide)).length || 0
}

computeCarouselWidth

ルーセルの表示領域を計算します。window がリサイズされた時にも再計算が必要なので関数化しています。

computeCarouselWidth() {
  let carouselInnerElements = this.$el.getElementsByClassName("carousel-inner");
  this.carouselWidth = carouselInnerElements[0].clientWidth || 0;
}

lodash.debounce

リサイズのイベントはこまめに発生するので lodash.debounce を使って関数の実行を抑制します。以下の記事に詳しい解説が載っていたのでこちらも参考にしてみてください。

https://qiita.com/waterada/items/986660d31bc107dbd91c

provide と inject

parent component である Carousel を、child component である Navigation で使いたい、というケースで便利な機構です。こちらも以下の記事に詳しい解説が載っていたのでこちらも参考にしてみてください。

https://qiita.com/kaorun343/items/397b1fa6afe96fa2b30f

まとめ

というわけで世の中には Carousel のライブラリが多数あるにも関わらず、車輪の再開発を行ってみました。再開発といっても同等の機能を求めた訳でなく、あくまで学習目的なので、厳密には車輪の再開発ではないと思いますが。

私はすでに Vue.js や Nuxt.js でやりたい事ができるような状態ではあったのですが、それでもまだ知らなかった便利な機構が存在したので学べた事があってとても有意義な時間となりました。というわけで車輪の一部を再開発すると他の優れたエンジニアの叡智を学ぶ事ができてなかなか良い経験でした。

今回作成したソースコードは以下に置いてあります。実用的かどうかはさておき、学習する分には適度な量のソースコードだと思いますのでよかったらご覧になってください。

https://github.com/okamuuu/vue-tiny-carousel/

*1:アラビア語のような Right to left に未対応だったり細かいデザインの調整をする props を用意していません

Vue.js を書くために最小構成でセットアップする方法

f:id:okamuuu:20190429135426j:plain

あらすじ

vue cli を使う方法とスクラッチで書く方法を紹介します。基本的には vue cli を使う方が便利なのですが、初学者の方で vue の文法についてだれかに質問したい場合は後者の方法を一応覚えておくと良いと思います。

vue cli を使う方法

vue-cli を install します。以下の例ではプロジェクト単位で vue-cli を追加しています。

mkdir practice-vue-minimum && cd $_
touch  App.vue
yarn init -y
yarn add -D @vue/cli

この記事を書いている時点の vue-cli の version は 3.7.0 です。ちなみに vue.js の version は 2.6.10 です。

node_modules/.bin/vue --version
3.7.0

App.vue を以下のように編集します。

<template>
  <h1>Hello!</h1>
</template>

以下のコマンドで上記で作成した component をエントリーポイントにして起動する事ができます。

yarn vue serve App.vue

以下のようにメッセージが表示されれば OK

DONE  Compiled successfully in

 App running at:
 - Local:   http://localhost:8080/
 - Network: http://172.16.2.86:8080/

 Note that the development build is not optimized.
 To create a production build, run yarn build.

npm を日常的に使っている方であればこの方法が一番簡単だと思います。vue-cliwebpack の設定などを代行してくれますので Vue.js の学習に集中する事ができます。

vue cli を使わない方法

誰かに質問をする時に、例えば codepen.io や jsfiddle.net で公開してから質問をしたい時があると思います。jsfiddle だと以下のような URL で雛形を利用する事ができます。

https://jsfiddle.net/boilerplate/vue

この形式で vue を始めたい人は以下のような手順を踏むと良いでしょう。

mkdir practice-vue-scratch && cd $_
touch touch index.html style.css
yarn init -y
yarn add -D http-server

index.html と sytle.css を以下のように修正します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Vue.js App</title>
  <link href="/style.css" rel="stylesheet">
</head>
<body>
<div id="app">
  <h2>Todos:</h2>
  <ol>
    <li v-for="todo in todos">
      <label>
        <input type="checkbox"
          v-on:change="toggle(todo)"
          v-bind:checked="todo.done">

        <del v-if="todo.done">
          {{ todo.text }}
        </del>
        <span v-else>
          {{ todo.text }}
        </span>
      </label>
    </li>
  </ol>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
<script>
new Vue({
  el: "#app",
  data: {
    todos: [
      { text: "Learn JavaScript", done: false },
      { text: "Learn Vue", done: false },
      { text: "Play around in JSFiddle", done: true },
      { text: "Build something awesome", done: true }
    ]
  },
  methods: {
    toggle: function(todo){
      todo.done = !todo.done
    }
  }
})
</script>
</body>
</html>

style.css

body {
  background: #20262E;
  padding: 20px;
  font-family: Helvetica;
}

#app {
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;
}

li {
  margin: 8px 0;
}

h2 {
  font-weight: bold;
  margin-bottom: 15px;
}

del {
  color: rgba(0, 0, 0, 0.3);
}

以下のコマンドで localhost で動作確認ができます。

yarn http-server .

まとめ

Vue.js の書き方について学習する際に学習する環境を簡単にセットアップする方法を紹介しました。もっと良い方法がある気もしますが、やりかたは一つだけではないのでこちらの記事が参考になる場面があれば思い出して頂ければ幸いです。