<!DOCTYPE LNLY>

ノールック寿司グリップ🍣

Ripple (波紋) エフェクトのボタンをVue.jsで作る

ユーザが操作したことを視覚的に伝えるボタンを、Vue.jsを使ってコンポーネントとして作ります。

Ripple (波紋) エフェクト

クリックした箇所を中心に波紋が広がるようにかかるエフェクトです。マテリアルデザインで広く知られるようになりました。 マテリアルデザインガイドラインによれば、クリック箇所を特定せず、どこをクリックしても中心から広がるものは正確には Ripple エフェクトとは呼べないそうです。

コンポーネント化の恩恵

コンポーネント化することにより、複数の箇所で同じボタンを再利用できるわかりやすい利点の他に、ボタンを利用する側のコンポーネント上から、 Ripple エフェクトの実装を隠し、あたかもHTML標準の button 要素 かのように使うことができる利点があります。

Ripple エフェクトの実装は、シンプルな仕組みにしたとしてもそれなりにコード量があります。 クリック位置の特定や、 Ripple を表現するための要素、CSS Transition を含むボタン全体の CSS がそれです。 Vue.jsを使ってコンポーネントを分割することでこれらのコードを隠匿します。

Ripple エフェクトの再現

今回作成したRipple エフェクトは以下の手順で実現しました。

  1. button 要素のクリックした位置を取得・保持する
  2. Ripple を形成する span 要素 を挿入する
  3. 保持していたクリック位置を使って span 要素 の位置を変更する
  4. span 要素scale をゼロに変更する
  5. CSS Transition によって span 要素scaleopacity を変化させる
  6. Ripple を形成する span 要素 を削除する

Tips

今回ボタンを作成する過程で発見した点や気を使った点を記します。

type 属性について

button 要素の type 属性のデフォルト値は type="submit" ですが、経験上 type="button" で指定することがほとんどなので、 コンポーネント内で type="button" として初期値を変更しました。親要素からは特に指定せずとも、 type="button" として振舞います。

pointer-events プロパティについて

あまり使ったことがなかった CSS プロパティ pointer-events を使いました。このプロパティは値として none を指定することにより、その要素が補足するマウスイベントを無視するようになります。1 ボタンを高速で連打したとき、表示されている span 要素 がクリックを補足してしまい、 Ripple エフェクトの出現位置が意図しないところに変わってしまう問題を解消しました。 このプロパティは CSS Level 4 ですが、モダンブラウザは対応しており、 IEバージョン11から対応しています。

disable 属性について

Rippleエフェクト中にボタンを連続的にクリックできるかどうかは要件により異なると思われます。今回は、Ripple エフェクトが出ている間はボタンを非活性にして連打できないようにしました。

click イベントのハンドリングについて

今回作成したボタンはそれ単体では機能を持たず、単純に click イベントを vm.$emit するだけです。 click イベントはボタンを使うコンポーネント側で定義したハンドラを @click で補足して発火します。そのため、ボタンを再利用する箇所がどれだけ増えてもボタン側のコードを変更する必要がありません。

属性の上書きについて

ボタンを使う上で頻繁に制御すると思われる disabled 属性は、標準の button 要素に指定するのと同様に呼び出し側から制御できます。 今回作成したコンポーネントprops を受け取らないようになっていますが、標準で指定可能な属性はそのまま記述すれば上書きされます。 このため、先述した type 属性も、type="submit" として使用したい場合はそのように記述できます。

今から僕は、ビルド環境を作る

使っているライブラリとか、ビルド環境に使っているnpmモジュールが知らぬ間にバージョンアップしたりすることはよくあって、ボイラープレートはすぐに陳腐化するので、たまにはゼロからビルド環境を構築する。読んでいるのが、2018年8月10日よりも後の日付なら、あまり真にうけないでちゃんと調べた方が(新しくなってることあると思うので)いい。

どんな環境にするか

  • ビューはVue.js
  • CSSはSass(SCSS)
  • CSSSFC上にscoped
  • watchはもちろんのこと
  • 開発中はlocalhostでアクセスしたい
  • ホットリロードする
  • eslintをかけたい

そんな感じ。

できたもの

https://github.com/linlymatsumura/boilerplate/tree/master/20180711/myApp

できていくまで

プロジェクト作る

npm init -y

yをつけると対話インターフェースをすっ飛ばす。

webpack導入

webpack-cliも一緒に。

npm i --save-dev webpack webpack-cli

babelとbabel-loader導入

早くbabelかまさなくていいようになるといいな。

npm i --save-dev babel-core bable-loader

webpack.config.jsを書く。配置はプロジェクトルートに。

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
};

entryはどこにソースファイルがあるか。outputはどこに出力するか。rulesで、末尾が.jsのファイルはbabelかけるよ。って書いてある。

eslint導入

npm i --save-dev eslint

.eslintrc.jsが設定ファイル。これもプロジェクトルートに置く。

module.exports = {
    "extends": "standard",
    "parserOptions": {
      "ecmaVersion": 2017,
      "sourceType": "module"
    }
};

extendsはどんなルールでlintするか。https://standardjs.com/を使う。 Standardだけど、標準ってわけじゃないのであんまり気にしない。 コードの様式は、どう揃っているかよりも、揃っていることそれ自体が重要。

これでlintをかけるための、これら。

npm i --save-dev eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard

webpackに組み込むために、eslint-loaderを。

npm i --save-dev eslint-loader

さっきのwebpack configに追加する。

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        enforce: 'pre',
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'eslint-loader'
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
};

基本的なeslintの設定とjsのbuildまでできた。

webpack-dev-server導入

localhostアクセスできるように

npm i --save-dev webpack-dev-server

webpack configに設定を追加。 ./distをルートにしたり、ポートを3000にしたり。

関係ないけどmain.jsbundle.jsに変更したりして、

<!doctype html>
<html>
<head>
  <title>my App</title>
</head>
<body>
  <script src="bundle.js"></script>
</body>
</html>

./dist/index.js作って

window.onload = () => {
  console.log(123)
}

./src/index.js書いたら

  "scripts": {
    "build": "webpack --config webpack.config.js",
    "dev": "webpack-dev-server --config webpack.config.js"
  }

npm scriptをちょっと書いたり。 これで、

npm run build

したり

npm run dev

できるようになった。

vueとvue-loader導入

次はvueを書けるように。

npm i --save-dev vue vue-loader vue-template-compiler

webpack.config編集。

const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new VueLoaderPlugin()
  ],
  module: {
    rules: [
      {
        enforce: 'pre',
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'eslint-loader'
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, './dist'),
    port: 3000
  }
}

rulesとか、pluginとかその辺りを足す。 vue-loaderはv15からpluginとしてwebpackに組み込まないと動かないようになった。

import Vue from 'vue'
import MyApp from 'MyApp.vue'

window.onload = () => {
  new Vue({
    render: h => h(MyApp)
  }).$mount('#myApp')
}

./src/index.js書いて

<template>
  <h1>this is myApp</h1>
</template>

<script>
export default {
  mounted () {
    console.log(123)
  }
}
</script>

./src/MyApp.vue書いて。 ビルドできるようになった。

CSS

Vueコンポーネント上でStyleを書けるようにするvue-style-loadercss自体のcss-loadersass-loaderpostcss-loadernode-sassなどを一気に入れる。

npm i --save-dev vue-style-loader css-loader sass-loader postcss-loader node-sass

postcss-loaderは当初予定していなかったが、autoprefixerをかけるために導入

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

postcss.config.jsを書いて。

これで冒頭に書いた環境が整った。

終わり。イェーイ。