20200124のJavaScriptに関する記事は24件です。

Svelte ハンズオン

はじめに

社内でSvelteのハンズオンを開催して好評だったので書き残します!
まず、資料読んでね!

ハンズオン資料

画像をクリックするとslideshareに飛びます。

thumbnail

ハンズオン環境

GitHubで管理したい場合はForkしてね!

https://github.com/oekazuma/svelte-hands-on

git clone https://github.com/oekazuma/svelte-hands-on.git

おわりに

Svelteの勉強資料はまだ少ないと思うので参考にしていただけたらと思います。

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

CLI のテンプレートプロジェクト by node and TypeScript

node で CLI(Command Line Interface) を開発する機会が数回あって、せっかくなのでテンプレートプロジェクトとしてまとめてみた。

テンプレートプロジェクト

必要なモノ

  • nodejs: v11.13.0+
  • typescript: v3.7.3+

試し方

  1. 上記のリポジトリを Clone する
  2. リポジトリのディレクトリに cd して npm ci する
  3. npm run build する
  4. npm link する
  5. source ~/.bash_profile を行うかまたはターミナルを再起動する

これでどのディレクトリでも my-great コマンドが使用できるようになる。

$ my-great hello -f Echizen -s Ooka -a 42

Hello Echizen Ooka.
You're 42 years old.
$ my-great something wrong param

Command Line Interface for My great service

  Sample for CLI.

Commands

  my-great hello -f <first_name> -s <second_name>   Say Hello.
  my-great version                                  Show version.

アンインストール

npm uninstall -g @amay/my-great-cli

要点

コマンドライン引数の解析と使用方法の表示

yargs とかいろいろあったけど、自分的に使いやすかったのでこれを選択。

プログラム構成

my-great hello -f <first_name> -s <second_name>   Say Hello.
my-great version                                  Show version.

のように、第一引数を「コマンド」とし、第2引数以降をそのコマンド専用の引数群としたかったので、index.ts で第一引数のみを parse して取得し、コマンド毎に command-xxxx.ts へ委譲している。

command-line-args では commandLineArgs(this.paramDef, { partial: true })partial:true を設定すると、引数定義(paramDef) に存在しない引数があっても無視する。

cli コマンド名

cliコマンド名 my-greatpackage.jsonbin: で指定している。

package.json

{
  "name": "@amay/my-great-cli",
<省略>
  "bin": {
    "my-great": "build/index.js"
  },
<省略>

ビルドされた ./build/index.js を指すように設定している。
ちなみに npm run 経由で node を実行する場合は、引数の前に -- を付ける(例: node ./build/index.js -- version)。

必須引数のチェック

command-line-args では 引数の必須チェックを自力で行わなければならない ようなので、定義体の paramDefrequire: boolean を追加し、パースした実際の引数である XxxxConfigrequire = true な項目が含まれているかをチェックするようにした。

// Valid require params
const requiresNotSetted = this.paramDef
  .filter(x => x.require)
  .filter(x => cfg[x.name] == null)
  .map(x => `--${x.name}`);

if (requiresNotSetted.length > 0) {
  console.log(`Param: ${requiresNotSetted.join(' ')} is required.`);
  console.log(`------------------------------------`);
  this.usage[1].optionList = this.paramDef;
  const usg = commandLineUsage(this.usage)
  console.log(usg);  
  return -1;
}

kebab-case VS camelCase VS snake_case

コマンドの引数は kebab-case がデファクトスタンダードの模様。
command-line-args では commandLineArgs(this.paramDef, { camelCase: true }) とすると、--first-name に渡された引数を、firstName 変数に格納してくれる。

が、前述の必須引数のチェックが(定義体と実体の変数名が異なるため)正しく機能しなくなるので妥協案として snake_case の --first_name を採用している。

コマンドを追加するには

  1. index.tsCommandTypexxxx を増やす
  2. command-xxx.ts(CommandXxxx クラス) を作る
  3. index.tscommandMap に追加する
  4. mainUsage になんか書く

参考

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

Rails Javascript/Html: 投稿編集画面で写真の表示を確認したい!

= f.file_field :image, id: :message_img, placeholder: "写真の貼り付け:" 以下にJavaScriptを配置

  :javascript 
    $(function() {
     function readURL(input) {
      if (input.files && input.files[0]) {
        var reader = new FileReader();
        reader.onload = function (e) {
          $('#img_prev').attr('src', e.target.result);
        }
        reader.readAsDataURL(input.files[0]);
      }
     }
     $("#message_img").change(function(){
       readURL(this);
     });
    });

・img_prevの属性操作
・changeメソッドでmessage_imgの読み込みURLを変更する

.field__img-show

  = image_tag @message.image.url, id: :img_prev,size: '800x600' if @message.image?

最後にimage_tagで変更されたimg_prevを表示。
つぎはぎの知識でなんとか起動させることができました^^;

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

初心者によるプログラミング学習ログ 219日目

100日チャレンジの219日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

219日目は

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

ifの入れ子をできるだけ少なくするための工夫

JSで処理を書いているとif文の中にifを書くこともあると思います。
ただ、if文を入れ子にすると可読性が下がり、読みにくい・わかりにくいコードになっていくと思います。今回入れ子が発生し、それを解消するために行った工夫を記載します。

状況

かなりカンタンな例を作成しました。

1.名前と数値を設定
2.名前がメンバーと名付けた変数たちに含まれるか確認
3.含まれる場合は数値が0か+(プラス) か -(マイナス) かで条件分岐

コードで書くとこんな感じ

test.js
const name = 'yamada';
const num = 100;

//メンバー
const member1 = 'sato';
const member2 = 'yamada';
const menber3 = 'suzuki';

//設定した名前がメンバー変数にあるか確認
if(name === member1, name === member2, name === member3){
    //設定数値が 0 か +(プラス) か -(マイナス) か
    if(num === 0){
        //0の場合
    } else if (num > 0) {
        //+(プラス)の場合
    } else {
        //-(マイナス)の場合
    }
}

ifが入れ子になってます。
シンプルな内容なので入れ子でもまだわかるかとおもいますが、どの条件でどの処理をするのかがパッと見わかりにくいです。

解消後

ifが入れ子になっているときは、複数ある条件式をなんとか1つの条件式で記載できないか?と考えるのが有効だと教わりました!

そして解消後のコードがこちら

test.js
const name = 'yamada';
const num = 100;

//メンバー
const member1 = 'sato';
const member2 = 'yamada';
const menber3 = 'suzuki';

//メンバーを配列化
const list = [member1, member2, member3];

if(name.includes(list) && num === 0) {
    //設定した名前がメンバー変数にある かつ 設定数値が0の場合
} else if (name.includes(list) && num > 0) {
    //設定した名前がメンバー変数にある かつ 設定数値が+(プラス)の場合
} else if (name.includes(list) && num < 0) {
    //設定した名前がメンバー変数にある かつ 設定数値が-(マイナス)の場合
} else {
    //設定した名前がメンバー変数にない
}

こちらにすることでどの条件で分岐したものがなんの処理を行っているのかわかりやすくなったと思います!

ポイントとして、確認したい変数たちを配列変数に入れて条件式に .includes を使用することでまとめました!

まとめ

少しずつコードが書けるようになってくると、やりたいことは実現できるが、余計な動作も記載してしまっている、可読性が悪い、コードが長すぎて時間がかかる、などまだまだプログラミングは奥が深く、課題は山積みだと実感します。。!!
少しずつ綺麗で可読性が高くわかりやすいコードが書けるように勉強していきたいとおもいます。

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

Vue.jsプロジェクトにESLintを設定する最もシンプルな方法

ESLintをシンプルに設定する

ESLintの設定ってややこしいですよね

Vue.jsとJavaScriptの開発環境で
最低限のものをシンプルに設定する方法を紹介します

今回使用するJS Concept: 

 - Standard JS

今回使用するpackages:

 - ESlint
 - husky
 - lint-staged

1.package.jsonに以下を追記します。

˜/package.json
{
  "scripts": {
    "lint": "eslint src"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,vue}": [ 
      "eslint --fix",
      "git add"
    ]
  },
  "devDependencies": {
    "babel-eslint": "^10.0.3",
    "eslint": "^6.5.1",
    "eslint-config-standard": "^14.1.0",
    "eslint-plugin-import": "^2.18.2",
    "eslint-plugin-node": "^10.0.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-standard": "^4.0.1",
    "eslint-plugin-vue": "^5.2.3",
    "vue-eslint-parser": "^6.0.4"
  }
}

2.eslintの設定ファイルを作成します

˜/.eslintrc.js
module.exports = {
    root: true,
    parser: 'vue-eslint-parser',
    parserOptions: {
        ecmaVersion: 2018,
        parser: 'babel-eslint',
        sourceType: 'module'
    },
    extends: ['standard'],
    plugins: [
        'vue'
    ],
    rules: {
        'vue/html-indent': ['error', 2] // 2 spaces for html indent
    }
}


3. npm install を実行します。(yarn userは yarn )

command-line
$ npm install

4. Editorでの設定を追加します。

JetBrains系の場合

Preference -> Search: ESlintで Manual ESLint configuration を選択
ESlint package と Configuration fileを以下の様に設定します。
image.png

VSCodeの場合

ESLint用のextentionがあるので追加します。
https://github.com/microsoft/vscode-eslint

設定は以上です。

eslintを実行します。

command-line
$ eslint --fix FILE_PATH

以上です。
シンプルな設定から始めてカスタマイズしていくと段々理解が深まって面白いです!

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

イベントのバブリングとキャプチャリング

バブリング

要素上でイベントが発生すると、最初にその要素でイベントハンドラが実行され、その親、その親と、documentオブジェクトまでイベントハンドラが実行される。

ほぼ全てのイベントでバブリングが発生する。focusイベントはバブリングしない。

event.target⋯イベントの発生元の要素。

event.currentTarget⋯そのイベントハンドラが紐づけられている要素。

バブリングを止める

htmlタグ、documentオブジェクト、イベントによってはwindowにも伝わる。

event.stopImmediatePropagation()⋯親にバブリングするのを防ぐ。
要素が、1つのイベントに対して複数のイベントハンドラを持っている場合に、他のイベントハンドラを実行しないようにするためにはevent.stopImmediatePropagation()を実行する。

基本的にはバブリングを止めてはいけない。

キャプチャリング

キャプチャリングフェーズはほとんど使われない。
キャプチャリングフェーズでイベントをキャッチするにはaddEventListenerの第3引数をtrueにする。

event.eventPhase⋯イベントが補足されたフェーズの番号のプロパティ

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

CodePen - HTML+JavaScriptで色サンプル(RGBカラーのパレット)作ってみた

クリックした座標のカラーコードを#RRGGBB形式で表示します。

See the Pen ColorTest by kob58im (@kob58im) on CodePen.

step 51/255選んだときに右端クリックしてもFFにならないのでバグってるっぽい・・というかstepの割り振りの設計ミスってます。
⇒直せたはず。

参考サイト

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

Facebookのシェア用URLでテキストを入れる方法

始めに

URLにアクセスするだけでシェア画面を表示することができるのですが、色々調べてもFacebookではテキストを入れる方法が見つかりませんでした。

Facebookのシェアリンク
https://www.facebook.com/sharer/sharer.php?u=<URL>

ただ色々試していたら、引用テキストとして表示する方法が見つかりましたので記事に残しました。

テキストを入れる方法

結論から言うと以下のようにquoteパラメータが引用文の役割を果たしているようです。

引用文を含めたシェアリンク
https://www.facebook.com/sharer.php?quote=<引用文>&u=<URL>
サンプル
https://www.facebook.com/sharer.php?quote=引用文&u=http://127.0.0.1

サンプルのURLを開くと以下の画像のようになります(ユーザー名や入力テキスト部分は除外しています)。

スクリーンショット 2020-01-24 15.53.17.png

注意点

このquoteプロパティはFacebook SDKの仕様を参考にしました。
ただこのプロパティの説明で以下のようなことが書かれており、もしかしたら使えない仕様かもしれない音でご注意くださいm(_ _)m
(FacebookのURLをシェアする時には使えませんと言う意味なら問題ないですが)

リンクでシェアする引用。記事のプルクオートと同様に、利用者によりハイライトされるか、開発者により事前定義されています。
このパラメーターはFacebook URLには利用できません。

終わりに

Facebookのシェア用URLでテキストを入れる方法について紹介しました。
ただ公式のドキュメントから見つけたものではないので、ご注意ください。

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

IE11でbackground-imageのJQueryが反映されない...!!

問題となったコード。

html
<div id="image_area"></div>
JQuery
$("#image_area").css("background-image","url(画像パス)");

このコードでは、Chrome、FireFoxでは動作するが、IE11では動かず。
いろいろ原因を探って、たどり着いた答えがコレ。

修正後
$("#image_area").css({
    "background-image": 'url("画像パス")',
});

IE11でbackground-imageプロパティを指定するときは

1. { "background-image": 値 } の形式で書く
2. 画像パスはダブルクオーテーションで囲む

必要があるようです。なんでこうしないといけないのかはわからないですが...
はやくIE絶滅しないかな♪

参考にしたところ
jQueryで背景画像を変える時にIEでの対処法【JS】
背景画像をJavaScriptで書き換える

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

フッターのコピーライトとかで自動的に今の年を表示する方法

フッター等によく書いてあるコピーライト。
Copyright © hoge.exmaple.com 2010-2020 All Rights Reserved.
こういうやつです。

この2020という年数を年が変わるたびに手動で変更するのは面倒ですよね。今回はJavaScriptで自動で今の年を表示する方法を紹介します。ものすごい小ネタです。

フッターのコピーライトとかで自動的に今の年を表示する方法

<footer>
  <p class="copyright">Copyright © <span class="thisYear"></span> hoge.example.com All Rights Reserved.</p>
</footer>

JavaScriptで今の年を表示しているだけです。

  const yearElm =  document.querySelector('.thisYear'); // <span class="thisYear"></span>を取得
  const thisYear = new Date().getFullYear(); // 今の年を取得
  yearElm.innerHTML = thisYear; // <span class="thisYear"></span>の中に年を表示

コピーライトの書き方

ちなみに、最初に例示したコピーライトは間違ってはいませんがいろいろ不要なものが多いです。というかそもそもコピーライトの表記自体なくても問題ないのです。でも慣習的に書いておいた方が無難。

正しくは
© 2010 hoge.exmaple.com ←年数はそのサイトが開設された年(著作物が発行された年)
で十分。今の年を表示させる必要は実はないんです。

参考
年号と©マークと何が必要?Copyright(コピーライト)表記の正しい書き方
Wikipedia: 著作権表示

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

JavaScriptでgetUserMediaでマイク・カメラ(ビデオ)を使うときの注意点とデバイスへのアクセス権限について

マイクのみ使用するのにaudioのみの指定では動かないブラウザがある

とあるサンプルがEdgeで動作したがChromeで動作しなかったので調べてみた。

Chromeはaudioだけでなくvideoも指定しないと音声入力が取得ができない。
https://www.html5rocks.com/ja/tutorials/getusermedia/intro/ より引用:

注: Chrome にはバグがあり、「audio」のみを渡しても無効です(crbug.com/112367)。Opera でも を動作させることはできませんでした。

自分でも確認しましたが、
 Chromeのバージョン: 79.0.3945.130(Official Build) (64 ビット)
で再現しました。audio指定のみでは動作せず、videoaudioと一緒に指定すると動作する。

navigator.mediaDevices.getUserMediaが推奨らしい

navigator.getUserMediaではなくnavigator.mediaDevices.getUserMediaが推奨らしい

https://qiita.com/Futo23/items/bff1ce1d2e1b219b243d

アクセス権限について

ブラウザが管理しているようである。
サイト(ドメイン)単位で管理されるっぽい?
意識していないと結構危険かもしれない。

例:CodePenのある1作品で許可してたら、ほかの作品からアタックされる恐れがある。
(マイクやカメラを許可済みにすると、許可したときのページのみではなく、ドメイン上の任意のページからのアクセスを許可したことになってしまうので、知らない間にCodePenの作品を埋め込んだページを含んで情報が吸い上げられることも考えられる。)1

アクセス権を削除するには - Microsoft Edge

image.png

image.png

image.png
アクセス権を設定したドメインの名前が一覧表示されるので、変更したいものを選んで処置してください。

アクセス権を削除するには - Chrome (PC版)

image.png

image.png

image.png

image.png
あとはお好みで。


  1. CodePenが悪いわけではなく、レンタルサーバ上のブログなどでも同様。CodePenを例にしたのはたまたま自分が使ってたから。 

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

【javascript】文字列の配列をArrayに変換する方法

jsで文字列の配列をArrayに変換する方法はいくつかの方法があります

evalは使い所を間違えるとユーザーに任意のjsを実行させてしまうことになるので注意しましょう。

カンマ区切りの文字列を配列にする方法

元データがこれの場合

'Shiro,Ai,Kaguya,Tokino'
'Shiro,Ai,Kaguya,Tokino'.split(',');

evalで実装する方法

元データが文字列の配列の場合

'["BoraOne", "RacingZero", "Zonda"]'
JSON.parse('["BoraOne", "RacingZero", "Zonda"]')

json parseで実装する方法

元データが文字列の配列の場合

'["BoraOne", "RacingZero", "Zonda"]'
eval('["BoraOne", "RacingZero", "Zonda"]')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSのグローバル変数について備忘録

JSを勉強している中で「グローバル変数」なるものを知りました。
なにに使うの?他には何があるの?どういうところを気を付ければいいの?と調べた内容を備忘録的に記載します。

※参考サイトは一番下に記載してます。
もはやそれらサイトの中身をちょいちょいかいつまんだ内容になっております。。あくまで備忘録

グローバル変数とは

グローバル変数とは、プログラムのどこからでもアクセスできる変数。

グローバル変数の対義語として、「ローカル変数」というものがあり、ローカル変数は関数内で宣言して関数内で使用できる変数、とのことです。

グローバル変数を定義するには

①関数の外で変数の宣言をする
②varなしで変数宣言(JSでは「var」を使わずに宣言した変数はすべてグローバル変数とみなされる)
このどちらかで宣言された変数はグローバル変数となります。

グローバル変数の懸念点

varを使わずに関数の内外どこでも宣言でき便利なようで、どこでも取り出せてどこでも再代入できてしまうため、同じ変数名が複数存在したときにバグになりやすい という懸念点があります。

varの懸念点

じゃあ関数内でvarで宣言すればいいか?というとそうでもない。
同じ関数内であれば再代入も再宣言も可能なので、気が付いたら自分が思っていたのと違う中身になっていたそれがどこで発生している事象なのかおっかけるの大変・・・などという状況になり、予期せぬバグになりやすい。

JSで変数を使用するときは

基本的には const を使用し、再代入が必要な変数には let を使用する のが良いとのこと。

constとは

constとは値書き換えを禁止した変数を宣言する方法で、定数として一度宣言したら再代入・再宣言は不可となる。(書き換えようとするとエラーが発生するため、予期せぬバグになりにくい)

letとは

再代入可能だが再宣言は不可。ブロックスコープ。
同じ関数内でもブロックごと分けることで再代入の影響を受けなくなる。
多少書き換えがあっても影響範囲がブロックに留まるため、どこでなにしてるかわかりやすくなる。

まとめ

指定せずグローバル変数にするのは辞めましょう。
var は自由度が高すぎるので予期せぬエラーが発生しやすい。

そのため、予期せぬエラーを防ぐ・可読性を上げるためにも変数は基本 const 再代入を行いたい場合 let を使用する。

参考サイト
JavaScriptのグローバル変数を解説!ローカル変数と違いとは
JavaScriptでのグローバル変数の宣言方法
JavaScript初心者必見!constとは?基礎の基礎を解説!

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

JavaScriptでテキストの差分を見るライブラリ

まえがき

JavaScriptで2ファイルのテキストの差分を確認するためのDiff用ライブラリについて調べます。

difflib

image.png

GitHub:
https://github.com/cemerick/jsdifflib

デモサイト
http://cemerick.github.io/jsdifflib/demo.html

サンプルコード

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
<link rel="stylesheet" href="diffview.css">
<script src="difflib.js"></script>
<script src="diffview.js"></script>
<script id="src1" type="sourcecode">
function b() {
  console.log('TESTdddddddddddddddddddddddddddddddddddddddddddddddddddddddddsfafdasdfffffffffffffffffffffffffff0');
  console.log('TEST');
  console.log('TEST1');
}
</script>
<script id="src2" type="sourcecode">
function b() {
  console.log('TEST');
  console.log('TEST2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
  console.log('TEST3');
}
</script>
<!-- End Matomo Code -->
    </head>
    <body>
        <div id="output"></div>
        <script  type="text/javascript">
          const src1 = document.getElementById('src1').innerText;
          const src2 = document.getElementById('src2').innerText;

          var base = difflib.stringAsLines(src1);
          var newtxt = difflib.stringAsLines(src2);

          // create a SequenceMatcher instance that diffs the two sets of lines
          var sm = new difflib.SequenceMatcher(base, newtxt);

          // get the opcodes from the SequenceMatcher instance
          // opcodes is a list of 3-tuples describing what changes should be made to the base text
          // in order to yield the new text
          var opcodes = sm.get_opcodes();
          var contextSize = 0;
          document.getElementById('output').append(diffview.buildView({
              baseTextLines: base,
              newTextLines: newtxt,
              opcodes: opcodes,
              // set the display titles for each resource
              baseTextName: "Base Text",
              newTextName: "New Text",
              contextSize: contextSize,
              viewType: 1 // 0にするとbaseとnewTextが別の列になります
          }));


        </script>
    </body>
</html>

使用感

・シンプルで使い易いですが、細かい調整はできないようです。(たとえば文字レベルでの差分の表示は現時点でできない)
・積極的な開発はおこなわれていないようです。
・BSDライセンスです。

prettydiff

image.png

GitHub
https://github.com/prettydiff/prettydiff/

デモ
https://prettydiff.com/

ドキュメント
https://prettydiff.com/documentation.xhtml

サンプル

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
<script src="prettydiff/js/browser.js"></script>
<link href="prettydiff/css/index.css" media="all" rel="stylesheet" type="text/css"/>

<script id="src1" type="sourcecode">
function b() {
  console.log('TESTdddddddddddddddddddddddddddddddddddddddddddddddddddddddddsfafdasdfffffffffffffffffffffffffff0');
  console.log('TEST');
  console.log('TEST1');
}
</script>
<script id="src2" type="sourcecode">
function b() {
  console.log('TEST');
  console.log('TEST2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
  console.log('TEST3');
}
</script>
<!-- End Matomo Code -->
    </head>
    <body>
        <div class="white" id="prettydiff"></div>
        <script  type="text/javascript">
          const src1 = document.getElementById('src1').innerText;
          const src2 = document.getElementById('src2').innerText;

// integrate into the browser
let output     = "",
    prettydiff = window.prettydiff,
    options    = window.prettydiff.options;
options.source_label = "修正前";
options.source = src1;
options.diff_label = "修正後";
options.diff = src2;
options.diff_format = "html";

options.mode = "diff";
options.language = "auto";
options.lexer = "text";
options.wrap = 10;
options.diff_view = 'inline'; // 'sidebyside'で別の列で表示
output         = prettydiff();
console.log(output);
          document.getElementById('prettydiff').innerHTML = output;
// You can include the Pretty Diff code in any way that is convenient,
// whether that is using an HTML script tag or concatenating the
// js/browser.js code with your other code.

        </script>
    </body>
</html>

使用感

・ブラウザで使用するbrowser.jsはnpmでインストール後、tscコマンドで作成されます
さまざまなオプションがあります
・ライセンスはCC0

mergely

image.png

GitHub
https://github.com/wickedest/Mergely

デモ
http://www.mergely.com/

サンプル

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>DIFF</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/codemirror.min.js"></script>
<link rel="stylesheet" media="all" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/codemirror.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/addon/search/searchcursor.min.js"></script>
<script src="mergely/libs/mergely.js" type="text/javascript"></script>
<link rel="stylesheet" media="all" href="mergely/libs/mergely.css" />
    </head>

<script id="src1" type="sourcecode">
function b() {
  console.log('TESTdddddddddddddddddddddddddddddddddddddddddddddddddddddddddsfafdasdfffffffffffffffffffffffffff0');
  console.log('TEST');
  console.log('TEST1');
}
</script>
<script id="src2" type="sourcecode">
function b() {
  console.log('TEST');
  console.log('TEST2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
  console.log('TEST3');
}
</script>


<body>
<div class="mergely-full-screen-8">
  <div class="mergely-resizer">
    <div id="mergely"></div>
  </div>
</div>

        <script>
$(document).ready(function () {
    $('#mergely').mergely({
        wrap_lines : true,
        cmsettings: { 
            readOnly: false,
            lineNumbers: true
        },
        lhs: function(setValue) {
            setValue(document.getElementById('src1').innerText);
        },
        rhs: function(setValue) {
            setValue(document.getElementById('src2').innerText);
        }
    });
});
        </script>
</body>

    </body>
</html>

使用感

・依存ライブラリが多い(JQuery,codemirror)
・codemirrorがエディタ用のライブラリなので、修正したテキストの差分がすぐ確認できる
・ライセンスはGNU LGPL v3.0

まとめ

・差分を表示するだけならprettydiffがよさそうです。
・マージなどの編集が必要ならmergelyになるでしょうが、JQueryに依存しています。
・2ファイルを入力とするのでなく、diffの結果を入力としてHTML表示するならdiff2htmlも使えそうです。
・なお、画像の差分をとるならjs-imagediffが使えそうでした(未検証)

参考

JavaScript based diff utility [closed]
https://stackoverflow.com/questions/3053587/javascript-based-diff-utility

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

あっさり読むrails 番外(image)

はじめに

以前の記事の応用で、画像アドレスをフォームに入力すると、自動的に画像が表示されるようにします。

前提

前記事をそのまま修正します。

実行

ビューファイルを次の様に修正します。

sample.haml.html
= form_with model:@sample, local: true do |f|
  = f.text_area :name, placeholder: "サンプル", class: "sample-form"

.add-image

add-textadd-imageに変えただけです。

JSファイルを次の様に書きます。

sample.js
$(function(){
 $(".sample-form").on("change" functon(){
   var image = $(this).val();
   let html = `
     <div class = "add-image__sample">
       <img src = "$(image)" height = "500" //heightはなんでもいい
     </div>
   `
   ;
   $(".add-image").append(html);
 })
});

大雑把な流れとしては、
1.フォーム(.sample-form)の中身が変わった("change")時=画像アドレスを貼り付けた時
2.その値をimageに代入し
3.それを表示させるhtmlデータを変数に代入し
4..add-imageに追加(append)する

となります。

本当は、別の画像アドレスを貼り付ける時に元の画像を削除する必要がありますが、
ひとまずはこれで非同期の画像表示ができます。

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

Reactコンポーネントのテスト設計と実装(後編)

はじめに

この記事ではReactコンポーネントのテストユーティリティであるReact Testing Libraryについて説明します。

この記事で説明すること

  • 前編
    • Testing Trophyの概要
    • React Testing Libraryのコンセプト
  • 後編(この記事)
    • Reactコンポーネントのテストケースの設計
    • React Testing Libraryを使用したテストコードの実装

開発環境

  • React: 16.12.0
  • Jest: 24.9.0
  • React Testing Library: 9.3.2

React Testing Libraryを使ってみようと思ったきっかけ

だいぶ前のことになるのですが、ReactコンポーネントのテストユーティリティとしてEnzymeに代えてReact Testing Libraryを使ってみました。

Enzymeを使っていたときは、find(#id)find(displayName)といったコードをよく書いていました。ただこのようなコードを書くと、id名やdisplayNameを変更するというリファクタリングをしただけでもテストコードを書き直す必要が出てきてしまいます。

これではリファクタリングのコストが大きすぎるということで、Enzymeで良い方法が無いかを探していたところ、むしろEnzymeに代わるものとしてReact Testing Libraryというライブラリがあることを知りましたのでこのライブラリを使用してみました。合わせて、React Testing Libraryの作者によりTesting Trophyという考え方が提唱されているということも知りました。

そこで、この記事では私がReact Testing Libraryを学習する中で知ったTesting TrophyとReact Testing Libraryの考え方と、この考え方を実際のテストコードに適用した例を説明します。

コンポーネント単体のテスト

まず最初にコンポーネント単体のテストについて考えます。

テストの方針

テストの方針としては、React Testing Libraryの開発者であるKent C. Dodds氏の言葉にあるように、propsと描画される結果に着目します。

so what parts of our code do each of these users use, see, and know about? The end user will see/interact with what we render in the render method. The developer will see/interact with the props they pass to the component. So our test should typically only see/interact with the props that are passed, and the rendered output.

また、ロジックがない部分については、単純な実装でありテストをする必要性が低いものであったり、型チェックなどができるため、コンポーネント単体のテストの対象からは除外します。

Things that really have no logic in them at all (so any bugs could be caught by ESLint and Flow). Maintaining tests like this actually really slow you and your team down.

つまり、コンポーネント単体のテストとしては、以下のようなコンポーネントをテストする方針とします。

  • テスト対象のコンポーネント
    • コンポーネント内で何らかのDOM要素を描画する際にロジックを含むもの
  • テストの内容
    • コンポーネントのpropsの値に応じてDOM要素が正しい内容で描画されるかどうか

テスト設計の手順

前述したテストの方針を踏まえて、以下の手順でテストケースの設計を行います。

  1. コンポーネントのpropsをリストアップする

  2. 各propsが取り得る値をリストアップする

    1. propsの初期状態がどのようになっているか
    2. 正常時にはpropsがどのような値を取り得るか
    3. エラー時にはpropsがどのような値を取り得るか
  3. 各propsの取り得る値ごとに期待値(=コンポーネントの描画結果)を定義する

  4. それぞれの状態をテストケースとする

テスト設計とテストコードの実例

では、実際にテストケースの設計とテストコードの実装を行ってみます。

テスト対象のコードとして、Reduxのチュートリアルで使用されているTodoリストを使用します。

(本記事では、Todoリストアプリのコードについては説明はしませんので、コードの内容はReduxのチュートリアルページを見てください。また、Todoリストアプリの完成イメージはこちらで確認できます)

1つ目の例として、Todoコンポーネントのテスト設計をします。

1.コンポーネントのpropsをリストアップする
Todoコンポーネントが受け取るpropsはcompleted: booleantext: stringになります。

2.各propsが取り得る値をリストアップする

  • 初期状態: なし
  • 正常値:
    • completed = true, text = 'todo item'
    • completed=false, text='todo item'
  • エラー値: なし

3.各propsの取り得る値ごとに期待値(=コンポーネントの描画結果)を定義する

  • 正常値
    • completed = true, text = 'todo item' → Todoコンポーネント内にlist要素があり、text-decoration: line-throughのstyleが適用されている
    • completed=false, text='todo item' → Todoコンポーネント内にlist要素があり、text-decoration: line-throughのstyleが適用されていない

4.それぞれの状態をテストケースとする
1〜3をまとめると以下のようになります。

(2)正常値のケース
(2-1)completed=true

(2-1)completed=false
props This This
 completed true false
 text 'todo item' 'todo item'
期待値
 list要素 あり あり
 style text-decoration: line-through text-decoration: none

実際のコードは以下のようになります。

import React from 'react';
import { render } from '@testing-library/react';

import Todo from './Todo';

describe('Todo component', () => {
  describe('(2-1)completed=true', () => {
    it('has list and line-through style', () => {
      const todo = 'todo item'
      const completed = true
      const onClickMock = jest.fn();

      const { getByText } = render(<Todo text={todo} completed={completed} onClick={onClickMock}/>);

      expect(getByText(todo)).toBeInTheDocument()
      expect(getByText(todo)).toHaveStyle('text-decoration: line-through;')
    })
  });

  describe('(2-1)completed=false', () => {
    it('has list and no line-through style', () => {
      const todo = 'todo item'
      const completed = false
      const onClickMock = jest.fn();

      const { getByText } = render(<Todo text={todo} completed={completed} onClick={onClickMock}/>);

      expect(getByText(todo)).toBeInTheDocument()
      expect(getByText(todo)).toHaveStyle('text-decoration: none;')
    })
  });
})

APIリファレンス

サンプルコード内で使用されているReact Testing Libraryの各APIについては、公式のAPIリファレンスをご参照ください。

2つ目の例として、TodoListコンポーネントのテスト設計をします。

1.コンポーネントのpropsをリストアップする
TodoListコンポーネントが受け取るpropsはtodosになります

todos = [{
  id: 1,
   completed: true,
   text: 'Todo Item 1',
    }, {
      id: 2,
   completed: false,
   text: 'Todo Item 2',
  }
]

2.各propsが取り得る値をリストアップする

  • 初期状態: todos=[ ]
  • 正常値: todos = [{id:1,...}, {id:2,...}, {id:3,...}]
  • エラー値: なし(配列以外の値がpropsで渡される可能性はありますが、型チェックによってテストできているものとして、今回のテストからは除外します)

3.各propsの取り得る値ごとに期待値(=コンポーネントの描画結果)を定義する

  • 初期状態: Todoリスト(list要素)が描画されない
  • 正常値: todosの配列の要素数分だけ、Todoリスト(list要素)が描画される

4.それぞれの状態をテストケースとする
1〜3をまとめると以下のようになります。

Left align Right align Center align
This This This
column column column
will will will
be be be
left right center
aligned aligned aligned
(a)初期状態のケース (b)正常値のケース
props
 todos [ ] todos=[{id:1,...}, {id:2,...}, {id:3,...}]
期待値
 Todoリストの数 なし 3つ

実際のコードは以下のようになります。

import React from 'react';
import { render } from '@testing-library/react';

import TodoList from './TodoList';

describe('TodoList component', () => {
  describe('(a)初期状態のケース', () => {
    it('has no todo item', () => {
      const todos =[];
      const toggleTodoMock = jest.fn();
      const { queryByTestId } = render(<TodoList todos={todos} toggleTodo={toggleTodoMock}/>);

      // Todoリストの数が0個であることを確認する
      expect(queryByTestId('todo')).toBeNull();
    })
  });

  describe('(b)正常値のケース', () => {
    it('has 3 todo item', () => {
      const todos =[{
        id: 0,
        completed: false,
        text: 'パンを買う',
      }, {
        id: 1,
        completed: false,
        text: '牛乳を買う',
      }, {
        id: 2,
        completed: true,
        text: '掃除する'
      }];
      const toggleTodoMock = jest.fn();

      const { queryAllByTestId } = render(<TodoList todos={todos} toggleTodo={toggleTodoMock}/>);
      // Todoリストの数が3個であることを確認する
      expect(queryAllByTestId('todo').length).toBe(3);
    })
  });
})

APIリファレンス

サンプルコード内で使用されているReact Testing Libraryの各APIについては、公式のAPIリファレンスをご参照ください。

ここで、queryByTestIdというものが出てきましたので少し補足します。

ByTestIdというのは、APIリファレンスによると以下のようなものになります。

The ...ByTestId functions in DOM Testing Library use the attribute data-testid

In the spirit of the guiding principles, it is recommended to use this only after the other queries don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible.

つまり、画面に表示されるテキスト(ByText)やラベル(ByLabelText)のようにユーザーから見えるDOM要素を取得するべきなのですが、今回の様なテストケースではそういったものがないため、ByTestIdで代用します。

また、ByTestIdで要素を取得できるようにTodo.jsコンポーネントも以下のように修正します。

import React from 'react'
import PropTypes from 'prop-types'

const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through' : 'none'
    }}
    data-testid="todo" // ← この行を追加
  >
    {text}
  </li>
)
...
export default Todo

シナリオテスト

次にシナリオテストについて考えてみます。

テストの方針

シナリオテストとしては、ユーザーが実際に操作する方法でテストを行えばよいかと思います。

Write down a list of instructions for that user to manually test that code to make sure it's not broken. (render the form with some fake data in the cart, click the checkout button, ensure the mocked /checkout API was called with the right data, respond with a fake successful response, make sure the success message is displayed).

Turn that list of instructions into an automated test.

テスト設計の手順

具体的なテストケースとしては以下のような手順で設計します。

  1. ユーザーがアプリケーション使用する際のシナリオを書き出す
    要件定義書や設計書があるならば、ユースケース一覧やユースケース記述からシナリオを書き出せばよいかと思います。

  2. 各ユースケースの初期状態を決める

  3. 各ユースケースのイベントで発生する内容を洗い出す

  4. 2と3の期待値を定義する

  5. テスト内容としてまとめる

テスト設計とテストコードの実例

引き続き、Reduxのチュートリアルで使用されているTodoリストアプリを使用して実際のテストケースの設計を行ってみます。

1.ユーザーがアプリケーション使用する際のシナリオを書き出す
シナリオとしては以下の3つになります。

  • (a) Todoリストを追加する
  • (b) フィルターを切り替える
  • (c) Todoリストを完了済みにする

2.各ユースケースの初期状態を決める

  • (a) Todoリストを追加する
    • フィルターは'All'、Todoリストは空の状態
  • (b) フィルターを切り替える
    • フィルターは'All'、Todoリストは3つ登録された状態
  • (c) Todoリストを完了済みにする
    • フィルターは'All'、Todoリストは3つ登録された状態

3.各ユースケースのイベントで発生する内容を洗い出す

  • (a) Todoリストを追加する
    • 空の文字列のTodoリストを追加する
    • Todoリストを追加する
  • (b) フィルターを切り替える
    • フィルターを'Active'に切り替える
    • フィルターを'Completed'に切り替える
  • (c) Todoリストを完了済みにする
    • Todoリストを未完了から完了済みにする
    • Todoリストを完了済みから未完了にする

4.2と3の期待値を定義する

(a) Todoリストを追加する (b) フィルターを切り替える (c) Todoリストを完了済みにする
期待値
 初期状態 (a-1)
・Todoリストなし
・Allのフィルターが選択された状態
(b-1)
・すべてのTodoコンポーネントが表示された状態
・Allのフィルターが選択された状態
(c-1)
・すべてのTodoコンポーネントが表示された状態
・Allのフィルターが選択された状態
 イベント発生後 (a-2)
[空の文字列のTodoリストを追加する]
・Todoリストなし
(b-2)
[フィルターを'Active'に切り替える]
・未完了のTodoリストのみが表示された状態
・Activeのフィルターが選択された状態
(c-2)
[Todoリストを未完了から完了済みにする]
・完了済みにしたTodoリストには取り消し線が引かれる
(a-3)
[Todoリストを追加する]
・Todoリストが1つ
(b-3)
[フィルターを'Completed'に切り替える]
・完了済みのTodoリストのみが表示された状態
・Completedのフィルターが選択された状態
(c-3)
[Todoリストを完了済みから未完了にする]
・未完了にしたTodoリストには取り消し線が削除される

実際のテストコードとしては以下のようになります。

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import reducer from '../reducers'
import App from './App';

function renderWithRedux(
  ui,
  { initialState, store = createStore(reducer, initialState) } = {}
) {
  return {
    ...render(<Provider store={store}>{ui}</Provider>),
    store,
  }
}

describe('App component', () => {
  describe('(a) Todoリストを追加する', () => {
    it('add todo', () => {
      const { getByText, queryByTestId, getByTestId } = renderWithRedux(<App />);

      // (a-1)初期状態の確認
      // Todoリストなし
      expect(queryByTestId('todo')).toBeNull();
      // Allのフィルターが選択された状態
      expect(getByText('All')).toBeDisabled();
      expect(getByText('Active')).toBeEnabled();
      expect(getByText('Completed')).toBeEnabled();

      // (a-2)空の文字列のTodoリストを追加する
      fireEvent.change(getByTestId('input'), { target: { value: '' } })
      // Todoリストなし
      expect(queryByTestId('todo')).toBeNull();

      // (a-3)Todoリストを追加する
      fireEvent.change(getByTestId('input'), { target: { value: '123' } })
      fireEvent.click(getByText('Add Todo'))
      // Todoリストが1つ
      expect(getByText('123')).toBeInTheDocument();

    });
  });


  describe('(b) フィルターを切り替える', () => {
    it('switch filter', () => {
      const todos =[{
        id: 0,
        completed: false,
        text: 'パンを買う',
      }, {
        id: 1,
        completed: false,
        text: '牛乳を買う',
      }, {
        id: 2,
        completed: true,
        text: '掃除する'
      }];
      const { getByText, queryByText } = renderWithRedux(<App />, {
        initialState: {todos: todos},
      });

      // (b-1)初期状態の確認
      // すべてのTodoコンポーネントが表示された状態
      expect(getByText(todos[0].text)).toBeInTheDocument()
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: none;')

      expect(getByText(todos[1].text)).toBeInTheDocument()
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')

      expect(getByText(todos[2].text)).toBeInTheDocument()
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: line-through;')

      // Allのフィルターが選択された状態
      expect(getByText('All')).toBeEnabled();
      expect(getByText('Active')).toBeDisabled();
      expect(getByText('Completed')).toBeEnabled();


      // (b-2)フィルターを'Active'に切り替える
      fireEvent.click(getByText('Active'))

      // 未完了のTodoリストのみが表示された状態
      expect(getByText(todos[0].text)).toBeInTheDocument()
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: none;')

      expect(getByText(todos[1].text)).toBeInTheDocument()
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')

      expect(queryByText(todos[2].text)).toBeNull()

      // Activeのフィルターが選択された状態
      expect(getByText('All')).toBeEnabled();
      expect(getByText('Active')).toBeDisabled();
      expect(getByText('Completed')).toBeEnabled();


      // (b-3)フィルターを'Completed'に切り替える
      fireEvent.click(getByText('Completed'))

      // 完了済みのTodoリストのみが表示された状態
      expect(queryByText(todos[0].text)).toBeNull()

      expect(queryByText(todos[1].text)).toBeNull()

      expect(getByText(todos[2].text)).toBeInTheDocument()
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: line-through;')

      // Completedのフィルターが選択された状態
      expect(getByText('All')).toBeEnabled();
      expect(getByText('Active')).toBeEnabled();
      expect(getByText('Completed')).toBeDisabled();

    })
  });

  describe('Toggle todo scenario', () => {
    it('toggle todo', () => {
      const todos =[{
        id: 0,
        completed: false,
        text: 'パンを買う',
      }, {
        id: 1,
        completed: false,
        text: '牛乳を買う',
      }, {
        id: 2,
        completed: true,
        text: '掃除する'
      }];

      const { getByText } = renderWithRedux(<App />, {
        initialState: {todos: todos},
      });

      // (c-1)初期状態の確認

      // すべてのTodoコンポーネントが表示された状態
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: none;')
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: line-through;')

      // Allのフィルターが選択された状態
      expect(getByText('All')).toBeEnabled();
      expect(getByText('Active')).toBeDisabled();
      expect(getByText('Completed')).toBeEnabled();


      // (c-2)Todoリストを未完了から完了済みにする
      fireEvent.click(getByText(todos[0].text));

      // 完了済みにしたTodoリストには取り消し線が引かれる
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: line-through;')
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: line-through;')


      // (c-3)Todoリストを完了済みから未完了にする
      fireEvent.click(getByText(todos[2].text));

      // 未完了にしたTodoリストには取り消し線が削除される
      expect(getByText(todos[0].text)).toHaveStyle('text-decoration: line-through;')
      expect(getByText(todos[1].text)).toHaveStyle('text-decoration: none;')
      expect(getByText(todos[2].text)).toHaveStyle('text-decoration: none;')

    });
  });
})

APIリファレンス

サンプルコード内で使用されているReact Testing Libraryの各APIについては、公式のAPIリファレンスをご参照ください。

またReduxコンポーネントをテストをするときにRedux storeの値をコンポーネントを渡す方法は、公式サイトに記述されている方法を参考にしています。

参考にしたサイト

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

iOSを“区別”したモーダル背景のスクロール固定

概要

つい先日Win7のサポートが終わり、2020年4月以降にはEdgeもChromium製にアップデートされるとのことで、ようやくIEの壁から目を逸らせる雰囲気になってきた先にそり立つ第二の壁ことiOS Safari(個人の感想です)。

数多のトラップがiOSのアップデートのたびに仕込まれるこのブラウザですが、なまじ日本における(iPhoneの)シェア率が高いばかりに泣く泣く対応せねばなりません。どこかで聞いた話ですね:thinking:

今回はその中でもモーダルウィンドウを用いるサイトの対応について、iOSかそれ以外かを区別することでうまいこと実装できた気がするので備忘録として投稿します。

あくまで差別ではなく“区別”です。

……iOS版のVivaldiはやく出ないかな。(Safariを虚ろな目で見つめつつ)

要望と仕様

モーダルウィンドウなどのコンテンツに被る情報が表示されているあいだ、背景となったコンテンツ……つまり body 部のスクロールは動かないよう固定されるべきであると考えられます。
背景まる見えレベルのサイズ感であれば許されるかもしれませんが、画面をほとんど覆うような大きさのモーダルの場合、弄っている間にいつの間にかモーダルを開いた時とは全く違う場所までスクロールしていた……となるとユーザに混乱を招くことになりかねないので、固定する方が自然な挙動だと言えます。(お客様からの要望としてもよく言われます)

加えて、モーダルウィンドウで表示される情報が一画面内に収まるとは限りません。
PCのデザインや開発者ツール上では十分収まっている量でも、画面の小さいスマートフォンや横向きにした時(見辛いことこの上なしですが……)、またはヒョコヒョコとウザいツールバーの影響などによって想定よりも狭い表示領域で見られることを考えると、モーダルウィンドウ内もスクロールに対応するのが柔軟性のある実装だと言えるでしょう。

こうした場合、touchmove イベントをe.preventDefault(); などでキャンセルする暴力的方法は、モーダル内のスクロールも禁止してしまうため使えません。きちんと影響範囲を把握して実装すれば可能ではありますが、そのためだけにタッチイベントを制御するのは別の問題が発生しやすく、手間もリスクも多いと考えます。

よって、手早く背景 body のスクロールを固定する方法は、おおむね2つに絞られます。

  • bodyoverflow: hidden; をかける
  • bodyposition: fixed; にし、その時点のスクロール位置だけ表示をズラす

シンプルな実装

実装がシンプルかつ分かりやすいのは前者の overflow: hidden; です。

CSS
body {
  overflow: hidden;
}

body (表示領域)外のコンテンツが省略されるため、ほとんどのブラウザではこれだけでスクロールを固定することができます。
モーダルを閉じる時にも overflow:hidden; を解除するだけ。超超簡単……(カゴノトリ)

そう、iPhone(iOS)“以外”ならね。

面倒な実装

iOS(iPadOSを含む)のSafariのみ、先ほどの実装ではスクロールを止めることができません。
そのため、結局は後者の position: fixed; による実装が必要となります。

CSS
body {
  width: 100%; /* position:fixed;になった際に幅が変わるのを防ぐ */
  position: fixed;
  top: -XXXpx; /* モーダルを開いた地点のスクロール量(XXX)だけズラす */
}

bodyposition: fixed; をかけることでスクロールを無効化されるため、iOS Safariを含めた全てのブラウザで強制的にスクロールを固定することが可能です。(この際、 body の内容によっては表示幅が変わってしまうことがあるため、保険としてwidth: 100%;をかけておきましょう)

ただ、position: fixed; をかけた時点でこれまでのスクロール情報が失われてしまう(表示がページトップに戻ってしまう)ため、 top プロパティによってモーダルを開いた地点のスクロール量だけbodyをマイナス(ネガティブ)方向にズラすことで表示位置が変わらないように見せかける必要があります。手間ですね:cry:

また、先ほどの方法と同様モーダルを閉じた際には position: fixed; を解除するのですが、その際にも失われたスクロール情報は戻らないので、今度はスクロール位置をモーダルを開く前と同じ位置に自力で戻す必要があります。二度手間ですね:sob:

……愚痴を言っても仕方ないので、モーダルを開いた時点のスクロール量 XXX をJSのグローバル変数に逐一保持しておくことで、これらの問題を解決します。

さらに面倒な仕様

しかし、position: fixed; を使う方法にもいくつかの穴があります。

ひとつはスクロール情報が失われることにより、スクロール量に応じて表示が切り替わるコンテンツに影響が出ます。例えばスクロールすると表示されるページトップに戻るボタンであったり、position: sticky; ないしはそれに近い動きをJSで実装しているものはスクロールを固定した途端に表示が消えます。ただ、これらはスクロールの固定を解除した後にはきちんと元の状態に戻るはずなので、些細な問題ではあります。

別の問題としてはMacOS(PC)のSafariで見た場合に、position: fixed; によって「ページトップに戻った」瞬間が見えてしまうことがあります。つまりモーダルを開閉するたびにページトップが一瞬チラつくような表示が稀に発生するため、モーダルを開く箇所が多いサイトだと地味に鬱陶しく感じます。IEですら起きないのに

また、よく他サイトなどで紹介されている方法として overflow: hidden;position: fixed; を併用している(2つの実装が合体したような)ものをよく見ますが、そうするとiOSのSafariモーダルの開閉時に一瞬白いチラつきが発生することがありました。これは overflow: hidden; がなければ発生しないようなので、上記のCSS例では省いています。同じSafariって名前なら挙動ぐらい統一してくれ

ともあれ、起こってしまうものは仕方ありません。それぞれの環境に適した実装対応を表にまとめてみました。

iOS Safari MacOS Safari それ以外
overflow: hidden; ×
position: fixed;
(併用)

iOS, MacOS以外の環境(WinやAndroid等)はどちらの方法でもほぼ同じ挙動にはなるのですが、前述した通り overflow: hidden; の方がシンプルかつ確実な方法なので二重丸つけてます。

さらにまとめるとこうなります。

  • iOS: position: fixed; で対応
  • MacOS、それ以外: overflow: hidden; で対応

つまり、iOSか否かを区別できれば、それぞれに適した方法でスクロールを固定することができそうです。

iOSかどうかをチェックするUA(ユーザーエージェント)は以下を参考にさせていただきました。
https://qiita.com/mtdune/items/97abb9c0bd926d4c8a13

実装

position: fixed; 対策の width: 100%; は予めCSSの方で適用していてもあまり問題なさそうなので、先に書いておきます。この辺りは好みだと思います。

CSS
body {
  width: 100%; /* position:fixed;になった際に幅が変わるのを防ぐ */
}

準備が済んだところで、スクロール固定・解除用の関数を作成します。

jQuery
//モーダルを開いた時のスクロール位置を保持
var scrollPosition;
//iOS(iPadOSを含む)かどうかのUA判定
var ua = window.navigator.userAgent.toLowerCase();
var isiOS = ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1 || ua.indexOf('macintosh') > -1 && 'ontouchend' in document;

//bodyのスクロール固定
function bodyFixedOn() {
    if(isiOS){
        // iOSの場合
        scrollPosition = $(window).scrollTop();
        $('body').css('position', 'fixed');
        $('body').css('top', '-' + scrollPosition + 'px');
    }else {
        // それ以外
        $('body').css('overflow', 'hidden');
    }
}

//bodyのスクロール固定を解除
function bodyFixedOff() {
    if(isiOS){
        // iOSの場合
        $('body').css('position', '');
        $('body').css('top', '');
        $(window).scrollTop(scrollPosition);
    }else {
        // それ以外
        $('body').css('overflow', '');
    }
}

これでモーダルを開く際に bodyFixedOn() を読み込むことで、

  • iOS(iPhone, iPad): position:fixed;による固定
  • それ以外のPC(Win,Mac)やAndroid等: overflow:hidden; による固定

が成されるようになります。
また、モーダルを閉じる際には bodyFixedOff() で解除すれば元通り。

ちなみに、上記の条件だと端末部分のみの判定となるため、iOSアプリ版のChromeなどを使った場合であっても同じくiOSに該当するかと思います(iOSだとSafariしかちゃんと開発者ツールでの検証ができない……)。
ただ、そういった環境でもチェックした限り挙動としてはほとんど問題はないかなと思っています。

とはいえ、そもそもUAを廃止する流れやiOSのアップデートによってコロコロ仕様が変わることを考えるとこの方法も永久に使えるわけではなさそうなので、ひとまず現在(2020年1月)時点の暫定対応というつもりです。備えよう。無理……しんどい……

おまけ

ES6できちんと書くならこういう感じ……?(テンプレート文字列って便利ですね)

JavaScript(ES6)
let scrollPosition;
const ua = window.navigator.userAgent.toLowerCase();
const isiOS = ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1 || ua.indexOf('macintosh') > -1 && 'ontouchend' in document;
const body = document.querySelector('body');

function bodyFixedOn() {
    if(isiOS){
        scrollPosition = window.pageYOffset;
        body.style.position = 'fixed';
        body.style.top = `-${scrollPosition}px`;
    }else {
        body.style.overflow = 'hidden';
    }
}

function bodyFixedOff() {
    if(isiOS){
        body.style.removeProperty('position');
        body.style.removeProperty('top');
        window.scrollTo(0, scrollPosition);
    }else {
        body.style.removeProperty('overflow');
    }
}

余談

この記事ではSafariばかりを目の敵にしてしまいましたが、今回紹介したどの方法でも「スクロールを固定する」=「スクロールバーが非表示になる」ので、スクロールバーの常時表示がデフォルトであるWindows環境ではスクロール固定時に画面がスクロールバーの幅の分だけ横にガタつくなどの問題もあります。

挙動自体は正しいものなので気になるなら〜程度ですが、対応策としてはいちおうIE/Edge(レガシ)専用ですが -ms-overflow-style: -ms-autohiding-scrollbar; あたりで強制的にauto-hide(コンテンツに被るスクロールバー)に変えてやるのが良さげです。ただWin版のFireFoxやChrome、Chromium版Edgeなどはスクロールバーごと非表示にする以外どうしようもなさそう……?

結論としてはなんでもかんでもモーダルに頼るのをやめるのがよさそうというオチでした。やめて(懇願)

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

始めてWebサイトを作成した時にお世話になった記事

初めに

プログラミングを始め、初めてWebサイトを作成した時に自分がお世話になった記事をここにあげます。
もし自分と同じようなところで悩んでいるところがあれば、参考にしていってください。

参考にした記事一覧

環境構築編
WindowsでPHP/Apache環境構築(ダウンロード~画面を起動するまで)
なぜか自分のパソコンにxamppがダウンロードできなかったので、この記事を参考にApacheサーバーを入れました。

CSS編
サルワカ ChromeでCSSが反映されない?キャッシュ消去で対処
CSSの変更がブラウザになかなか反映されないという問題がこれで解決しました。

JavaScript編
JavaScriptに"Maximum call stack …"で怒られた1例と解決までにやったこと
Google ChromeのデベロッパーツールのPerformanceを使ったエラーの検出方法、Maximum call stack というエラー文の意味について知ることができました。

Samurai Blog【JavaScript入門】誰でも分かるPromiseの使い方とサンプル例まとめ!
実際のWebサイトには実装しませんでしたが、JavaScriptの関数を同期的に使う方法について学べました。

PHP編
【PHP】HTMLSPECIALCHARS関数でエスケープ処理をしてください【XSSの防止】
利用者の入力情報のエスケープの方法について学ぶことができました。

Qiita PHPオブジェクト指向入門(前半)
本だけでは理解しきれなかったPHPの基本について知ることができました。

終わりに

Webサイトを始めて作成した際にブックマークをつけた記事はもう少し多かったのですが、いくつか消してしまったので7割程度しかあげられませんでした。
Webサイトを作成して思ったことは、自分が悩んでいる問題の解決策を無料で教えてくれる現代のネット環境は素晴らしいな、ということでした。
これからも楽しみながらプログラミングに取り組んでいきたいです。

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

chrome拡張機能で抽選機を作った

突然ですが私は極度の優柔不断です。
今日どのゲームするかを決めるのにも迷ってしまいます。
ということで前々から触ってみたかったchrome拡張機能で抽選王的なものを作りました。
と言っても今回は拡張機能APIを使う部分が無くてほぼ生のhtmlとjsだけ

出来たもの

https://github.com/engabesi/RandomPicker
tyusenk.gif

やること

  • chrome拡張機能セットアップ
  • popup作成

セットアップ

構成予定
RandomPicker/
├─ css/
│   └─ bulma.min.css
├─ html/
│   └─ popup.html
├─ icons/
│   ├─ icon16.png
│   ├─ icon48.png
│   └─ icon128.png
├─ js/
│   └─ popup.js
└─ manifest.json

セットアップと仰々しく書きましたが実際はjsonを一つ作るだけです。
chrome拡張機能のsettingsとなるmanifest.jsonを作ります。

manifest.json
{
  "manifest_version": 2,
  "version": "1.0",
  "name": "Random Picker",
  "description": "Pick a character string at random",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "browser_action": {
    "default_popup": "html/popup.html"
  }
}

manifest_version, version, nameは必須です。
また、今回は拡張機能のアイコンをクリックした際に出てくるポップアップ内で抽選ページを作るのでbrowser_actiondefault_popupにhtmlファイルを指定します。

詳細は公式ドキュメントや以下の方の記事が参考になります。
https://developer.chrome.com/extensions/manifest
https://qiita.com/mdstoy/items/9866544e37987337dc79

次にhtmlディレクトリを作成し、その中にpopup.htmlを作ります。

html\popup.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  </head>
  <body>
    <h1>Hello World!</h1>
    <script src="../js/popup.js"></script>
  </body>
</html>

ここまでだけでもうポップアップだけですが拡張機能ができました。
次にchromeに導入します。
chrome://extensions/をURL欄に入力して拡張機能設定画面に飛びます。
右上のスイッチを切り替えてデベロッパーモードをONにします。
「パッケージ化されていない拡張機能を読み込む」をクリックしてRandomPickerディレクトリを指定します。
するとアドレスバーの右側にRのアイコンが出現します。(iconを設定済みであればそのicon)
それをクリックして以下のようにポップアップが出現したら無事拡張機能インストール完了です。
image.png

作る

ここからhtmlとjsを書いていきます。
拡張機能要素はほぼ無いです。
解説するようなものが無いのでコードを貼るだけにしておきます。

CSSフレームワーク導入

htmlを書く前に、cssを極力書きたくないのでフレームワークを導入します。
今回はbulmaを使用します。
npmやCDN等がありますが、オフライン且つなるべく容量を少なくしたかったのでminだけ引っ張ります。
以下からbulmaをDLし、bulma.min.cssをcssフォルダ内に入れてインポートします。
https://bulma.io/

html + js

popup.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="../css/bulma.min.css" />
    <style>
      body {
        width: 500px;
      }
    </style>
  </head>
  <body>
    <section class="section">
      <div class="container">
        <!-- header -->
        <h1 id="title" class="title">Random Picker</h1>
        <!-- add area -->
        <div class="columns is-mobile is-vcentered">
          <div class="column">
            <form name="addForm" class="field has-addons">
              <div class="control">
                <input
                  name="addInput"
                  class="input"
                  type="text"
                  placeholder="New Text"
                />
              </div>
              <div class="control">
                <button type="submit" class="button is-info" tabindex="-1">
                  ADD
                </button>
              </div>
            </form>
          </div>
        </div>
        <!-- pick area -->
        <div class="columns is-mobile is-vcentered">
          <div class="column is-7">
            <form name="pickForm" class="field has-addons">
              <div class="control">
                <input
                  name="pickInput"
                  class="input"
                  type="number"
                  placeholder="Count of Pick"
                  value="1"
                />
              </div>
              <div class="control">
                <button type="submit" class="button is-success" tabindex="-1">
                  Pick
                </button>
              </div>
            </form>
          </div>
          <div class="column">
            <button id="clearBtn" class="button is-danger" tabindex="-1">
              Clear
            </button>
          </div>
        </div>
        <div class="columns is-mobile is-multiline">
          <!-- pick list -->
          <div class="column">
            <div
              id="pickList"
              class="column field is-grouped is-grouped-multiline"
            ></div>
          </div>
          <!-- picked list -->
          <div class="column">
            <p class="subtitle is-6">抽選結果</p>
            <div id="pickedList"></div>
          </div>
        </div>
      </div>
    </section>
    <script src="../js/popup.js"></script>
  </body>
</html>
popup.js
const addForm = document.addForm;
const pickForm = document.pickForm;
const pickListItems = [];

/** LocalStorage */
const saveToLocalStorage = str => {
  if (!str) return;
  localStorage.setItem(str, str);
};

const deleteFromLocalStorage = str => {
  localStorage.removeItem(str);
};

/** pick list */
const pickList = document.getElementById("pickList");

const addString = event => {
  event.preventDefault();
  const inputStr = addForm.addInput.value.trim();
  if (inputStr === "") return;
  appendAddHtml(inputStr);
  addForm.reset();
};

const appendAddHtml = str => {
  const html = createAddHtml(str);
  pickList.innerHTML += html;
  pickListItems.push(str);
  saveToLocalStorage(str);
};

const createAddHtml = str => {
  return `
  <div class="control">
    <div class="tags has-addons">
      <a class="tag is-success del">${str}</a>
      <a class="tag is-delete del"></a>
    </div>
  </div>`;
};

const deleteString = event => {
  if (!event.target.classList.contains("del")) return;
  const parent = event.target.parentElement.parentElement;
  parent.remove();
  const str = parent.textContent.trim();
  pickListItems.forEach((item, index) => {
    if (item === str) pickListItems.splice(index, 1);
  });
  deleteFromLocalStorage(str);
};

const clearStr = () => {
  pickListItems.length = 0;
  pickList.innerHTML = "";
  localStorage.clear();
};

/** picked list */
const pickedList = document.getElementById("pickedList");

const createPickedListHtml = strs => {
  let pickedListHtml = `<div class="list">`;
  strs.forEach(str => (pickedListHtml += `<a class="list-item">${str}</a>`));
  pickedListHtml += "</div>";
  return pickedListHtml;
};

const randomPick = event => {
  event.preventDefault();
  let length = pickListItems.length;
  if (length <= 0) return;
  const pickedStrs = [];
  const pickListItemsCpy = pickListItems.concat();
  const pickInputVal = pickForm.pickInput.value;
  const pickCount = pickInputVal > length ? length : pickInputVal;
  for (let i = pickCount; i > 0; i--) {
    const randomIndex = Math.floor(Math.random() * length);
    pickedStrs.push(pickListItemsCpy[randomIndex]);
    pickListItemsCpy.splice(randomIndex, 1);
    length--;
  }
  pickedList.innerHTML = createPickedListHtml(pickedStrs);
};

addForm.addEventListener("submit", e => addString(e));
pickList.addEventListener("click", e => deleteString(e));
pickForm.addEventListener("submit", e => randomPick(e));
document.getElementById("clearBtn").addEventListener("click", _ => clearStr());

// init
(() => {
  addForm.addInput.focus();
  Object.keys(localStorage).forEach(key => {
    const str = localStorage.getItem(key);
    if (!str) return;
    appendAddHtml(str);
  });
})();

これで冒頭に貼った画像のものが完成します。

まとめ

久しぶりにhtmlとjsを生で書きましたが中々辛い。
各種フレームワーク、ライブラリの有難みを感じる開発でした。
また今回拡張機能API等、拡張機能らしいことをほぼしていないので今度はその辺を絡めたものを作りたい。

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

【永久保存版】「||」とか「&&」ってなんやねん

結論

論理演算子と言います。
どうぞよろしくお願いします。

前提知識

  • 論理演算子は左から右という順番で評価をする
  • 論理演算子が返す値は基本的には真偽値(true or false)です。
    • ただ、真偽値以外も返すことができます
    • それは後程説明します。

解説

「||」

  • any1 || any2のどちらかがtrueであればtrueを返す。
    • any1がfalseの場合any2を見て、any2もfalseだった場合初めてfalseを返し、trueであればtrueを返す。
    • つまり、どちらもfalseでなければfalseを返さない。
a1 = true || true // true
a2 = true || false // true
a3 = false || true // true
a4 = false || false // false
a5 = (1 === 1) || false // true
a6 = false || (1 === 2) // false
  • 文字列も使用することができる。
    • が、その場合、必ずtrue判定(?)になる
a1 = "dog" || "cat" // "dog"
a2 = false || "cat" // "cat"
a3 = "dog" || false // "dog"

「&&」

  • any1 && any2のどちらもtrueであればtrueを返す
    • any1がtrueの場合any2を見て、any2もtrueであればtrueを返し、falseであればfalseを返す。
a1 = true && true // true
a2 = true && false // false
a3 = false && true // false
a4 = false && false // false
a5 = (1 === 1) && true // true
a6 = true && (1 === 2) // false
  • 「||」と同様、文字列を使用することができます。
    • が、文字列同士を比較した場合、「||」と違い、後ろの文字列を返す
a1 = "dog" && "cat" // "cat"
a2 = false && "cat" // false
a3 = "dog" && false // false

慣れてきた方へ

ショートサーキット評価

  • 論理演算子の左から右の順番で評価する仕様を利用した記法。
  • これにより、無駄な関数を動かさないようにして軽くしたりすることが可能。

heavyFunction() {
  for(let i = 0; i < 100; i++) {
    console.log('無駄に重い関数', i)
  }
}

if (true || heavyFunction()) {
  console.log('||のショートサーキット評価')
}
if(false && heavyFunction()) {
  console.log('&&のショートサーキット評価')
}    
  • ||は左がtrueの時点で右を見る必要もないのでtrueを返す
  • &&は左がfalseの時点で右を見る必要もないのでfalseを返す

結果、無駄に重いheavyFunction()を呼ばなくて済み、動作を軽くすることができます!

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

LocalファイルにJavascriptを使って書き込みする方法(承認必要)

はじめに

LocalファイルにJavascriptを使って書き込みする方法をまとめました。
ただし、セキュリティ上必ず承認が必要となってきます。そのセキュリティを回避する方法は今回説明しません。

関連リンク

関連リンクを下記に載せておくので、必要であれば参考にしてください。。

やりたいこと

ブラウザにてHTMLファイル(HTML:write_to_local_file.html)に埋め込んだJavascriptを動かして、ローカル上にあるtextファイル(text.txt)へ情報の書き込みをしたい。

コード実装

write_to_local_file.html
<!DOCTYPE html>
<html lang="ja" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Test for writing to local gile</title>
  </head>
  <body>
    <script type="text/javascript">
      // Scripting.FileSystemObject というオブジェクトを作成(JavaScript内でWSHを使ってファイルを扱う)
      var fs = new ActiveXObject("Scripting.FileSystemObject");

      // text.txtという新規のファイルを作成
      var file = fs.CreateTextFile("text.txt");

      // texxt.txtファイルへ書き込み
      file.Write("Complete, written!");

      // text.txtファイルを閉じる
      file.Close();
    </script>
  </body>
</html>

実行

IE11にて実行しました。

以下のような警告が出て"はい"をクリックすると、その下のテキストファイルの画像の通り、書き込みが完了しました。

参考

参考

まとめ

一般的に使われているブラウザを使用する場合はセキュリティ上毎回承認が必要になるので、この方法だけでは完全自動化することはできません。
回避する方法としては、Javascriptの実行環境を構築する、もしくは"はい"のボタンを自動で押す何かをつける、と今思いつくのはこれくらいです。
chromeブラウザを使用できる場合は、FileSystemAPIというものを使用するとできるというのを見かけましたので参考までに。。。

参考

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

気づいたこと日記(1日目)

1# 配列の長さぶん処理をするときには
ForIn ではなく ForEach を使おう!
 
fruits.forEach(function(item, index, array) {
console.log(item, index);
});
// りんご 0
// バナナ 1
 

→indexも実は使える…

 for(let i in fruits){
console.log(fruits[i],i);
// りんご 0
// バナナ 1
/*
一見こっちの方が簡単にすませられそうだが、
*/
console.log(typeof i ,i+1);
// string,NaN
// string,NaN
/*
Indexが文字列なので計算できなかったり…
*/

}

なので、forinを使う

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

VueコンポーネントのfunctionをES6スタイルで書く

はじめに

Vueでは関数定義をES6のアロー関数(Arrow Function)を使わずにES5以前からの書き方のfunction式(function expression)を使う方法が推奨されています

メソッド(例 plus: () => this.a++) を定義するためにアロー関数を使用すべきではないことに注意してください。
アロー関数は、this が期待する Vue インスタンスではなく、this.a が undefined になるため、親コンテキストに束縛できないことが理由です。

var vm = new Vue({
  data: { a: 1 },
  methods: {
    plus: function () {
      this.a++ // this = VueComponent
    }
  }
})
vm.plus()
vm.a // 2

例えばmethods内部でfunctionを定義した場合、thisに呼び出し元のVueインスタンスが格納されます。

var vm = new Vue({
  data: { a: 1 },
  methods: {
    plus: () => {
      this.a++ // this = undefined
    }
  }
})
vm.plus()
vm.a

一方でアロー関数で定義した場合、thisの内容はundefinedとなります。
そして上記の例でのthis.aの呼び出しはundefinedのプロパティを参照しようとするのでエラーとなってしまうのです。

これは従来のfunction expressionが呼び出し元のコンテキストをthisに格納する挙動に対して、アロー関数ではthisが呼び出し元ではなくレキシカルスコープのthisにバインドされるという違いによるものです。

私も当初アロー関数に書き直そうとして、このthis参照問題に当たってしまいました。
GoogleやQiitaで検索してみるとアロー関数を諦めてfunctionで関数定義しなさいと解説している記事がいくつかヒットするので同じ悩みの人が一定数いるようです。

しかし別の記法でES6スタイルで記述されている方のgistを参考に、functionを撤廃することができたので具体的な方法を紹介します。

要約

ES6からObjectのpropertyにfunctionを定義する場合の短縮構文である、メソッド定義構文(Method Definition)が実装されました。
この記法を使うことでVueのcomputed, methodsなどの内部で定義したfunctionを置き換えていきます。
メソッド定義 - JavaScript | MDN

var obj = {
  foo: function() {
    /* コード */
  },
  bar: function() {
    /* コード */
  }
};

var obj = {
  foo() {
    /* コード */
  },
  bar() {
    /* コード */
  }
};

なぜアロー関数ではなくこの記法にするのかは後述します。

それでは個別の事例ごとにNGパターンを踏まえつつES6スタイルに書き直してみましょう。

props

propsなどの内部のプロパティとして値を返すfunctionが定義されている場合

props: {
  colorCodes: {
    type: Array,
    default: function() {
      return ['#FFFFFF', '#F0F0F0'];
    }
  }
}

これは単純にアロー関数にできそうですね?

props: {
  colorCodes: {
    type: Array,
    default: () => {
      return ['#FFFFFF', '#F0F0F0'];
    },
  },
},

できました!しかし…
functionの内部でthisが参照されている時はどうでしょうか?

props: {
  colorCodes: {
    type: Array,
    default: function() {
      console.log(this); // VueComponentが参照できる
      return this.colorArray;
    },
  },
}

NGな書き方

前述のように単純にアロー関数に変換すると内部にthisが記述された時に参照できなくなってしまいます

props: {
  colorCodes: {
    type: Array,
    default: () => {
      console.log(this); // undefined
      return this.colorArray; // undefinedのプロパティを参照しようとしてエラーになる
    },
  },
}

OKな書き方

メソッド定義形式で書けば内部でthisが参照できます

props: {
  colorCodes: {
    type: Array,
    default() {
      console.log(this); // VueComponentが参照できる
      return this.colorArray;
    },
  },
}

computed, methodsなど

computedの内部でfunctionで定義されている場合

computed: {
  mySomething: function() {
    const something = this.getSomething();
    ...
  },
},

NGな書き方

単純にアロー関数に書き換えると、内部にthisが記述された時に参照できなくなります

computed: {
  mySomething: () => {
    const something = this.getSomething(); // undefinedのプロパティを参照しようとしてエラーになる
    ...
  },
},

OKな書き方

メソッド定義形式で書けば内部でthisが参照できます

computed: {
  mySomething() {
    const something = this.getSomething(); // thisでVueComponentが参照できる
    ...
  },
},

computedに限らずmethodsなどの中身も同様にメソッド定義形式に書き換えられます

methods: {
  getColorCodes: function() {
    return {
      startColor: this.colorCodes[0],
      endColor: this.colorCodes[1],
    };
  }
},

OKな書き方

methods: {
  getColorCodes() {
    return {
      startColor: this.colorCodes[0],
      endColor: this.colorCodes[1],
    };
  }
},

実例

Nuxtのサイトのサンプルがメソッド定義形式で書かれていました
Vuex ストア - NuxtJS

export default {
  fetch ({ store }) {
    store.commit('increment')
  },
  computed: mapState([
    'counter'
  ]),
  methods: {
    increment () {
      this.$store.commit('increment')
    }
  }
}

実例2

element-uiのサンプルもメソッド定義形式で書かれていました
Element - The world's most popular Vue UI framework

<el-collapse v-model="activeNames" @change="handleChange">
  ... 省略 ...
</el-collapse>
<script>
  export default {
    data() {
      return {
        activeNames: ['1']
      };
    },
    methods: {
      handleChange(val) {
        console.log(val);
      }
    }
  }
</script>

参考サイト

超素晴らしい解説のgistです。ほぼこれを参考にしています。
Clean up your Vue modules with ES6 Arrow Functions · GitHub

MDN公式のメソッド定義(Method definitions)リファレンス
メソッド定義 - JavaScript | MDN

本記事は個人ブログに投稿したものをベースにリライトしました
https://pisolino.hatenablog.com/entry/2020/01/24/020059

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