20200218のJavaScriptに関する記事は28件です。

RasDashを使ってみた

RasDashは「メモリやCPU使用率、センサの値などを表示するNode.js製ダッシュボード」です。
MOON GIFTさんの「RasDash - Raspberry Pi用のダッシュボード」という記事を見て、面白そうと感じたので、使ってみました。

導入

下記の手順で導入。最後のsystemctl enable RasDash.serviceは自動起動するために入力しただけなので、実行しなくても問題なし。

$ git clone https://github.com/sykeben/RasDash.git ~/RasDash
$ cd ~/RasDash/
$ sudo ./install_deps
$ sudo ./service_manager install
$ sudo ./service_manager start
$ sudo systemctl enable RasDash.service

表示

http://<Raspberry piのIPアドレス>:5808/dash/0をブラウザで表示。
各値はリアルタイムに反映される。
RasDashの画面

ポート変更

デフォルトでは、ポート番号5808へ接続している。
~/RasDash/config.jsonに記述された数値を変更すれば、他のポート番号に変更できる。

{
    "port": 5808
}

表示内容変更

ダッシュボードに載せる情報は下記のファイルで変更可能。
表示するボードの順番を修正:~/RasDash/views/pages/dash_0.ejs

さいごに

htmlやjavascript、cssの知識はもちろん、EJSというNode.jsのテンプレートエンジンに関する知識が必要なようです。

RasDashの他の項目をいじれるよう、暇を見つけていろいろ試してみます。

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

FileListは作れる!~file inputの中身も自由自在~

ふつうにvalueを書くことができない<input type="file">ですが、ブラウザによっては動的な設定もほぼ自由に行えることが判明しました。

<input type="file">へ値を設定する

入力するものがファイルである以上、<input type="file">に対してvalueは(空にするvalue = ''を除けば)使えないのですが、別ルートとしてfilesというプロパティがあります(MDN)。ここには選択したファイルのリスト(multipleでなければ1つだけ)が入ったFileListオブジェクトが入っていて、また代入することで設定が可能です。

FileListって、何?

ところが、filesに代入するものもFileListオブジェクトでなければならず、代わりにFileの配列を持ってきてもエラーになります。しかも、new FileListも動かないため、一見すると他の<input type="file">から持ってくる、ぐらいの動作しかできないようにも見えます。実際、昔はそうでした。

裏口から作る~DataTransfer

ここで登場するのがDataTransferです。これは、ドラッグアンドドロップで動かしているデータを持つためのオブジェクトです。ファイルを投げ込むような動作も存在する以上、このオブジェクトもfilesとしてFileListを持っています。

そして、どういうわけか、DataTransfernewできます。なので、DataTransfer経由でFileListをゼロから作ることが可能となります。

    const dt = new DataTransfer();
    dt.items.add(file);
    const list = dt.files;

なお、iOS(とIE)はこの方法に対応していませんので、要注意です(Can I use)。

動作例

<input type="file" multiple>で一気に選んだファイルを、1つ1つ<input type="file">に分配するような例を実装しました。

See the Pen DataTransfer経由でFilesを作る by Jkr2255 (@jkr2255) on CodePen.

その他

DataTransfer経由でFileListの作成が可能となったため、new FileListができないことが有害無益となってしまい、これも可能とする動きもあるようです。

参考リンク

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

Amazonで見ている本のタイトルを図書館のサイトで検索するChrome拡張の作り方

概要

Amazonで見ている本のタイトルを図書館のサイトで検索するChrome拡張を作ってみた。
image.png

はじめに

ヘビー図書館ユーザーなので、WEB予約を効率化したくて試行錯誤中。ブックマークレットで、効率化しようとしてある程度成功したけれど、ページ遷移を挟んだスクリプトは実行できなかった。なので、Chrome拡張使って試してみることにした。
自分のスキル的には、JavaScriptとHTMLなんとなくならわかる程度。

ブックマークレット初心者がWEBサイト入力を自動化するためにやったこと - Qiita

方針

とりあえず、公開はおいといて、自分のために動くものを作る。
Amazonのサイトで気になる本を表示してるときに、Chrome拡張のアイコンを押すと、図書館のサイトで検索した結果を表示するようにしたい。本読むとき、それなりにAmazonのレビューとか評価を気にするタイプなので。

やったこと

他の人のマネ

作り方、全然知らないので、調べた。
いくつか見たけど、ややこしそうやった。
一番シンプルそうな、このサイトに書かれてること試してみた。
Chrome拡張の作り方 (超概要) - Qiita

すんなり動いたので、これを真似して作ることにした。
content_scriptsっていうのを使ってる。
特定のサイトを開いたときに動作するスクリプト。

manifest.jsonってので、全体的な設定を書いて、JavaScriptのファイルで動作を書いたら良さそう。HTMLとかアイコンはなくても良さそう。

やりたいことするための調査

やりたいのは、Amazonのサイトを開いてるときに、アイコンをクリックしたら動作するイメージ。どうも、アイコンをクリックしたときに動作するpage_actionってのと、バックグラウンドで動作するbackgroundってのを設定したらよさそう。

Chrome拡張の開発方法まとめ その1:概念編 - Qiita

できたもの

試行錯誤して、処理の流れはこんな感じになった。

  1. バックグラウンドで、クリックを監視
  2. クリックされたら、Amazonのサイトでタイトルを取得して(|でタイトルとか著者名が区切られてる)、タイトルをローカルに保存して、図書館のサイトを開く
  3. 図書館のサイトが開かれたら、ローカルに保存してあるタイトルで検索する。

※ permissionsをエラーとか見ながら、適当に検索して設定した。
※ スクリプト間のデータ受け渡しはchrome.storage.local使った。他にも方法ありそう。
※ page actionのbrowser_styleも、style.jsがどうこうっていうエラーが出たので、適当に設定してみた。

ファイルはこれ(↓)で全部。

manifest.json
{
    "name": "Amazon2KobeLibrary",
    "version": "1.0.0",
    "manifest_version": 2,
    "description": "Amazonで見ている本のタイトルを神戸市図書館のサイトで検索",
    "background": {
        "scripts": [
            "background.js"
        ]
    },
    "page_action": {
        "default_title": "page action",
        "browser_style": true
    },
    "content_scripts": [
        {
            "matches": [
                "https://www.lib.city.kobe.jp/opac/opacs"
            ],
            "js": [
                "content_lib.js"
            ]
        }
    ],
    "permissions": [
        "storage",
        "activeTab"
    ]
}
background.js
(function(){
    chrome.tabs.onUpdated.addListener(function(tabId){
        chrome.pageAction.show(tabId);
    });

    chrome.pageAction.onClicked.addListener(function(tab){
      chrome.tabs.executeScript(null,{ file: "script.js"});
    });
})();
script.js
var title = document.title;
var title1 = title.split('|');
chrome.storage.local.set({ hold_title: title1[0] });
// alert('タイトル:' + title1[0]);
window.open('https://www.lib.city.kobe.jp/opac/opacs')
content_lib.js
chrome.storage.local.get("hold_title", function (items) {
    document.forms['search_form'].elements['keywords'].value = items.hold_title;
    var x = document.getElementsByTagName("form");
    var request = document.createElement('input');
    request.name = 'btn1';
    request.value = '検索';
    document.forms['search_form'].appendChild(request);
    document.forms['search_form'].submit();
});

拡張機能として追加

想像以上に簡単やった。
Extensionsを開いて、Load Unpackedをクリックして上記ファイルが入ったフォルダを選択するだけ。たぶん、Developer modeにチェック必要。はじめ、Pack extensionをクリックしてたけど、違った。
image.png

こんな感じで登録される。ソースコードを更新したときは、右下の方の更新ボタン押したら勝手に読み込んでくれた。ファイルのパスを覚えてるっぽい。たぶん。
image.png

残課題

普通に、図書館のサイトを開いたときも、勝手に遷移するようになった。どうしよ・・・。
Amazonからしか検索できない。微妙すぎる。検索窓にタイトル入れるまでにしといた方がましかも。
公開方法わからない。もうちょい、ちゃんと作ったら公開してもいいのかも。

おわりに

とりあえず、動くもの作れてよかった。ちょっと微妙ではあるけれど。
思ったより簡単やった。食わず嫌いよくない。

追記

妥協案として、検索窓にタイトルを入れて検索窓にフォーカス(カーソルで選択した状態)するところで止めてみた。悪くない気がする。

content_lib.js
chrome.storage.local.get("hold_title", function (items) {
    document.forms['search_form'].elements['keywords'].value = items.hold_title;
    document.forms['search_form'].elements['keywords'].focus();
    // var x = document.getElementsByTagName("form");
    // var request = document.createElement('input');
    // request.name = 'btn1';
    // request.value = '検索';
    // document.forms['search_form'].appendChild(request);
    // document.forms['search_form'].submit();
});

参考:
テキスト入力欄に最初からカーソルを入れる(フォーカスを与える)方法 - JavaScript TIPSふぁくとりー

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

ブックマークレット初心者がWEBサイト入力を自動化するためにやったこと

概要

自動化したいWEBサイトのHTMLを確認して、Bookmarkletを作ってみた。
検索窓に文字列を入れたり、POSTしたり、Amazonから本のタイトルコピーしたりできるようになった。デバッグツールの使い方も少し知ることができた。
今回、題材にしたサイト以外でも同じように対応できるはず。

はじめに

Bookmarkletを自分で作ってみた。今までは、検索して同じことしようとしてる人を見つけてコピペして登録してた。今回、やりたいことが見つからなかったので、自分で作ってみた。HTTPもHTMLもJavaScriptもなんとなく知ってるくらいだったけれど、なんとかなった。

やりたいこと

市の図書館のWEBサイトが微妙なので、もうすこし楽に(クリック少なめで)検索と予約したい。(一応書いておくと、WEBで予約できるの便利で活用してる。とても感謝してる。)

環境

ブラウザはChrome。
image.png

デバッグ環境

ここを参考にした。ちゃんとデバッグできる。すばらしい。
Chrome Dev Toolでブックマークレットを実行する - Qiita

ChromeのCtrl+Shift+Iで開くDeveloper Toolsを開く。
Consoleにコピペしてエンターキー。console.log()とか使える。エラーの行番号もでる。ブレークポイントも設定できる(らしい)。試してみると、すごく簡単。実際に使うときと同じChrome環境で動くので、安心感すごい。

image.png

自動化したいWEBサイトの中身を調べる

Developer Tools使ったり、Ctrl+uでHTMLのソースコード見たりして自動化したい部分のformとかinputを確認する。今回、対象にしたサイトが割と古いからか、HTMLにそのまま色々書かれてた。

ただ、自動化したかったサイトはauthenticity_tokenってのが毎回変わる。結構、やっかいやった。
検索がHTTP GETじゃなくてHTTP POSTなのも微妙やった。一度、このサイトを開いてauthenticity_tokenってのを確認してPOSTしないと検索できない。予約もおんなじようなシステム。Bookmarkletじゃ他のサイトから直接検索したり予約できなかった。残念。

サイトとHTMLは、こんな感じ↓
image.png

コードを短くする

GoogleのClosure Compilerってのを使うと短くしてくれる。デバッグもできる(らしい)。
image.png

Closure Compiler Service

登録

適当にブックマークとして登録。URL部分にコピペ
image.png

Bookmarklet

いくつか試したものを抜粋。

Amazonの本のタイトルを取得

HTMLを確認するとtitleに著者名とかが入ってて|で区切られてる。本のタイトルだけ取得して、コピーする(クリップボードに入れる)。
(ここで取得した本のタイトルを図書館のサイトの検索窓に入れたかったけど、あんまり自動化できなくて結局手動でやることにした。)

javascript:(
    function(){
      const title = document.title;
      var title1 = title.split('|');
      console.log('ページタイトル=%s\n', title1[0]);
      alert('ページタイトル:'+title1[0]);

      var tmp = document.createElement('p');
      var pre = document.createElement('p');
      pre.style.userSelect = 'auto';
      tmp.appendChild(pre).textContent = title1[0];
      document.body.appendChild(tmp);
      document.getSelection().selectAllChildren(tmp);
      document.execCommand('copy');
    }
)();

image.png

検索窓に文字列を挿入

formの名前を確認して、とりあえず適当にテキストを入力してみた。普通に動いた。
(結局使わなかった。クリップボードからペーストもできそうやったけど、あんまり自動化できてないしやめた。)

javascript:(
    function(){  
      document.forms['search_form'].elements['keywords'].value = 'test';
    }
)();

まじめにformとinputを設定してsubmit

formに紐づくinputを全部、自分で設定してPOSTするパターン。結構めんどい。
var request部分をコメントアウトしたら、最後にappendしたものしか送られていなかった。変数を再利用できてなさそう。JavaScriptってそういうもんなんかな?

javascript: (
    function postForm() {
        var form = document.createElement('form');
        form.method = 'POST';
        //    form.accept-charset = 'UTF-8';                                                                                          
        form.action = 'https://xxx';

        var request = document.createElement('input');
        request.name = 'y';
        request.value = 'z';
        form.appendChild(request);

        var request = document.createElement('input');
        request.name = 'a';
        request.value = 'b';
        form.appendChild(request);
        form.submit();
    }
)();

formを取得して、inputを追加してsubmit

HTMLにかかれているformを再利用する。一部ユーザ入力によって変わる部分があったので、そこだけ追加。formに名前とかIDがついていなかったので、何番目のformかを指定した。x[2]の部分。3番目。

javascript: (
    function postForm() {
        var x = document.getElementsByTagName("form");
        var request = document.createElement('input');
        request.name = 'commit';
        request.value = '予約';
        x[2].appendChild(request);
        x[2].submit();// Form submission                                                                                              
    }
)();

ページ遷移後のスクリプト

一度、POSTして新しいページに行ってから、別のスクリプトを実行したかったけれど、Bookmarkletじゃできなさそうやった。残念。Chrome拡張でなんとかしようかなぁ。

おわりに

作りたいもの駆動で勉強できてよかった。
デバッグツールがあったので、試しながら作れたのが良かった。
HTTPとかHTMLとかJavaScriptの知識がもうすこしあれば、もっとスムーズに作れるかもしれない。
でも、ちょっと自動化したいだけだったので、ちょうど良い感じで学べた気がする。
複数ページにまたがって自動化したかったけれど、調べた限りできないみたいやった。残念。
次は、Chrome拡張でも作ってみよっかな。

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

Reactにおけるstateのイミュータビリティ

この記事の目的

公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。

関連するQiita記事

Reactのざっくり概要
Reactコンポーネントとは
Reactコンポーネント間の値の受け渡し
Reactコンポーネントでstateをリフトアップ

参考資料

ドキュメント

公式ドキュメント

チュートリアル

公式チュートリアル

※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough Tutorial

Reactにおけるstateのイミュータビリティ

イミュータビリティとは

変更可能なデータオブジェクトの値を変更するときは、以下の2つの方法がある。

  1. 対象のデータオブジェクトを直接変更する → mutable(変異)
  2. 対象のデータオブジェクトをコピーし、コピーしたデータオブジェクトを変更してから対象のデータオブジェクトをコピーオブジェクトで入れ替える(書き換えではなく入れ替え) → immutable (不変)

Reactにおけるイミュータビリティ

Reactのstateを変更する際はsetState()という関数を使用するが、この関数はイミュータブルの手法で値を変更する必要がある。

コードの例

以下のBoardコンポーネントを例に説明する。

board.js
class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();  //ポイント1
    squares[i] = 'X';  //ポイント2
    this.setState({squares: squares});  //ポイント3
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

BoardコンポーネントのhandleClick関数(Squareコンポーネントでマスをクリックされたときに動作する関数)においてsetState関数を使用してstateの置き換えをしているが、その中でイミュータブルな処理が行われている。

注目すべきポイント

見るべきポイントは以下の3点。

  1. state.squaresの値を変更したいが、まずはconst squares = this.state.squares.slice();state.squaresのコピーを作成する
  2. コピーしたsquaresに'X'を格納する
  3. this.setState({squares: squares});で、state.squaresをコピーしたsquaresで置き換える → 書き換えではなく置き換えをしている

オブジェクトをコピーする方法

JavaScriptでオブジェクトのコピーを作成する方法はいろいろあるが、「配列をコピーする例」と「オブジェクトのプロパティをコピーする例」を示す。

配列をコピーする例

Array.prototype.slice()

ArrayCopy.js
const newSquares = this.state.squares.slice();
newSquares[i] = 'X';
this.setState({squares: squares});

オブジェクトのプロパティをコピーする例

Object.assign()

ObjectAssign.js
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
this.setState({player: newPlayer});

なぜイミュータブルであることが重要なのか

なぜ、書き換えによるmutable(変異)な値の変更を行わず、オブジェクトの入れ替えによってimmutable (不変)な値の変更を行うのが重要なのか、理由を3つ説明する。

複雑な処理を簡略化できる

stateをimmutable (不変)の方針でオブジェクトによって置き換えると、値の変更は「オブジェクトの置き換えをしていない」か「オブジェクトの置き換えをしている」かの2択になる。
ある一部分だけ値を書き換えたような中途半端な状態にはならない。

image.png

対して、stateをmutable(変異)の方針で直接書き換えると、ある一部分だけ書き換わった中途半端な状態が存在する可能性がある。
仮に、途中で書き換えの処理が失敗した場合、「一部分だけ書き換わった中途半端な状態」を元に戻す複雑な処理が必要となってしまう。

image.png

また、immutableで「オブジェクトの置き換えをした結果」を履歴として保持すれば、過去の状態に戻す処理もmutableよりも簡単に実現することができる。

image.png

変更の検出が容易

mutableで値を直接書き換えた場合、stateが参照するオブジェクトに変化がないため、state内のどのオブジェクトに変化があったのかをReactは検知することができない。
そうなると、Reactは仮想DOM内の新旧差分を全走査することになり、再描画が遅くなる。

image.png

対して、imutableでstateのオブジェクトを入れ替えた場合、stateが参照するオブジェクトに変化があるため、state内のどのオブジェクトに変化があったのかをReactは検知できる。
ReactはViewの再描画をする際に、仮想DOMの新旧比較を変化があったオブジェクトでのみ行うため、再描画が高速になる。

image.png

再描画のタイミングを決めやすい

上記で書いたとおり、imutableなオブジェクトはstate内のどこで変更があったのかを検知できる。
それを利用して、再描画が必要ない変更であれば再描画せず、再描画が必要な変更であれば差描画するといったことが可能となる。
それによって、不要な再描画を押さえて描画パフォーマンスを向上させることができる。

image.png

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

2020年 2月のJestセットアップ

ありとあらゆるリファレンスを読みまくってまとめました。
一番スッキリJestをセットアップしよう。

Jestとは

Facebookが開発したJavaScriptの単体テストフレームワーク。
Node上で動作するため、手軽にテストを実行できる事が特徴。

Jest · ?快適なJavaScriptのテスト

作業環境

  • Laravel 5.7
  • Docker on Mac
  • Vue.jsとかはLaravel mixのもの

セットアップ

Jestとかをnpm install

npm install --save-dev jest babel-jest @babel/core babel-core@bridge @babel/preset-env vue-jest @vue/test-utils

それぞれのひとこと説明

jest
Jest本体

babel-jest
babelとjestを連携させるもの

@babel/core
Babel本体の最新版
Babelは八百万のJavaScript構文を一定のバージョンと互換のある構文に変換するもの。
Nodeでは最新のJavaScript構文(import / exportなど含め)が動作しません。
そのため、Babelを使ってトランスパイルしてあげます。

babel-core@bridge
vue-jest@babel/coreではなく、babel-coreを参照してしまうらしく、これを解決するためにインストール。
Babel 7.x系だけど名前はbabel-coreなBabel本体。

@babel/preset-env
山ほどあるBabelの構文変換に関わるライブラリをいい感じに自動的に使ってくれるもの。
参考 : babel-preset-envを簡単にさわってみた。 - Qiita

vue-jest
vueをjestでテストできるようにするもの

@vue/test-utils
Vueのテストを書きやすくしてくれるユーティリティ

Babelのバージョンに注意

Babel 7.xからプラグインのプレフィックスとして@babel/がつくようになりました。
ECサプリに入ってるBabelは7系なのでプラグインを追加する時、@babel/から始まっているか注意してください。

6.x以前のプラグインを使うとうまく動かない事があるらしい

設定ファイルをつくる

babel.config.js (.babelrc)

Babelの設定ファイルです。
babel.config.jsでも.babelrcでもいいのでソースルートに配置します。おそらく両方あるのはよろしく無いのでどちらか片方にしましょう。

babel.config.js
module.exports = {
    presets: [
        ['@babel/preset-env', {modules: false}]
    ],
    env: {
        test: {
            presets: [
                ['@babel/preset-env', {targets: {node: 'current'}}]
            ]
        }
    }
};

jest.config.js

どんなファイル名のものをテスト対象とするか、どのファイル名をどのモジュールで変換するか、などを設定できます。

jest.config.js
module.exports = {
    transform: {
        '.*\\.(vue)$': '<rootDir>/node_modules/vue-jest',
        '^.+\\.js$': '<rootDir>/node_modules/babel-jest'
    },
    moduleFileExtensions: [
        'js',
        'vue'
    ],
};

package.json (既存のものを編集)

Jestの起動コマンドを書き足します。
NODE_ENV=testとすることでNodeへテスト環境であることを明示しています。

package.json
{
    "private": true,
    "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "npm run development -- --watch",
        ... ,
        "test": "NODE_ENV=test jest"  <-追記
    },
    "devDependencies": { ... },
    ...
}

テストを書く

テストケースを作ってみましょう。

こんなファイルをテスト対象にしてみました。
カリー化してある2つの数字を足すだけの関数です。

ExampleSum.js
export default{
    sum(x){
        return (y){
            return x + y;
        }
    }
}

これをテストするスクリプトはこんな感じ。

example.test.js
import ExampleSum from "path/to/ExampleSum";

test('test sum()', ()=>{
    expect(ExampleSum.sum(1)(2)).toBe(3);
}

実行してみよう

npm testとターミナルへタイプしてJestを動かしてみよう。
--でファイル名を連結することでそのファイル名と部分一致するファイルのみをテスト対象としてくれます。

npm test -- example.test.js

そのほかのCLIオプションは Jest CLI Options · Jest へ。

async/awaitもテストできる

PromiseExample.js
export default {
    sleep() {
        return new Promise(resolve => {
            setTimeout(() => {
                resolve();
            }, 500);
        });
    }
}
test('test async', async () => {
    await PromiseExemple.sleep();
    expect(true).toBe(true);
});

:smiley: :tada:

Vueのフロントエンドテストもかけます

ガイド | Vue Test Utils

ExampleComponent.vue
<template>

</template>

<script>
    export default {
        name: "ExampleComponent",
        methods: {
            sum(x, y) {
                return x + y;
            }
        }
    }
</script>

<style scoped>

</style>
component.test.js
import ExampleComponent from "./models/ExampleComponent";

const {mount} = require("@vue/test-utils");

test('Vue component test', () => {
    const component = mount(ExampleComponent);
    expect(component.isVueInstance()).toBeTruthy();
    expect(component.vm.sum(1, 2)).toBe(3);
});

component.vmが僕らのよく知るVueコンポーネントインスタンスです。
DOMを操作してフォーム入力をしたり、クリックしたりもできます。


@babel/preset-envがびっくりするほど便利だったことがわかった。

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

[python] コールバック関数(引数に関数を渡す)

javascriptとpython

javascript
const sayHello = word => console.log(word);
const func = callback => {
  console.log('ここはsayHelloが呼ばれたあとに実行される');
  return callback;
 }

func(sayHello('hello'));
実行結果
hello
ここはsay_somethingが呼ばれたあとに実行される
python
def say_something(word):
    print(word)

def func(callback):
    print('ここはsay_somethingが呼ばれたあとに実行される')
    return callback 

func(say_something('hello'))
実行結果
hello
ここはsay_somethingが呼ばれたあとに実行される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactコンポーネントでstateをリフトアップ

この記事の目的

公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。

関連するQiita記事

Reactのざっくり概要
Reactコンポーネントとは
Reactコンポーネント間の値の受け渡し

参考資料

ドキュメント

公式ドキュメント

チュートリアル

公式チュートリアル

※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough Tutorial

Reactコンポーネントでstateをリフトアップ

stateを子コンポーネントに持たせる場合の問題点

Reactコンポーネント間の値の受け渡しでは、Squareコンポーネントのstateで値を保持していたが、このままだとBoardコンポーネントで9つあるSquareコンポーネントの値を取得しようとした場合に、9つのSquareコンポーネントにそれぞれ問い合わせをする必要がある。

image.png

そのようなコードもReactは許容するが、コードがわかりにくくなりバグを発生しやすく、リファクタリングも難しくなるので、Reactは「stateのリフトアップ」を推奨している。

stateのリフトアップとは?

各Squareコンポーネントで持っていたstateをBoardコンポーネントに移し、各Squareコンポーネントのpropsに対してstateの値を渡すようにする。

image.png

このように、SquareコンポーネントにあったstateをBoardコンポーネントに移すようなリファクタリングを、「stateのリフトアップ」と呼ぶ。

stateのリフトアップをするコード例

Squareコンポーネントのstateをリフトアップすると、以下のようなコードになる。

コードの例

board.js
class Square extends React.Component {  //ポイント2
  render() {
    return (
      <button
        className="square"
        onClick={() => this.props.onClick()} 
      >
        {this.props.value}  //ポイント4
      </button>
    );
  }
}

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {  //ポイント1
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}  //ポイント3
        onClick={() => this.handleClick(i)} 
      />
    );
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

注目すべきポイント

stateのリフトアップを見る上でのポイントは以下の4点。

  1. Boardコンポーネントにstateを持たせる
  2. Squareコンポーネントからstateを除去
  3. Boardコンポーネントのstateを、Squareコンポーネントのpropsに渡す
  4. Squareコンポーネントは、propsで受け取ったBoardコンポーネントのstateを表示する

子コンポーネントから親コンポーネントのstateを変更する例

Boardコンポーネントから見て、Squareコンポーネントは子コンポーネントになるので、BoardコンポーネントとSquareコンポーネントには親子関係があることになる。

Squareコンポーネント(子コンポーネント)からBoardコンポーネント(親コンポーネント)のstateを変更したい場合、子コンポーネントから親コンポーネントのstateを直接変更することはできない。

なぜなら、コンポーネントのstateはプライベートなフィールドなので外部から直接値を変更することはできないためである。

そのため、SquareコンポーネントからBoardコンポーネントのstateを変更する場合は、以下のように親コンポーネントから子コンポーネントにstateを変更する関数をpropsとして渡すことになる。
子コンポーネントでは、propsで受け取った親の関数を通して、親のstateを変更する。

コードの例

board.js
class Square extends React.Component {
  render() {
    return (
      <button
        className="square"
        onClick={() => this.props.onClick()}  //ポイント3
      >
        {this.props.value}
      </button>
    );
  }
}

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {  //ポイント1
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}  //ポイント2
      />
    );
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}{this.renderSquare(1)}{this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}{this.renderSquare(4)}{this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}{this.renderSquare(7)}{this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

注目すべきポイント

子コンポーネントから親コンポーネントのstateを変更する上でのポイントは以下の3点。

  1. 親コンポーネント(Boardコンポーネント)にstateを変更する処理(今回の場合はhandleClick関数)を持たせる
  2. 子コンポーネント(Squareコンポーネント)にstateを変更する処理(今回の場合はhandleClick関数)をpropsとして渡す
  3. 子コンポーネント(Squareコンポーネント)から親コンポーネント(Boardコンポーネント)のstateを変更するときは、propsで渡された親コンポーネント(Boardコンポーネント)の処理(今回の場合はhandleClick関数)を呼び出す

stateのリフトアップを検討するときのポイント

以下の流れで実装とリファクタリングを区別し、リファクタリングでstateのリフトアップを行うのが好ましい。

  1. 実装

    1. 親子関係にあるコンポーネントであっても、子コンポーネントに”state”と”stateを変更する処理”がある状態で実装を進める
    2. 実装が完了し、テストで動作を確認する
  2. リファクタリング

    1. 子コンポーネントにある”state”を、親コンポーネントにリフトアップする
    2. 子コンポーネントにある”stateを変更する処理”を、親コンポーネントにリフトアップする
    3. 子コンポーネントのpropsに、親コンポーネントの”state”と”stateを変更する処理”を渡す
    4. 子コンポーネントは、propsで渡された”state”と”stateを変更する処理”を使って、子コンポーネント側の変更を親コンポーネントのstateに格納する
    5. リファクタリングが完了したら、テストで動作を確認する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactコンポーネント間の値の受け渡し

この記事の目的

公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。

関連するQiita記事

Reactのざっくり概要
Reactコンポーネントとは

参考資料

ドキュメント

公式ドキュメント

チュートリアル

公式チュートリアル

※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough Tutorial

Reactコンポーネント間の値の受け渡し

Reactコンポーネント間で値の受け渡しをするには、propsとstateというものを使用する。
ReactコンポーネントとはのSquareコンポーネントとBoardコンポーネントにpropsとstateを加えて説明する。

propsの追加

propsとは、コンポーネントのプロパティ(引数)のことである。コンポーネントに外から値を引き渡したい場合、propsを使用する。

コードの例

Squareコンポーネントにpropsを追加すると以下のようなコードになる。

board.js
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}  //ポイント2
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;  //ポイント1
  }

  render() {
    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

注目すべきポイント

ポイントは以下の2点。

  1. BoardコンポーネントのrenderSquare関数でSquareコンポーネントを返す際に、Squareコンポーネントにvalue={i}を渡している
  2. Squareコンポーネントは渡されたvalueをthis.props.valueで取り出している

これによって、Squareコンポーネントを描画するときにvalue={i}で0〜9の値が渡されることになり、渡された値はSquareコンポーネントのタグに挟まれる形で{this.props.value}の部分で表示されることになる。

実行結果

実行結果としては以下のようになる。
(実際にブラウザで表示させたい場合はReact公式チュートリアルのCode Penで確認)

image.png

stateの追加

stateとは、コンポーネント内で値を保持するための記憶領域を指す。コンポーネント内で値を保持したい場合にstateを使用する。

コードの例

Squareコンポーネントにstateを追加すると以下のようなコードになる。

board.js
class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {  //ポイント1
      value: null,
    };
  }

  render() {
    return (
      <button
        className="square"
        onClick={() => this.setState({value: 'X'})}  //ポイント2
      >
        {this.state.value}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

注目すべきポイント

ポイントは以下の2点。

  1. Squareコンポーネントのコンストラクタで、this.statevalue: nullを含むオブジェクトを格納している(コード上で明示されていないが、Squareコンポーネントはthis.stateというフィールドを持っている)
  2. SquareコンポーネントのボタンがクリックされたときにonClickイベントが発生し、this.setState({value: 'X'})が実行される。これによってSquareコンポーネントのthis.state{value: 'X'}が格納される

実行結果

実行結果としては以下のようになる。

(実際にブラウザで表示させたい場合はReact公式チュートリアルのCode Penで確認)

Squareコンポーネントをクリックする前

Squareコンポーネントのstateには、value: nullが格納されている。
image.png

Squareコンポーネントをクリックした後

Squareコンポーネントのstateには、value: "X"が格納されている。
image.png

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

Reactコンポーネントとは

この記事の目的

公式チュートリアルで三目並べを作ってもらうのが一番理解できるが、その中でも特に日本語で説明しておいたほうがよさそうなポイントを纏めている。
本資料を読んだ後に、チュートリアルを両方やってもらったほうがより理解が深まる。

関連するQiita記事

Reactのざっくり概要

参考資料

ドキュメント

公式ドキュメント

チュートリアル

公式チュートリアル

※余力があれば、以下のチュートリアルも行うことを推奨する
Getting Started with React - An Overview and Walkthrough Tutorial

Reactコンポーネントとは

ReactはUIを「コンポーネント」という部品単位で構築する。

1つのコンポーネントを描画する例

例えば、以下のような四角形のボタンを描画する場合を考える。
image.png

これをReactコンポーネントで定義すると、以下のようになる。(スタイルは省略している)

square.js
class Square extends React.Component {
  render() {
    return (
      <button className="square"></button>
    );
  }
}

SquareというReactコンポーネントを定義しており、render関数で描画内容をreturnしている。

このときというHTMLタグをreturnしているように見えるが、これはJSX記法であり、以下のようにJSXを使わずにReact.createElementで記述した場合と等価である。

square.js
class Square extends React.Component {
  render() {
    return (
      React.createElement(
      'button',
      {className: 'square'}
      );
    );
  }
}

複数のコンポーネントを描画する例

上記のSquareを更に3×3のマス目でボタンを並べて描画する場合を考える。
image.png

これをReactコンポーネントで定義すると、以下のようになる。(スタイルは省略している)

board.js
class Square extends React.Component {
  render() {
    return (
      <button className="square"></button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

新たに加わったBoardというReactコンポーネントは、Squareコンポーネントを9つ組み合わせて3×3のマス目を実現している。

image.png

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

Ruby と JavaScript でライフゲームを書いた

ハッカーになろう模造クリスタル『ゲーム部』2で言及されてたライフゲームが面白そうだったので書いてみた。ライフゲームはセル平面上(要は方眼紙のこと)で生命をシミュレートするモデルです。

ライフゲームのルール

  • セルには生と死の二つの状態がある。
  • 時間(世代)が経過すると、あるルールに従ってセルの状態が変わる。
  • あるルールとは次の4つ。
    • 過疎による死: 自身が生で、周りの8セルのうち1セル以下が生のとき、次の世代で死ぬ。
    • 生存: 自身が生で、周りの8セルのうち2〜3セルが生のとき、次の世代で生き続ける。
    • 過密による死: 自身が生で、周りの8セルのうち4セル以上が生のとき、次の世代で死ぬ。
    • 誕生: 自身が死で、周りの8セルのうち3セルが生のとき、次の世代で生になる。

コードで表現しやすそうなルールだ。疑似コードで書いてみるとこんな感じ?

class Cell
  def next_alive?
    alive_count = 周囲.count { |item| item.alive? }

    # 過疎
    return false if self.alive? && alive_count <= 1
    # 生存
    return true if self.alive? && 2 <= alive_count && alive_count <= 3
    # 過密
    return false if self.alive? && 4 <= alive_count
    # 誕生
    return false if self.dead? && alive_count == 3

    false
  end
end

で、これを1世代ごとに全てのセルに対してチェックすればいけそう。

Ruby で実装

https://github.com/oieioi/lifegame.rb
こんな感じに動きます。JoyDivisionっぽい見た目になった。
lifegame.rb.gif

上に示した次世代の生死判定はこんな感じのコードになりました。

lib/lifegame/game.rb#L47-L78
    # あるセルが次のターンに生きてるか確認する
    def next_alive?(x, y)
      target = self[x, y]

      # 隣接セルの座標
      adjoining = [
        [x - 1, y - 1],
        [x    , y - 1],
        [x + 1, y - 1],
        [x - 1, y],
        # [x    , y],
        [x + 1, y],
        [x - 1, y + 1],
        [x    , y + 1],
        [x + 1, y + 1],
      ]

      dead_or_live = adjoining.map { |point|
        n_x, n_y = point
        self[n_x, n_y]&.alive?
      }.compact

      live_count = dead_or_live.count { |live| live }

      if target.dead?
        # 3の時のみ誕生
        live_count == 3
      else
        # 2,3の時のみ生き残る
        (2..3).include?(live_count)
      end
    end

JavaScript (with React)で実装

CLI だとインタラクティブなのがやりづらいのでJSでも書いてみた。

https://github.com/oieioi/lifegame.js
デモはこちら: https://dreamy-lumiere-0f384d.netlify.com/

画面収録 2020-02-04 14.36.33.gif

こちらは全てのセルを二次元配列で受け取って新しい生死の二次元配列を返すようにした。

src/lib/LifegameLogic.js#L1-L18
function nextCells(cells) {
  return cells.map((line, x)=> {
    return line.map((alive, y) => {
      // 周囲を調べる
      const aliveCount = getAdojoiningPositions(x, y).filter((position) => {
        const [x,y] = position;
        if (!cells[x]) return false;
        return cells[x][y]
      }).length
      if (alive) {
        // 周囲の生き残りが2,3のとき生存
        return aliveCount === 2 || aliveCount === 3;
      } else {
        return aliveCount === 3
      }
    });
  });
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Enumerable.Linq の各メソッドを JavaScript ではどう書くか?(メソッドの対応)

はじめに

内容は随時補完されます!
.NET の Linq に慣れ親しんでいる方向けに、JavaScriptでの似たメソッドへの対応を一覧化したものです。

Linq でいう「シーケンス」を、Javascript でいう「索引付きコレクション(配列など、インデックス値で順序付けされたデータのコレクション)」「配列風オブジェクト1」「キー付きコレクション(反復可能オブジェクト2)」のすべて、またはいずれかとほぼ同じとみなして対応させています。

一覧

まったく同じ機能とは限らないので注意です。(元となる配列自体を書き換える(*マークつき)、メソッドチェーンにならない、同値判定・False判定法が違うなど)

Linq JavaScript JSでの機能
Aggregate reduce

reduceRight
配列の各要素に対してreducer関数を実行して単一の値を返す

配列の各要素に対して右から左へreducer関数を実行して単一の値を返す
All every 配列のすべての要素が、与えられた関数によって実行されるテストに通るかテストする
Any some 配列の少なくとも1つの要素が、渡された関数によって実施されるテストに通るかテストする
Append *push

concat
配列自体の末尾に1つ以上の要素を追加する

配列に他の配列や値をつないだ新しい配列を返す
Average reduce で代用など
Cast map で代用など
Concat concat 配列に他の配列や値をつないだ新しい配列を返す。2以上の配列や値も一度に指定可能
Contains includes 配列に特定の要素が含まれているかを返す
Count length
Distinct Set オブジェクト生成など
Empty [ ]
First find 配列内の、提供されたテスト関数を満たす最初の要素の値を返す。配列すべての要素にテスト関数が呼び出されるので注意。
FirstOrDefault
Last sort と find で代用など
LastOrDefault
Max Math.max

reduce で代用など
引数として与えた複数の数の中で最大の数を返す
Min Math.min

reduce で代用など
引数として与えた複数の数の中で最小の数を返す
OrderBy *sort 配列自体の要素をソートする
OrderByDescending
Prepend *splice 配列自体の既存の要素を取り除いたり、置き換えたり、新しい要素を追加したりする
Reverse *reverse 配列自体の要素を反転させて返す
Select map 配列のすべての要素に対して与えられた関数を実行し、その結果からなる新しい配列を生成して返す
SelectMany flat

flatMap
すべてのサブ配列の要素を指定した深さで再帰的に結合した新しい配列を生成して返す

マッピング関数を使用してそれぞれの要素をマップした後、結果を新しい配列内にフラット化して返す
Skip *splice 配列自体の既存の要素を取り除いたり、置き換えたり、新しい要素を追加したりする
SkipLast *splice 配列自体の既存の要素を取り除いたり、置き換えたり、新しい要素を追加したりする
SkipWhile filter で代用など
Sum redure で代用など
Take subarray

slice
指定される範囲から同じ要素の型をもつ新しい配列を生成して返す

配列の一部をシャローコピーして新しい配列オブジェクトを返す
TakeLast
TakeWhile filter で代用など
ToArray from 「配列風オブジェクト1」や「反復可能オブジェクト2」から新しい浅いコピーのArrayインスタンスを生成して返す
Where filter 各配列要素に対して、与えられたテスト関数を実行し、合格したすべての配列要素からなる新しい配列を生成して返す

対応例

Select - map (配列添字情報つき)

C#
var req = new[] { 160, -2, 0, 5 };
var res = req.Select((s, i) => s * i);
// -> { 0, -2 , 0, 15 }
JavaScript
let req = [16, -2, 0, 5];
var res = req.map(function(s, i) { return s * i; });
// -> [ 0, -2, 0, 15 ]

補足

linq.js

便利なライブラリがあるようです。

ジェネレーター関数

.NET でいう IEnumerable<T>を戻り型とする関数と似た概念があり、function*と記載することで、yield (C# の yield return) や yield* を使用できます。

JavaScript
function* OneTwoThree(s) {
    yield "1" + s;
    yield "2" + s;
    yield "3" + s;
}

for (let v of OneTwoThree("")) { console.log(v); }
for (let v of OneTwoThree("")) { console.log(v); }
// ->
// 1個
// 2個
// 3個
// 1本
// 2本
// 3本

さいごに

間違いのご指摘、便利な書き方、ぜひぜひお寄せ下さいませ!

参考情報 ありがとうございます!


  1. 配列風オブジェクト (length プロパティおよびインデックス付けされた要素を持つオブジェクト (Array.from - MDN より) 

  2. MapSet のような要素を取得するオブジェクト(Array.from - MDN より) 

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

【Vue.js】vue-image-lightbox で画像ギャラリーを作成!

はじめに

Googleドライブの画像表示みたいなUIを作りたいと思っていたところ、vue-image-lightbox を使うと
簡単に実現できたので、最低限の実装方法を残したいと思います。

vue-image-lightboxとは?

サムネイルから、画像を拡大表示したり、スライドショーにしたりするための機能を提供しているライブラリです。

参考情報

今回の記事で実現した内容

以下の画像のようになるところをゴールとします。
記事中は主にライブラリに関係した部分を書いてます。ソースは参考情報から見てみてください。
画面収録 2020-02-18 12.19.11.mov.gif

環境

  • Vue.js 2.5.10
  • vue-cli 3.7.0

実装(準備編)

  • 環境構築
    vue createコマンドでプロジェクトを作成していきます。
    プロジェクト名は任意で入れてください。
    セットアップにあたり聞かれる質問も任意で答えていただいて構いません。
vue create image-app(プロジェクト名を入れてください)
cd image-app
  • 必要なライブラリのインストール
yarn add vue-image-lightbox vue-lazyload

※画像の遅延読み込み(Webページを先に表示して画像は読み込んだら表示させること)が必要になるため
vue-lazyloadも併せてインストールしています。

  • CSSのインポート
    まず、app.scssにvue-image-lightboxのCSSプロパティを読み込みます。
app.scss
@import '../../../node_modules/vue-image-lightbox/dist/vue-image-lightbox.min.css';
  • vue-lazyloadのインポート
    画像の遅延読み込みのため、main.jsにvue-lazyloadをインポートしていきます。
main.js
import VueLazyLoad from 'vue-lazyload';
Vue.use(VueLazyLoad);

実装(コンポーネント作成)

vue-image-lightboxを使用したコンポーネントを作成します。
CSSはBuefy(BulmaベースのUIコンポーネントライブラリ)を使い、サムネイルだけCSSを当てています。
要点は以下の通りです。

  • import文でvue-image-lightboxを読み込みます。
  • 画像表示は配列(images)を用意してv-forでimg要素を生成し、v-lazyで遅延読み込みしています。
  • img要素をクリックするとopenGalleryメソッドを発火させることでクリックした画像がギャラリー表示されます。  openGalleryメソッドはvue-image-lightboxのshowImageメソッドに配列(images)のインデックスを渡しています。
  • ImageLightBox要素で指定しているプロパティshow-light-boxがtrueだとブラウザにページを読み込んだタイミングでギャラリー表示されます。
  • show-captionがtrueにすると、ギャラリー表示時に画像オブジェクトに指定されたcaptionが表示されます。
LightBox.vue
<template>
    <div>
        <div class="media">
            <div class="column">
                <div class="columns is-multiline">
                    <div v-for="(image, index) in images" :key="image.id" class="column is-one-quarter has-text-centered">
                        <img v-lazy="image.src" @click="openGallery(index)" class="thumbnail" />
                        <p>{{ image.title }}</p>
                    </div>
                </div>
                <ImageLightBox ref="lightbox" :images="images" :show-light-box="false" :show-caption="true"></ImageLightBox>
            </div>
        </div>
    </div>
</template>

<script>
import ImageLightBox from 'vue-image-lightbox';

export default {
    name: 'LightBox',
    components: {
        ImageLightBox
    },
    data() {
        return {
            images: [
                {
                    thumb: 'https://www.pakutaso.com/shared/img/thumb/YAMAhokkaido015_TP_V.jpg',
                    src: 'https://www.pakutaso.com/shared/img/thumb/YAMAhokkaido015_TP_V.jpg',
                    title: '朝日が昇る摩周湖(北海道川上郡弟子屈町)',
                    caption: '朝日が昇る摩周湖(北海道川上郡弟子屈町)',
                    id: 1
                },
                {
                    thumb: 'https://www.pakutaso.com/shared/img/thumb/YAMAhokkaido032_TP_V.jpg',
                    src: 'https://www.pakutaso.com/shared/img/thumb/YAMAhokkaido032_TP_V.jpg',
                    title: '水鏡の青い池(北海道川上郡美瑛町白金)',
                    caption: '水鏡の青い池(北海道川上郡美瑛町白金)',
                    id: 2
                },
                {
                    thumb: 'https://www.pakutaso.com/shared/img/thumb/KMKC428D354_TP_V.jpg',
                    src: 'https://www.pakutaso.com/shared/img/thumb/KMKC428D354_TP_V.jpg',
                    title: '悪天候の上高地と大正池',
                    caption: '悪天候の上高地と大正池',
                    id: 3
                }
            ]
        };
    },
    methods: {
        openGallery(index) {
            this.$refs.lightbox.showImage(index);
        }
    }
};
</script>
<style lang="scss" scoped>
img.thumbnail {
    height: 100px;
    cursor: pointer;
}
</style>

最後に

画像のギャラリー表示をやりたくなった時になかなかvue-image-lightboxに行きつけなかったのですが、
vue-image-lightboxを見つけてから実装まではスムーズにいったので記事にしてみました。
ぜひお試しください!

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

Electronでの印刷時に余白をピッタリ調整する

モチベーション

ElectronではwebContents.print()関数やwebContents.printToPDT()関数を使って簡単に印刷やPDF作成ができます。この時、CSSで印刷サイズをmm等の実物長さで指定することで、実際の印刷時の各種要素の大きさを指定できるので便利です。が、注意しないと余分な余白が入ってしまいます。経験則からくる注意点を情報共有します。

注意点

  • CSSでpaddingmarginを正しく設定する。特に設定しない時は、明示的に0mmを設定すること。デフォルトでは0mmでないことがある。
  • body要素にもpaddingmarginを明示的に設定する事。実際に余分な余白が消えずに悩みましたが、body要素のmarginがデフォルトで0mmではなかったことが原因でした。
  • 印刷時だけ設定したいCSSは@media print項目で設する

body要素の中にdiv要素があり、その中に文章が書かれている場合のCSS記述は次のようになります。

@media print{
  body{
    padding: 0mm;  
    margin: 0mm;   /* これが無いと余分な余白が入る */
  }
  div{
    padding: 0mm;
    width:180mm;   /* 用紙幅はA4(210mm)を仮定して余白を引いたもの*/
    margin-left: 15mm;   /*左右の余白15mm*/
    margin-right: 15mm;
    margin-top: 20mm;   /*上下の余白20mm*/
    margin-bottom: 20mm;
  }
}

雑感

Electronの印刷はとても楽でいいですね。CSSで微調整できるところも素敵です。すごい時代になってますね。

子供の頃、親父にPCを買ってもらった際、弟と協力して年賀状印刷ソフトを作ったのを思い出しました。当時はVisual Basic6で印刷物の位置合わせをするのに苦労しました。

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

モダンブラウザとElectronでの印刷時に余白をピッタリ調整する

モチベーション

ElectronではwebContents.print()関数やwebContents.printToPDT()関数を使って簡単に印刷やPDF作成ができます。この時、CSSで印刷サイズをmm等の実物長さで指定することで、実際の印刷時の各種要素の大きさを指定できるので便利です。が、注意しないと余分な余白が入ってしまいます。経験則からくる注意点を情報共有します。

※2020/2/19:色々試行錯誤して、@pageとの関連、print()printToPDF()の違いなど大幅に改定しました。

最重要注意点

  • ChromeおよびChromium系ブラウザ、Electron(7.1.9~8.0.1で確認)において、@page内に記述したmargin, margin-left, margin-right,margin-top,margin-bottomについては、単位の変換におそらくbugがあり、設定値よりも1.5~1.8倍程度大きく設定されてしまう。
    • 確認したversion(これ以外も確認していないだけでおそらくbugがある)
    • Chrome: 80.0.3987.106
    • Electron: 7.1.9~8.0.1
    • Edge (Chromium): 80.0.361.54
  • 回避策として、@page内ではmargin系の設定値の 単位として%を使う。
  • Firefox、Edge(Legacy)では%以外の単位も問題なく動く。
  • margin-topmargin-bottom%を使う際は、ブラウザによって解釈が異なる
    • Chrome, Firefox, Edge(Chromium):ページ横幅に対する割合を設定する
    • Edge(Legacy):ページ縦幅に対する割合を設定する

注意点(試行錯誤の結果)

  • CSSでpaddingmarginを正しく設定する。特に設定しない時は、明示的に0mmを設定すること。デフォルトでは0mmでないことがある。
  • body要素にもpaddingmarginを明示的に設定する事。実際に余分な余白が消えずに悩んだが、body要素のmarginがデフォルトで0mmではなかったことが原因だった。
  • 印刷時だけ設定したいCSSは@media print項目で設する。
  • ブラウザ上の表示はどれだけ長くても縦に長い1枚ページであるのに対し、印刷時は縦幅も固定で複数ページに分割される。
  • 印刷時のページ毎のマージンは、HTML要素のプロパティーではなく、@pageのプロパティーとして設定するべき。さもなくば、 改ページ部分での上下マージンが反映されない。
  • webContents.print()はElectron 7.xではmarginType:"custom"を設定するとクラッシュする。
  • webContents.print()marginTypeに"default"以外の値を設定しても、"default"と表示が変わらない。(バグとも思える。Electron version 8.0.1)。
  • webContents.printToPDF()marginsType:0(デフォルトマージン)にすると CSSの@pageに設定したmargin, margin-left, margin-right,margin-top,margin-bottomが適用される。 marginsType:1(マージンなし)にすると、これらのCSS設定も無視される。
  • webContents.printToPDF()marginsType:0と、webContents.print()marginType: "default"はCSSの適用と表示に関して(おそらく)同じ挙動。

body要素の中にdiv要素があり、その中に文章が書かれている場合のCSS記述は次のようになります。

@page{
  size: A4;
/*  本来は以下の記述で良いはずが、Chromium系バグで設定値より大きくなってしまう
  margin-left:   15mm;
  margin-right:  15mm;
  margin-top:    20mm;
  margin-bottom: 20mm;
回避策として以下のように%で指定する
*/
  margin-left:   7.143%;   /* = 15mm / 210mm */
  margin-right:  7.143%;
  margin-top:    9.524%;      /* = 20mm / 210mm "横幅"に対する割合*/
  margin-bottom: 9.524%;
}
@media print{
  body{
    padding: 0mm;  
    margin: 0mm;   /* これが無いと余分な余白が入る */
  }
  div{
    padding: 0mm;
    margin: 0mm;
    width:100%;
  }
}

上記の様なCSSに対して、印刷のJavascriptコードはprint()関数のoptionとして次のようにmarginType:"default"を指定する。

  const wc = browserWindow.webContents;
  wc.print({margins:{marginType:"default"}},
    (success, error) => {
        if(success){
            console.log('Print successfully.');
        }else{
            console.log(error);
        }
    });

また、PDF出力時のJavascriptコードはprintToPDF()関数のoptionとして次のようにmarginsType:0を指定する。

  const wc = browserWindow.webContents;
  wc.printToPDF({pageSize:"A4", marginsType:0}).then(data => {
     fs.writeFile(result.filePath, data, (error) => {
       if (error) throw error;
         console.log('Write PDF successfully.');
       })
     }).catch(error => {
       console.log(error);
     });

雑感

Electronの印刷はとても楽でいいですね。CSSで微調整できるところも素敵です。すごい時代になってますね。

子供の頃、親父にPCを買ってもらった際、弟と協力して年賀状印刷ソフトを作ったのを思い出しました。当時はVisual Basic6で印刷物の位置合わせをするのに苦労しました。

雑感(改定時)

Chrome,Chromiumのバグと気づくまで今回は時間が掛かりました。Firefoxがあってよかった。Edge(Legacy)が無くなってしまった弊害として、今回の様なブラウザエンジンのバグに気づきにくくなりますね。そういう意味で復活して欲しいなぁ、EHTMLエンジン。

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

forEachをIE11で使う方法

ES2015

JavascriptのES2015(ES6)からは配列を便利に扱えるメソッドが増えましたが、
IE11ではサポートされていないものが多々あります。

詳しい対応状況はこちらが分かりやすいですね↓
http://kangax.github.io/compat-table/es6/

forEachもその一つで、Can I useを確認してみても、

コメント 2020-02-18 121912.png

サポートされていない事が分かります。
ですが、実際はひと工夫することでIE11でも使えます。

forEachのIE11対応

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>forEachのIE対応実験</title>
</head>
<body>
  <ul id="box">
    <li>りんご</li>
    <li>みかん</li>
    <li>バナナ</li>
  </ul>
</body>
</html>
//Chromeなどのモダンブラウザではこれで動く
const fruits = document.querySelectorAll('#box li'); //Nodelist;
fruits.forEach(fruit => {
 console.log(fruit.innerText); // りんご, みかん, バナナ
});

//IEだとエラーで動かない
オブジェクトは 'forEach' プロパティまたはメソッドをサポートしていません。

原因は...

IE11はquerySelectorAll等が作るNodelistのforEachに対応していないそうです。
対策としては、このNodelistを配列に変換します。
ですので、元から配列として定義されているオブジェクトに対しては、IE11でもforEachが使えます。

//IEでも動いた!
const fruits = document.querySelectorAll('#box li'); //Nodelist
const fruitsArr = Array.prototype.slice.call(fruits); //Array
fruitsArr.forEach(function(fruit) { //アロー関数はIEでは使えません
  console.log(fruit.innerText); // りんご, みかん, バナナ
});
//こう書いてもOK
const fruits = document.querySelectorAll('#box li'); //Nodelist
Array.prototype.forEach.call(fruits, function(fruit) {
  console.log(fruit.innerText); // りんご, みかん, バナナ
});

参考文献

https://lealog.hateblo.jp/entry/2014/02/07/012014
Array.prototype - JavaScript | MDN
Array.prototype.slice() - JavaScript | MDN

まとめ

他にも通常のfor文にお置き換えるという対応方法もありますが、
今回の対策の方が記述が少なくて済むので、今後も積極的に使っていきたいと思います。

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

【Ajax+Rails+Carrierwave】個別削除可能な画像複数(10枚まで)投稿

はじめに

初投稿になります。syomaと申します。よろしくお願いいたします。
某プログラミングスクールの最終課題で某フリマアプリのクローンにて、商品出品における画像の複数投稿が大変だった件について投稿いたします。
参考記事が幾つかございましたので、そちらをを参考にさせていただきました。この場をお借りして感謝申し上げます。ただ、どの記事にもscssの記載が無く、クラスに当たるscssの記述に苦戦したので画像複数(10枚まで)投稿の記述とscssをセットで記載してみます。

実現したいこと

1.画像をアップロードし、結果を送信する前にプレビューを表示する。(ドラッグ&ドロップは非対応です。すみません。)
2.そのほかのフォームの結果と一緒に画像をコントローラに送信する。
3.画像のプレビュー時に、削除ボタンで個別にアップロードする画像を変更する。
ezgif.com-video-to-gif.gif

環境

Rails 5.2.4.1
ruby 2.5.1

注意点

1.Rails初学者のため、間違っている部分が多々ある可能性がありますので、参考程度にご覧ください。
2.gemのCarrierwaveで1枚画像投稿ができている。
3.モデル側については、解説ありません。コントローラ、ビュー、javascript部分になります。モデル側は、good(商品)に対して複数のphotosテーブルのレコードを登録できるような中間テーブルを作成しています。

テーブル構造

↓goodsテーブル
スクリーンショット 2020-02-18 13.11.27.png

↓photosテーブル
スクリーンショット 2020-02-18 13.18.12.png

モデル同士の関係

userテーブル(誰が)、goodテーブル(何を)、categoryテーブル(どんな→Gemのancestryを使用)、photo(画像)と分けておりますが、今回はgood,photoテーブルのみ記載しておきます。

good(商品).rb
class Good < ApplicationRecord
  belongs_to :user
  belongs_to :category
  has_many :photos, dependent: :destroy
  accepts_nested_attributes_for :photos, allow_destroy: true
end
photo(画像).rb
class Photo < ApplicationRecord
  belongs_to :good , optional: true
  mount_uploader :image, ImageUploader
end

controller

goods_controller.rb
class GoodsController < ApplicationController

  def new
    @good = Good.new
    @good.photos.build()
  end

  def create
    @good = Good.new(good_params)
    respond_to do |format|
      if @good.save!
          params[:good_photos][:image].each do |image|
            @good.photos.create(image: image, good_id: @good.id)
          end
        format.html{redirect_to root_path}
      else
        @good.photos.build
        format.html{render action: 'new'}
      end
    end
  end

  def good_params
    params.require(:good).permit(:category_id, :brand, :name, :condition, :discription, :size, :delivery_type, :prefecture, :day, :fee, photos_attributes: [:image]).merge(user_id: current_user.id)
  end

end

.merge(user_id: current_user.id)は誰が出品したかをgoodデーブルに保存してくれる記述です。もし、user登録機能または出品を単体でしたい場合はこの一文を消してくださいまし。

haml

出品なのでnewになります。

new.html.haml
= form_for @good , html: {id: "item-dropzone"} do |f|
  .upload-box
    .upload-box__head
      %h3.bigger 出品画像
      %span 必須
    %p.discription 最大10枚までアップロードできます

    -# ここからが複数画像出品の部分です
    = f.fields_for :photos do |image|
      .dropzone-container
        #preview
        .dropzone-area
          = image.label :image, class: "dropzone-box", for: "upload-image" do
            .input_area
              = image.file_field :image, multiple: true, name: 'good_photos[image][]', id: "upload-image", class: "upload-image", 'data-image': 0
            %p ここをクリックしてください
      .dropzone-container
        #preview2
        .dropzone-area2
          = image.label :image, class: "dropzone-box", for: "upload-image" do
            %p ここをクリックしてください
    -# 複数画像出品終わり、以下は様々なフォームがある想定

10枚の画像を取り込むためにinputタグを10個用意すんのかい、せんのかいと不安でしたが、どうやら
= image.file_field :image, multiple: true
の一文で対応してくれそう!やったね!
クラスについてですが.dropzone-areaに画像が放り込まれると#previewに表示されるイメージです。
おいおいsyomaさん。消去・編集ボタンないけど!?(キレ気味)
ご安心を!javascript側で画像が投稿された瞬間に消去・編集ボタンを#previewに差し込むという記述にしております。

scss

new.scss
//image投稿欄のCSS
.dropzone-container{
  display: block;
  margin: 16px auto 0;
  display: flex;

  //プレビュー表示欄のCSS
  #preview , #preview2{
    display: flex;

    .img_view {
      height: 162px;
      width: 112px;
      margin: 0 15px 10px 0;

        img{
          width: 112px;
          height: 112px;
        }
      }

      .btn_wrapper {
        display: flex;
        text-align: center;

        .btn.edit {
          color: #00b0ff;
          width: 50%;
          height: 50px;
          line-height: 50px;
          border: 1px solid #eee;
          background: #f5f5f5;
          cursor: pointer;
        }

        .btn.delete {
          color: #00b0ff;
          width: 50%;
          height: 50px;
          line-height: 50px;
          border: 1px solid #eee;
          background: #f5f5f5;
          cursor: pointer;
        }
      }
    }

  //投稿クリックエリアのCSS
  .dropzone-area {
    margin-bottom: 10px;
    width: 620px;

    .dropzone-box {
      display: block;
      border: 1px dashed #ccc;
      position: relative;
      background: #f5f5f5;
      width: 100%;
      height: 162px;
      cursor: pointer;

      p {
        position: absolute;
        top: 50%;
        left: 16px;
        right: 16px;
        text-align: center;
        font-size: 14px;
        line-height: 1.5;
        font-weight: bold;
        -webkit-transform: translate(0, -50%);
        transform: translate(0, -50%);
        pointer-events: none;
        white-space: pre-wrap;
        word-wrap: break-word;
      }
      .input_area {
        display: none;
      }
    }
  }
  //投稿クリックエリアのCSS
  .dropzone-area2{
    display: none;
    margin-bottom: 10px;
    width: 620px;

    .dropzone-box {
      display: block;
      border: 1px dashed #ccc;
      position: relative;
      background: #f5f5f5;
      width: 100%;
      height: 162px;
      cursor: pointer;

      p {
        position: absolute;
        top: 50%;
        left: 16px;
        right: 16px;
        text-align: center;
        font-size: 14px;
        line-height: 1.5;
        font-weight: bold;
        -webkit-transform: translate(0, -50%);
        transform: translate(0, -50%);
        pointer-events: none;
        white-space: pre-wrap;
        word-wrap: break-word;
      }
      .input_area {
        display: none;
      }
    }
  }
}

.dropzone-area2はdisplay: none;にしております。javascript側で5枚目が選択された時点で表示される仕組みにするためです。

javascript

new_good.js
$(document).on('turbolinks:load', function(){
  var dropzone = $('.dropzone-area');
  var dropzone2 = $('.dropzone-area2');
  var dropzone_box = $('.dropzone-box');
  var images = [];
  var inputs  =[];
  var input_area = $('.input_area');
  var preview = $('#preview');
  var preview2 = $('#preview2');

  $(document).on('change', 'input[type= "file"].upload-image',function(event) {
    var file = $(this).prop('files')[0];
    var reader = new FileReader();
    inputs.push($(this));
    var img = $(`<div class= "img_view"><img></div>`);
    reader.onload = function(e) {
      var btn_wrapper = $('<div class="btn_wrapper"><div class="btn edit">編集</div><div class="btn delete">削除</div></div>');
      img.append(btn_wrapper);
      img.find('img').attr({
        src: e.target.result
      })
    }
    reader.readAsDataURL(file);
    images.push(img);

    if (images.length <= 4) {
      $('#preview').empty();
      $.each(images, function(index, image) {
        image.data('image', index);
        preview.append(image);
      })
      dropzone.css({
        'width': `calc(100% - (20% * ${images.length}))`
      })

      // 画像が5枚のとき1段目の枠を消し、2段目の枠を出す
    } else if (images.length == 5) {
      $("#preview").empty();
      $.each(images, function(index, image) {
        image.data("image", index);
        preview.append(image);
      });
      dropzone2.css({
        display: "block"
      });
      dropzone.css({
        display: "none"
      });
      preview2.empty();

      // 画像が6枚以上のとき
    } else if (images.length >= 6) {
      // 1〜5枚目の画像を抽出
      var pickup_images1 = images.slice(0, 5);

      // 1〜5枚目を1段目に表示
      $('#preview').empty();
      $.each(pickup_images1, function(index, image) {
        image.data('image', index);
        preview.append(image);
      })

      // 6枚目以降の画像を抽出
      var pickup_images2 = images.slice(5);

      // 6枚目以降を2段目に表示
      $.each(pickup_images2, function(index, image) {
        image.data('image', index + 5);
        preview2.append(image);
      })

      dropzone.css({
        'display': 'none'
      })
      dropzone2.css({
        'display': 'block',
        'width': `calc(100% - (20% * ${images.length - 5}))`
      })

      // 画像が10枚になったら枠を消す
      if (images.length == 10) {
        dropzone2.css({
          display: "none"
        });
      }
    }
    var new_image = $(`<input multiple= "multiple" name="good_photos[image][]" class="upload-image" data-image= ${images.length} type="file" id="upload-image">`);
    input_area.prepend(new_image);
  });
  $(document).on('click', '.delete', function() {
    var target_image = $(this).parent().parent();
    $.each(inputs, function(index, input){
      if ($(this).data('image') == target_image.data('image')){
        $(this).remove();
        target_image.remove();
        var num = $(this).data('image');
        images.splice(num, 1);
        inputs.splice(num, 1);
        if(inputs.length == 0) {
          $('input[type= "file"].upload-image').attr({
            'data-image': 0
          })
        }
      }
    })
    $('input[type= "file"].upload-image:first').attr({
      'data-image': inputs.length
    })
    $.each(inputs, function(index, input) {
      var input = $(this)
      input.attr({
        'data-image': index
      })
      $('input[type= "file"].upload-image:first').after(input)
    })
    if (images.length >= 5) {
      dropzone2.css({
        'display': 'block'
      })
      $.each(images, function(index, image) {
        image.attr('data-image', index);
        preview2.append(image);
      })
      dropzone2.css({
        'width': `calc(100% - (135px * ${images.length - 5}))`
      })
      if(images.length == 9) {
        dropzone2.find('p').replaceWith('<i class="fa fa-camera"></i>')
      }
      if(images.length == 8) {
        dropzone2.find('i').replaceWith('<p>ココをクリックしてください</p>')
      }
    } else {
      dropzone.css({
        'display': 'block'
      })
      $.each(images, function(index, image) {
        image.attr('data-image', index);
        preview.append(image);
      })
      dropzone.css({
        'width': `calc(100% - (135px * ${images.length}))`
      })
    }
    if(images.length == 4) {
      dropzone2.css({
        'display': 'none'
      })
    }
    if(images.length == 3) {
      dropzone.find('i').replaceWith('<p>ココをクリックしてください</p>')
    }
  })
});

画像の枚数によってif文で条件分岐をしております。ドチャクソ長いですがご理解ください。。。

参考文献

https://qiita.com/shinnosuke960801/items/66f2a511803d7dac53a3
https://qiita.com/yamayu_504/items/bdde3eeb9ae06a3876bc
https://kolosek.com/carrierwave-upload-multiple-images/

まとめ

ご指摘がございましたら編集リクエストをよろしくお願いいたします!
このままeditも書きたいところですが、時間との兼ね合いもあり少し先になりそうです(すまんの)。。。
参考になった方は是非「いいね」していただけると幸いです!
最後までご覧いただきありがとうございました。

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

タッチイベント(マルチ対応)JavaScript サンプルコード

タッチイベント(マルチ対応)JavaScript サンプルコード

簡潔で理解しやすいタッチイベント(マルチ)のコードが見当たらなかったので、
以下のページを参考にリファクタリングしてみた。

https://developer.mozilla.org/ja/docs/Web/API/Touch_events

https://mdn.mozillademos.org/ja/docs/Web/API/Touch_events$samples/Example

タッチイベントで渡ってくる情報って、ev.pageX, _ev.pageY なのね、、、使いにくいなぁ。
具体的には「そのまま canvas に描くとマージンの分描画位置がズレる」という問題がある(元のコードから)。どうしたものか、、、。
(以下のコードはそのズレの対策はしていないです。なぜ、
ev.offsetX, _ev.offsetY をくれないのか、、、)

リファクタリングした結果

リファクタリングした結果がこちら。

ご注意:
・スマホのブラウザで確認しないとタッチイベントは発生しないことにご注意ください
・PCのブラウザで起動した場合でも動作が確認できるように mousemove イベントのみ拾っていますが、あくまで確認用(メインのコードではない)なのでこちらもご注意ください

https://codepen.io/yamazaki3104/pen/QWbbBOK

See the Pen Javascript-touch-event-test by yamazaki.3104 (@yamazaki3104) on CodePen.

コードはこちら、参考まで

参考にしたコードの原型をとどめていないのは、、、、あえて触れないでおこう。いや、すでに「同じ機能」ではなくなっているから「リファクタリング」とは言えないのではないか??

<!DOCTYPE html>
<html><body>
<canvas id="canvas" width="600" height="600" style="border:solid black 1px;">
Your browser does not support canvas element.
</canvas>
<script>

class PointTable // 描画用の色と座標を覚えるためのクラス
{
    constructor( _table_max = 10 ) {
        this.color = []
        this.x = []
        this.y = []
        this.table_max = _table_max
        for ( let i=0 ; i<_table_max ; i++ ) {
            this.color.push( `hsla( ${ i / this.table_max * 360 }, 100%, 33%, 0.8 )` )
            this.x.push( 0 )
            this.y.push( 0 )
        }
    }

    get_idx( _i ) {
        const i = ( _i < 0 ? -_i : _i ) // abs
        return i % this.table_max
    }

    get_color( _tc ) {
        const r = this.get_idx( _tc.identifier )
        return this.color[r]
    }

    get_xy( _tc ) {
        const r = this.get_idx( _tc.identifier )
        return { x: this.x[r], y: this.y[r] }
    }

    set_xy( _tc ){
        const r = this.get_idx( _tc.identifier )
        this.x[r] = _tc.pageX
        this.y[r] = _tc.pageY
    }
}

const xy_tbl = new PointTable()
const elm = document.querySelector( 'canvas#canvas' )
const ctx = elm.getContext( '2d' )

// changedTouches を回す。 Array ではなく Object なんですよ。 Why?
const for_touches = ( _evt, _fnc ) => {
    for ( let i=0 ; i<_evt.changedTouches.length ; i++ )
        _fnc( _evt.changedTouches[i] )
}

const ctx_draw_line = ( _t ) => {   // ctxに線を描く
    const p = xy_tbl.get_xy( _t )
    ctx.beginPath()
    ctx.moveTo( p.x, p.y )
    ctx.lineTo( _t.pageX, _t.pageY )
    ctx.lineWidth = 4
    ctx.strokeStyle = xy_tbl.get_color( _t )
    ctx.stroke()
    xy_tbl.set_xy( _t )
}

elm.addEventListener( "touchstart", ( _evt ) => {
    _evt.preventDefault()
    for_touches( _evt, ( _t ) => xy_tbl.set_xy( _t ) )
}, false )

elm.addEventListener( "touchmove", ( _evt ) => {
    _evt.preventDefault()
    for_touches( _evt, ( _t ) => ctx_draw_line( _t ) )
}, false )

elm.addEventListener( "touchend", ( _evt ) => {
    _evt.preventDefault()
    for_touches( _evt, ( _t ) => {
        ctx_draw_line( _t )
        ctx.fillStyle = xy_tbl.get_color( _t )
        ctx.fillRect( _t.pageX - 4, _t.pageY - 4, 8, 8 )
    })
}, false )

//----------------
// PC用 mousemove イベントでの動作確認コード

elm.addEventListener( "mousemove", ( _evt ) => {
    _evt.preventDefault()
    ctx_draw_line( { identifier: 0, pageX: _evt.pageX, pageY: _evt.pageY } )
}, false )

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

canvas でできること

このサンプルコードでは、タッチイベントの結果をシンプルに表現するために、canvas に描画している。
canvas でできることは、このあたりにまとまっています。参考まで。

https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D

まとめ

コードを見ていただければ、かなり短いし、特に解説するほどではないいけど、

・changedTouches とかに複数の情報がまとまって渡っていくるところがタッチイベントのポイント
・で、changedTouches は Array ではなく Object なところもポイント(Arrayにしてほしいなぁ もっと簡潔に書きたいから)
・あとは、offsetX, offsetY が「無い」ところもポイントかな(いつかサポートされるのでしょうか、、、遠い目)

・情報保存用のクラス、PointTable の _table_max は 10 にしてるけど、もっと少なくてもいいかもしれない。3点以上来てもどう処理していいかわからないしね。

このコードが、みなさまのプログラミングライフの一助になれば幸いです。

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

Vuetifyのv-comboboxで自由に入力された文字列を取得したい

v-comboboxを使うということは自由入力させたいということなのに、自由入力した文字をうまく取得できない!
なぜか微妙にハマったのでメモっておきます。

例のとおりリスト要素から選択された際はselecteditemにオブジェクトがバインドされるまではいいが、では自由に入力された値は?
オフィシャルのドキュメントをよく読むと、、valueプロパティは The input's value、つまり入力された値はvalueプロパティに入る、と記載されております。そこでselecteditemのハンドリングとも併せて検証してみました。

結果、:value="inputedvalue"のバインドだと入力された値は取得できず、 ref="comb"で$refs経由の取得がうまくいきました。

<v-combobox
    v-model="selecteditem"
    :items="items"
    item-text="name" item-value="id" return-object
    ref="comb" :value="inputedvalue">
</v-combobox>

selecteditem: null,
items : [
    { id:"001", name:"牛丼" },
    { id:"002", name:"ピザ" },
    { id:"003", name:"ラーメン" },
]
<script>
var val = "";

// バインド経由では取れない
val = this.inputedvalue; 

// $refs経由でプロパティ直見すると取れる
val = this.$refs.comb.value;

// itemsの中の選択肢から選ぶ判定はv-modelがobjectになってることを確認すればよい
if(this.selecteditem && this.selecteditem.id) {
    val = this.selecteditem.id + " / " + this.selecteditem.name;
}

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

JavaScriptで日付の判定をする

実装

const hoge = new Date('aaa')
if(hoge.toString() === "Invalid Date"){
    console.log("日付エラー")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IM-FormaDesignerでテキストボックスの入力可否をチェックボックスで制御する

はじめに

intra-martのIM-FormaDesignerでは、ローコード開発を行うことが可能です。
画面上の制御が必要な場合は、JavaScriptでスクリプトを実行することも可能です。

やったこと

IM-FormaDesignerでテキストボックスの入力可否をチェックボックスで制御した。

if( document.getElementsByName("checkbox")[0].value == 1){
    // テキストボックスの背景色をグレーに変更する
    $('input[name="textbox"]').css({
        'background-color': '#CCCCCC'
    });
    // 透明の箱をテキストボックスに重ねて入力不可にする
    document.getElementById('clearbox').style.left = '20px';
} else {
    // テキストボックスの背景色を白に変更する
    $('input[name="textbox"]').css({
        'background-color': '#FFFFFF'
    });
    // 透明の箱を元の位置に戻して入力可にする
    document.getElementById('clearbox').style.left = '520px';
}

わかったこと

JavaScriptを使用してCSSの要素を制御することで、パッケージ製品やフレームワークの制約がある中でも、カスタマイズなしで画面制御を行うことが可能である。なぜならば、要素の取得と書き換えがプログラムで制御できれば、自由に制御ができるからです。

つぎにやること

パッケージ製品やフレームワークとの結合度を意識しながら、他の実装を検討する

メリット

バージョンアップによる影響を最低限に抑えられる

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

Javascriptで動的にファイルを読み込む

はじめに

ウェブサイトで外部ライブラリを読み込むというのはよくやる行為だが、それが多くなってきたり、外部ライブラリを踏み台に自作ライブラリを作る(&同じサイト内で使い回す)となると読み込みの管理も大変になってくる。
ので、その辺を自動化する関数を作ってみた。

headに読み込みリンクを生成する

二重読み込みの防止装置もつけてある。

javascript
function insert_link(tagname, obj, raw_url){

    // 差し込む要素の生成
    var target_tag = document.createElement(tagname);
    var keylist = Object.keys(obj);
    var currentkey;
    for(let int = 0; int < keylist.length; int++){
        currentkey = keylist[int];
        target_tag[currentkey] = obj[currentkey];
    }

    // その要素がhead内にすでに存在するかどうかチェックし、存在しない場合のみ読み込みを行う
    var head = document.querySelector('head');
    var headtext = head.innerHTML;
    var processed_url = raw_url.replace(/\//g, '\\/');
    processed_url = processed_url.replace(/\./g, '\\.');
    var regexstr = new RegExp( '=["\']' + processed_url + '["\']', 'g');
    if(!regexstr.test(headtext)){
        head.appendChild(target_tag);
    }
    return target_tag;
}

使用例

html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>無題</title>
</head>
<body>

<script type="text/javascript">
(function(){

function insert_link(tagname, obj, raw_url){

    // 差し込む要素の生成
    var target_tag = document.createElement(tagname);
    var keylist = Object.keys(obj);
    var currentkey;
    for(let int = 0; int < keylist.length; int++){
        currentkey = keylist[int];
        target_tag[currentkey] = obj[currentkey];
    }

    // その要素がhead内にすでに存在するかどうかチェックし、存在しない場合のみ読み込みを行う
    var head = document.querySelector('head');
    var headtext = head.innerHTML;
    var processed_url = raw_url.replace(/\//g, '\\/');
    processed_url = processed_url.replace(/\./g, '\\.');
    var regexstr = new RegExp( '=["\']' + processed_url + '["\']', 'g');
    if(!regexstr.test(headtext)){
        head.appendChild(target_tag);
    }
    return target_tag;
}

// 使うときは、キーを属性名、値を属性値としたオブジェクト(下のやつ)に設定を書き込みます
// CSSファイルの場合
var list01 = {
    rel: "stylesheet",
    href: "https://example.com/css/style.css",
}

// Jsファイルの場合
var list02 = {
    type: "text/javascript",
    charset: "UTF-8",
    src: "https://example.com/js/testscript.js",
}


// 引数は左から順にタグ名、設定オブジェクト名、読込先です。
insert_link('link', list01, list01.href);

// 上で挙げた通り、既に読み込まれているときは自動で処理中止してくれます。
insert_link('link', list01, list01.href);


// Jsの場合
insert_link('script', list02, list02.src);

// 上で挙げた通り、既に読み込まれているときは自動で処理中止してくれます。
insert_link('script', list02, list02.src);


})();
</script>
</body>
</html>

実行例

1581988906989.jpg

読み込みが完了したのを確認してから処理を実行

実は、上のコードだけだとある問題が発生する。
ライブラリの読み込みが、本命のjavascriptよりも後になってしまうのだ。
これでは、せっかく読み込んだライブラリの意味がない。

これを防ぐため、実際のコードでは以下の文を付け足して使う。

javascript
// insert_linkが実行された時点で、ライブラリの読み込み処理は終わっています。
var madedom = insert_link('script', list02, list02.src);

madedom.onload = function(){

    /* ~~ライブラリの読み込み完了後に行いたい処理をここに書く~~ */

}

onloadを使うことで、読込みの完了を待つことができるのだ。
多少速度は落ちるが、エラーが出るよりかはマシだろう。

全体のコード

html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>無題</title>
</head>
<body>

<script type="text/javascript">
(function(){

function insert_link(tagname, obj, raw_url){

    // 差し込む要素の生成
    var target_tag = document.createElement(tagname);
    var keylist = Object.keys(obj);
    var currentkey;
    for(let int = 0; int < keylist.length; int++){
        currentkey = keylist[int];
        target_tag[currentkey] = obj[currentkey];
    }

    // その要素がhead内にすでに存在するかどうかチェックし、存在しない場合のみ読み込みを行う
    var head = document.querySelector('head');
    var headtext = head.innerHTML;
    var processed_url = raw_url.replace(/\//g, '\\/');
    processed_url = processed_url.replace(/\./g, '\\.');
    var regexstr = new RegExp( '=["\']' + processed_url + '["\']', 'g');
    if(!regexstr.test(headtext)){
        head.appendChild(target_tag);
    }
    return target_tag;
}

// Jsファイルの場合
var list02 = {
    type: "text/javascript",
    charset: "UTF-8",
    src: "https://example.com/js/testscript.js",
}

// Js
var madedom = insert_link('script', list02, list02.src);

madedom.onload = function(){

    /* ~~ライブラリの読み込み完了後に行いたい処理をここに書く~~ */

}

})();
</script>
</body>
</html>

おわりに

間違いなどありましたら編集リクエストおねがいします。

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

Javascriptの関数で可変長引数を利用する方法 | Array.prototype.slice.call()

*この記事は、「Javascriptで可変長引数をスグにでも使いたい」と言う人を対象に書いております。細かいことは省いていますので、突っ込んだことを知りたい方は他のページを調べてください。

Rubyには普通に存在する可変長引数ですが、Javascriptで利用するには少し工夫が必要になります。

と言っても、Array.prototype.slice.call()を使うだけです。

以下、例をあげます。

hatsumori.js
var test = function() {
  var args = Array.prototype.slice.call(arguments);
  console.log(args);
}

test('ななまる', 'イマドキ', 'ショパン', 'アカデミー')
// ['ななまる', 'イマドキ', 'ショパン', 'アカデミー']

関数内でargumentsは渡された引数を渡してくるのですが、そのままでは「配列っぽい何か」であり、配列として加工できません。
Array.prototype.slice.call(arguments)で、配列として加工できるようになります。

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

JavaScriptの日付フォーマットにうるさいあの人も満足? formatDate関数つくりました

概要

  • とある事情で、JavaScriptで日付(Dateオブジェクト)をyyyy-MM-dd'T'HH:mm:ssXXXのようにパターン指定でフォーマットする関数を作りました
  • 外部ライブラリ不要で、そこそこ欲張っています
    • ISO8601形式、RFC1123形式、タイムゾーンRFC822など表現可能
  • パターン仕様はJavaのSimpleDateFormatライクにしています

コード

以下が、作成した関数formatDate

function formatDate(formatStr, date, opts) {

    if (!date) {
        date = new Date();
    }

    opts = opts || {};

    let _days = opts.days;

    if (!_days) {
        _days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    }

    let _months = opts.months;

    if (!_months) {
        _months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    }

    const pad = (number, strDigits, isUnpad) => {
        const strNum = number.toString();
        if (!isUnpad && strNum.length > strDigits.length) {
            return strNum;
        } else {
            return ('0000' + strNum).slice(-strDigits.length);
        }
    };

    const timezone = (date, letter) => {
        const chunk = [];
        const offset = -date.getTimezoneOffset();
        chunk.push(offset === 0 ? 'Z' : offset > 0 ? '+' : '-');//add Z or +,-
        if (offset === 0) return chunk;
        chunk.push(pad(Math.floor(offset / 60), '00'));//hour
        if (letter === 'X') return chunk.join('');
        if (letter === 'XXX') chunk.push(':');
        chunk.push(pad((offset % 60), '00'));//min
        return chunk.join('');
    };

    const DELIM = '\0\0';
    const escapeStack = [];

    const escapedFmtStr = formatStr.replace(/'.*?'/g, m => {
        escapeStack.push(m.replace(/'/g, ''));
        return `${DELIM}${escapeStack.length - 1}${DELIM}`;
    });

    const formattedStr = escapedFmtStr
        .replace(/y{4}|y{2}/g, m => pad(date.getFullYear(), m, true))
        .replace(/M{3}/g, m => _months[date.getMonth()])
        .replace(/M{1,2}/g, m => pad(date.getMonth() + 1, m))
        .replace(/M{1,2}/g, m => pad(date.getMonth() + 1, m))
        .replace(/d{1,2}/g, m => pad(date.getDate(), m))
        .replace(/H{1,2}/g, m => pad(date.getHours(), m))
        .replace(/h{1,2}/g, m => {
            const hours = date.getHours();
            return pad(hours === 0 ? 12 : hours > 12 ? hours - 12 : hours, m);
        })
        .replace(/a{1,2}/g, m => date.getHours() >= 12 ? 'PM' : 'AM')
        .replace(/m{1,2}/g, m => pad(date.getMinutes(), m))
        .replace(/s{1,2}/g, m => pad(date.getSeconds(), m))
        .replace(/S{3}/g, m => pad(date.getMilliseconds(), m))
        .replace(/[E]+/g, m => _days[date.getDay()])
        .replace(/[Z]+/g, m => timezone(date, m))
        .replace(/X{1,3}/g, m => timezone(date, m));

    const unescapedStr = formattedStr.replace(new RegExp(`${DELIM}\\d+${DELIM}`, 'g'),
        m => {
            const unescaped = escapeStack.shift();
            return unescaped.length > 0 ? unescaped : '\'';
        });

    return unescapedStr;
}

サンプルコード

以下のように使う

const date = new Date();

//ISO8601
console.log(formatDate("yyyyMMdd'T'HHmmssXX",date));
//->20200218T015020+0900

//ISO8601 Ex.
console.log(formatDate("yyyy-MM-dd'T'HH:mm:ssXXX",date));
//->2020-02-18T01:50:20+09:00

//RFC1123(RFC822)
console.log(formatDate("E, dd MMM yyyy HH:mm:ss Z",date));
//->Tue, 18 Feb 2020 01:50:20 +0900

//曜日を日本語に
console.log(formatDate("yyyy年MM月dd日(E曜日) HH時mm分ss秒SSS",date,{
    days:['','','','','','','']
}));
//->2020年02月18日(火曜日) 01時50分20秒789

//エスケープ(対象をシングルクォート"'"で囲む。シングルクォート自体は"''"で表現)
console.log(formatDate("'Hello!' It'''s' H:m:s.",date));
//->Hello! It's 1:50:20.

ライブデモ(pen)

See the Pen ZEGWdKE by Tom Misawa (@riversun) on CodePen.

パターン指定方法の詳細

  • パターンとしてH:m:sのように1文字指定した場合、時間が7時なら7のみ、17時なら17のように表示されるが、HH:mm:ssのように2文字指定した場合には7時なら07のように左から0埋め(zero padding)される。
  • パターン内に文字列を含めたい場合は 'Hello' hh:mm:ss のように文字列部をシングルクォート(')で文字列を囲むとエスケープされる。
  • 記法はJavaのSimpleDateFormatライク

パターン指定方法一覧

文字 内容
yyyy 2018
yy (年を2桁表示にする) 18
M 7
MM 月(0埋め) 07
MMM 月名(英語圏等向け) Jul
d 日付 17
dd 日付(0埋め) 17
a AM/PMのマーカー PM
H 時(0-23) 12
HH 時(0-23)(0埋め) 12
E 曜日 Tue
h 時(午前午後を1-12で) 12
hh 時(0埋め) 12
m 8
mm 分(0埋め) 08
s 56
ss 秒(0埋め) 56
S ミリ秒 789
Z RFC822形式のタイムゾーン +0900
X ISO8601形式のタイムゾーン(1) +09
XX ISO8601形式のタイムゾーン(2) +0900
XXX ISO8601形式のタイムゾーン(3) +09:00
' ' シングルクォートで囲むと中の文字をエスケープする
'' シングルクォート自体を表示したいとき

パターン指定いろいろな例

パターン
yyyyMMdd'T'HHmmssXX 20180717T120856+0900
yyyy-MM-dd'T'HH:mm:ssXXX 2018-07-17T12:08:56+09:00
E, dd MMM yyyy HH:mm:ss Z Tue, 17 Jul 2018 12:08:56 +0900
yyyy.MM.dd 'at' hh:mm:ss Z 2018.07.17 at 12:08:56 +0900
EEE, MMM d, ''yy Tue, Jul 17, '18
h:mm a 12:08 PM
hh 'o''''clock' a, X 12 o'clock PM, +09
yyyy年M月d日 H時m分s秒 2018年7月17日 12時8分56秒
yyyy年MM月dd日 HH時mm分ss秒 2018年07月17日 12時08分56秒
yyyyMMddHHmmssSSS 20180717120856789

おまけ

間違いやすいAM,PMの概念

アメリカなど12時制が慣用されている国はhh:mm:ss(hは1-12を表すパターン)を使うことが多い。
以下のようにAM/PMは夜(midnight)の12時が12:00AM。昼(noon)の12時が12:00PMとなるので、実装にもそれを反映した。

24時制 12時制
0 12AM
1 1AM
2 2AM
3 3AM
4 4AM
5 5AM
6 6AM
7 7AM
8 8AM
9 9AM
10 10AM
11 11AM
12 12PM
13 1PM
14 2PM
15 3PM
16 4PM
17 5PM
18 6PM
19 7PM
20 8PM
21 9PM
22 10PM
23 11PM

まとめ

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

JavaScriptの日付フォーマットにうるさいあの人も満足させたいformatDate関数

image.png

概要

  • とある事情で、JavaScriptで日付(Dateオブジェクト)をyyyy-MM-dd'T'HH:mm:ssXXXのようにパターン指定でフォーマットする関数を作りました
  • 外部ライブラリ不要で、そこそこ欲張っています
    • ISO8601形式、RFC1123形式、タイムゾーンRFC822など表現可能
  • パターン仕様はJavaのSimpleDateFormatライクにしています

コード

以下が、作成した関数formatDate

function formatDate(formatStr, date, opts) {

    if (!date) {
        date = new Date();
    }

    opts = opts || {};

    let _days = opts.days;

    if (!_days) {
        _days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    }

    let _months = opts.months;

    if (!_months) {
        _months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    }

    const pad = (number, strDigits, isUnpad) => {
        const strNum = Math.abs(number).toString();
        if (!isUnpad && strNum.length > strDigits.length) {
            return strNum;
        } else {
            return ('0000' + strNum).slice(-strDigits.length);
        }
    };

    const timezone = (date, letter) => {
        const chunk = [];
        const offset = -date.getTimezoneOffset();
        chunk.push(offset === 0 ? 'Z' : offset > 0 ? '+' : '-');//add Z or +,-
        if (offset === 0) return chunk;
        chunk.push(pad(Math.floor(offset / 60), '00'));//hour
        if (letter === 'X') return chunk.join('');
        if (letter === 'XXX') chunk.push(':');
        chunk.push(pad((offset % 60), '00'));//min
        return chunk.join('');
    };

    const DELIM = '\0\0';
    const escapeStack = [];

    const escapedFmtStr = formatStr.replace(/'.*?'/g, m => {
        escapeStack.push(m.replace(/'/g, ''));
        return `${DELIM}${escapeStack.length - 1}${DELIM}`;
    });

    const formattedStr = escapedFmtStr
        .replace(/y{4}|y{2}/g, m => pad(date.getFullYear(), m, true))
        .replace(/M{3}/g, m => _months[date.getMonth()])
        .replace(/M{1,2}/g, m => pad(date.getMonth() + 1, m))
        .replace(/M{1,2}/g, m => pad(date.getMonth() + 1, m))
        .replace(/d{1,2}/g, m => pad(date.getDate(), m))
        .replace(/H{1,2}/g, m => pad(date.getHours(), m))
        .replace(/h{1,2}/g, m => {
            const hours = date.getHours();
            return pad(hours === 0 ? 12 : hours > 12 ? hours - 12 : hours, m);
        })
        .replace(/a{1,2}/g, m => date.getHours() >= 12 ? 'PM' : 'AM')
        .replace(/m{1,2}/g, m => pad(date.getMinutes(), m))
        .replace(/s{1,2}/g, m => pad(date.getSeconds(), m))
        .replace(/S{3}/g, m => pad(date.getMilliseconds(), m))
        .replace(/[E]+/g, m => _days[date.getDay()])
        .replace(/[Z]+/g, m => timezone(date, m))
        .replace(/X{1,3}/g, m => timezone(date, m));

    const unescapedStr = formattedStr.replace(new RegExp(`${DELIM}\\d+${DELIM}`, 'g'),
        m => {
            const unescaped = escapeStack.shift();
            return unescaped.length > 0 ? unescaped : '\'';
        });

    return unescapedStr;
}

サンプルコード

関数の呼び出し方は以下のとおり

const date = new Date();

//パターンを指定してフォーマットできる
console.log(formatDate("yyyy年MM月dd日 HH時mm分ss秒SSS",date));
//->2020年02月18日 01時50分20秒789

//曜日も表示。日本語で曜日表示するときは曜日名を与える
console.log(formatDate("yyyy年MM月dd日(E曜日) HH時mm分ss秒SSS",date,{
    days:['','','','','','','']
}));
//->2020年02月18日(火曜日) 01時50分20秒789

//ISO8601形式(1)でフォーマット
console.log(formatDate("yyyyMMdd'T'HHmmssXX",date));
//->20200218T015020+0900

//ISO8601形式(2)でフォーマット
console.log(formatDate("yyyy-MM-dd'T'HH:mm:ssXXX",date));
//->2020-02-18T01:50:20+09:00

//RFC1123(RFC822)形式でフォーマット
console.log(formatDate("E, dd MMM yyyy HH:mm:ss Z",date));
//->Tue, 18 Feb 2020 01:50:20 +0900

//2019を「'19」みたいにフォーマットするときはyyにする
console.log(formatDate("EEE, MMM d, ''yy",date));
//->Tue, Feb 18, '20 01:50:20 +0900

//かぶる文字列を混ぜる場合はエスケープ(対象をシングルクォート"'"で囲む。シングルクォート自体は"''"で表現)
console.log(formatDate("'Hello!' It'''s' H:m:s.",date));
//->Hello! It's 1:50:20.

See the Pen eYNzpBO by Tom Misawa (@riversun) on CodePen.

パターン指定方法の詳細

  • パターンとしてH:m:sのように1文字指定した場合、時間が7時なら7のみ、17時なら17のように表示されるが、HH:mm:ssのように2文字指定した場合には7時なら07のように左から0埋め(zero padding)される。
  • パターン内に文字列を含めたい場合は 'Hello' hh:mm:ss のように文字列部をシングルクォート(')で文字列を囲むとエスケープされる。
  • 記法はJavaのSimpleDateFormatライク

パターン指定方法一覧

文字 内容
yyyy 2018
yy (年を2桁表示にする) 18
M 7
MM 月(0埋め) 07
MMM 月名(英語圏等向け) Jul
d 日付 17
dd 日付(0埋め) 17
a AM/PMのマーカー PM
H 時(0-23) 12
HH 時(0-23)(0埋め) 12
E 曜日 Tue
h 時(午前午後を1-12で) 12
hh 時(0埋め) 12
m 8
mm 分(0埋め) 08
s 56
ss 秒(0埋め) 56
S ミリ秒 789
Z RFC822形式のタイムゾーン +0900
X ISO8601形式のタイムゾーン(1) +09
XX ISO8601形式のタイムゾーン(2) +0900
XXX ISO8601形式のタイムゾーン(3) +09:00
' ' シングルクォートで囲むと中の文字をエスケープする
'' シングルクォート自体を表示したいとき

パターン指定いろいろな例

パターン
yyyyMMdd'T'HHmmssXX 20180717T120856+0900
yyyy-MM-dd'T'HH:mm:ssXXX 2018-07-17T12:08:56+09:00
E, dd MMM yyyy HH:mm:ss Z Tue, 17 Jul 2018 12:08:56 +0900
yyyy.MM.dd 'at' hh:mm:ss Z 2018.07.17 at 12:08:56 +0900
EEE, MMM d, ''yy Tue, Jul 17, '18
h:mm a 12:08 PM
hh 'o''''clock' a, X 12 o'clock PM, +09
yyyy年M月d日 H時m分s秒 2018年7月17日 12時8分56秒
yyyy年MM月dd日 HH時mm分ss秒 2018年07月17日 12時08分56秒
yyyyMMddHHmmssSSS 20180717120856789

ライブデモ(pen)

See the Pen ZEGWdKE by Tom Misawa (@riversun) on CodePen.

おまけ

間違いやすいAM,PMの概念

アメリカなど12時制が慣用されている国はhh:mm:ss(hは1-12を表すパターン)を使うことが多い。
以下のようにAM/PMは夜(midnight)の12時が12:00AM。昼(noon)の12時が12:00PMとなるので、実装にもそれを反映した。

24時制 12時制
0 12AM
1 1AM
2 2AM
3 3AM
4 4AM
5 5AM
6 6AM
7 7AM
8 8AM
9 9AM
10 10AM
11 11AM
12 12PM
13 1PM
14 2PM
15 3PM
16 4PM
17 5PM
18 6PM
19 7PM
20 8PM
21 9PM
22 10PM
23 11PM

まとめ

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

Cloud FunctionsでFirestoreの更新を特定のデバイスにpush通知する

Cloud Functions × Firestore × Firebase Cloud Messaging

Cloud Functionsはサーバレスコンピューティングサービスです。様々なイベントをトリガーにしてアクションを実行することができ非常にコストパフォーマンスに優れています。

今回はこのCloud Functionsを用いてFirestoreにデータが追加されたら特定のデバイスにpush通知が届くようにします。

Structure

通知を送信したいDBを設計します。今回は簡略化のためにnotificationsというコレクションにしています。
ここであらかじめテーブルにデバイスのidが紐付いているとやりやすいと思います。

notifications
│
├── notification
│   ├── text
│   └── users
.        ├── bJi8iJLw0ufF1HCO17v3b7UTVS53
.        .
.        .

Architecture

IMG_B2ADF5BEBE4E-1.jpeg

Steps

以下のことが既に完了している前提で実装していきます。多分この記事にたどり着くような人はコーディングで行き詰まった人だと思います。Firestoreのサンプルがここしかなかった。。。

  • Firebaseプロジェクトは作成済み
  • 必要なパッケージはインストール済み
  • クライアント側のリモート通知設定済み

1. Cloud Functionsをスタートする

まずプロジェクトを作成します

$ Firebase init


functionsディレクトリ内にあるindex.jsにコードを記述していきます。

2. コードを記述

index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.sendNotifications = functions.firestore.document('notifications/{notificationId}').onCreate(
    async (snapshot) => {
        // Notification details.
        const newValue = snapshot.data();

        const payload = {
            notification: {
                title:"新規ライブ",
                body: newValue.text + "の新しいライブが追加されました!",
                content_available: 'true',
                sound: "default",
                click_action: `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com`,
            }
        };

        const userIDs = newValue.users

        // Get the list of device tokens.
        userIDs.forEach(function (value) {
            admin.firestore().collection('fcmTokens').doc(value).get()
                .then(function (querySnapshot) {

                    let fcmToken = querySnapshot.data().fcmToken
                    admin.messaging().sendToDevice(fcmToken, payload);
                    return;
                }).catch(error => { return });
        });
    });

なんやこれポイント

サンプルに const allTokens = await admin.firestore().collection('fcmTokens').get(); ってあったのでてっきりFirestoreのfcmToken勝手にここに格納されてるのかと思いきや自分でDBつくるっぽかった。なので、userIdと紐付けて識別できるようにしました。

fcmTokens
│
├── fcmToken(id: uid)
│   └── fcmToken
.       
.    

まずfunctions.firestore.document()の引数にコレクションを指定します。今回はテーブルに新たなドキュメントが追加されたときに発火するようにしたいのでonCreate()を呼び出しています。
その他にもonUpdate()onDelete()などさまざまなトリガーがあります。詳しくはこちら

そして、payloadに通知の設定をします。

fcmTokensというコレクションからfcmtokenをゲットすることができます。RealtimeDBより便利な気がする。

最後にsendToDevice()で取得したユーザのトークンにまとめてpush通知を送信しています。

3. Deploy

$ firebase deploy

なんか死んでたらログを確認してちょこちょこ直していきましょう。

4. Test

ためしにPythonからFirestoreに新しいドキュメントを追加してみます

import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore


cred = credentials.Certificate()
firebase_admin.initialize_app()
db = firestore.client()

notification_data = {
    "text": "MY FIRST STORY",
    "users": ["gHrdAxQhFYdTmoFjgi8RX8PljXv1"]
}

db.collection(u'notifications').document().set(notification_data)


任意のデバイスにpush通知が来ればオッケー。たっくーTV見てたのは内緒。

IMG_9239.JPG

コードはこちら

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

pupetterを使ってAmazonの欲しいものリストの情報を取得する

概要

pupetterを使ってAmazon.co.jpから欲しい物リストをスクレイピングする手法のメモです。

下記のように欲しいものリストのIDを指定する事で、商品名と価格、商品IDを取得できる形にします。

サンプルコードのGitHubリンク

実装
  // 欲しい物リストのIDを指定
  const itemList = await amazonWishScraper.getProductInfo('hogehoge');
  console.log('itemList', JSON.stringify(itemList));

itemListの中身
[
    { "title" : "ProductA", "price": 3278, "productID": "429711111X" },
    { "title" : "ProductB", "price": 1234, "productID": "429711112X" },
]

背景

Amazonの欲しいものリストをスクレイピングで取得する例は、何点か見つかりましたが、紹介されているものの多くは、現在は使えないものでした。
というのも以前は、一定数商品が存在する場合にはページの切り替え行なっていましたが、現在はAjaxで同じページ内で画面をスクロールする事で非同期に読み込む方法に変更されたためです。

scroll.gif

こうなると、GETリクエストを投げてHTMLの解析だけではなく、ブラウザの操作をエミュレートして、スクロールを行うようなヘッドレスブラウザが必要になります。
今回は、puppeteerというヘッドレスブラウザを使用して実現します。

前提

MacOS 10.15.3(19D76)
VS Code 1.42.1
node.js v12.14.1
puppeteer 2.1.0

実装

実装方法の検討

画面スクロール

前述の通り、一定数以上の商品が登録されている場合にはページのスクロールを行いデータを読み出す必要があります。
puppeteerにはブラウザ内で任意のjsが実行可能なので、画面をスクロールさせる処理を実行します。

スクロールの停止(全商品読み込み完了の検知)

スクロールを続けた後に、全ての商品が読み込み終わった事を検知する必要があります。
欲しい物リストの末尾には、下図のようなリストの末尾を示す情報が表示されます。
この部分のタグが検知されたら、全ての商品の読み込みが終わったと判断します。

スクリーンショット 2020-02-18 1.57.34.png

該当箇所のタグ
<div class="a-row center-align-text full-width-element">
 <div id="no-items-section-anywhere" class="a-section a-spacing-none">
 リスト最後
 </div>
</div>

商品情報の取得

全ての情報が画面に描画されたのならば、後は通常のスクレイピングと同様にHTMLタグを解析するだけです。

実装コード

欲しいものリストサイトへのアクセス

puppeteerを初期化して、欲しいものリストのIDからURLを特定してアクセスします。
今回は表示される情報量を減らし、スクレイピングの効率を上げるためにデスクトップPCではなく、iPhone(スマホ)をシミュレートします。

この部分の処理は、puppeteerの基本的な動作方法ですので詳細は割愛します。

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 8'];
const urlbase = 'https://www.amazon.co.jp/hz/wishlist/ls/';

async function getProductInfo(wishListId) {
  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  try {
    const page = await initPage(browser);
    // 欲しいものリストのURLを開く
    await page.goto(urlbase + wishListId);
    // スクレイピング(中身は後述)
    return await scrapePage(page);
  } catch (err) {
    console.error(err);
    throw err;
  } finally {
    browser.close();
  }
}

async function initPage(browser) {
  const page = await browser.newPage();
  await page.emulate(iPhone);
  return page;
}

欲しいものリストサイトへのアクセス

実装方法の検討で説明した通り、

  1. リストの末尾が見つかるまで画面をスクロールする
  2. HTMLをquerySelectorで解析する

といったことを実施します。
querySelectorでのデータの切り出しは、生のHTMLを確認しながら商品ブロック単位で情報を取得するように対応しています。

async function scrapePage(page) {
  return await page.evaluate(async () => {
    // スクロールでの移動距離と待機間隔ms
    const distance = 500;
    const delay = 100;
    // リストの末尾が検知されない限りループする
    while (!document.querySelector('#no-items-section-anywhere')) {
      // 500pxずつスクロール移動して、100ミリ秒待機する
      document.scrollingElement.scrollBy(0, distance);
      await new Promise(resolve => {
        setTimeout(resolve, delay);
      });
    }

    // 全ての商品の表示が終わったらスクレイピングを実施
    const itemList = [];
    // 商品の情報のBOX単位でデータを切り出す
    [...document.querySelectorAll('a[href^="/dp/"].a-touch-link')].forEach(
      el => {
        const productID = el
          .getAttribute('href')
          .split('/?coliid')[0]
          .replace('/dp/', '');
        const title = el.querySelector('[id^="item_title_"]').textContent;
        let price = -1;
        const priceEle = el.querySelector('[id^="itemPrice_"] > span');
        if (priceEle && priceEle.textContent) {
          price = Number(priceEle.textContent.replace('', '').replace(',', ''));
        }
        itemList.push({
          price: price,
          title: title,
          productID: productID
        });
      }
    );
    return itemList;
  });
}

まとめ

簡単にはですが、puppeteerを使ってAmazonの欲しいものリストから情報を一覧取得する法法を紹介しました。
スクロールをシミュレートし、非同期で読み込まれるデータの取得を実現しています。
こういったユーザの操作が必要となる処理に関しても、puppeteerを使う事で簡単に実現ができました。

サンプルコードのGitHubリンク

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

axiosでエラーになった時のリトライ処理をinterceptorsで実行する

TL;DR

エラーオブジェクトにリクエスト時の設定が含まれているので、これを axios.request メソッドに渡す事で再実行することが出来ます

import axios from 'axios'
const client = axios.create({ baseURL: 'https://hoge.example/api/v1/' })

client.interceptors.response.use(
  (response) => {
    return Promise.resolve(response)
  },
  (err) => {
    // json文字列になっているままだとエラーになる為
    if (typeof config.data === 'string') {
      config.data = JSON.parse(config.data)
    }

    return axios.request(err.config)
  }
)

リトライの無限ループ対策

configにプロパティを追加して、リトライ回数を見るように実装しました

import axios from 'axios'

const LIMIT_RETRY_COUNT = 2

client.interceptors.response.use(
  (response) => {
    return Promise.resolve(response)
  },
  (err) => {
    if (config.retryCount > LIMIT_RETRY_COUNT) {
      return Promise.reject(err)
    }

    if (Number.isInteger(config.retryCount)) {
      config.retryCount += 1
    } else {
      config.retryCount = 1
    }

    return axios.request(err.config)
  }
)

ただしaxiosのv0.19.0ではこのやり方(custom config)が効かなくなっているので、もしこのバージョンを使用している場合はバージョンを上げる必要があります。
0.18系は問題ありません。

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