20201027のJavaScriptに関する記事は30件です。

Vue.jsで、いろんなやり方の画像プレビュー方法

ref属性から画像を取得して、表示する

// refからアップロード
input(type="file", ref="file", @change="leftSetImage")
v-img(:src="image.source")
// refから一時的なURLに変換して取得
leftSetImage() {
  const files = this.$refs.file;
  const fileImg = files.files[0];
  if (fileImg.type.startsWith("image/")) {
    this.image.source = window.URL.createObjectURL(fileImg);
  }
},

イベントオブジェクトから画像を取得して、表示する

一時的なURLでプレビュー表示

// イベントオブジェクトからアップロード
input(
  type="file",
  @change="setImageLeft($event)"
)
v-img(:src="office.left_image.image", type="submit")

// イベントオブジェクトから、一時的URLに変換して取得
setImageLeft(e) {
  const file = (e.target.files || e.dataTransfer)[0];
  this.LeftUploadedImage = file;
  if (file.type.startsWith("image/")) {
    this.office.left_image.image = window.URL.createObjectURL(file);
  }
},

base64に変換して、プレビュー表示

今までのやり方だと、ブラウザを放置して更新したり、そのデータを画像を保存したりした時の再表示の動作ができなくなるということが起きる。

base64に変換する場合は、そのような事象は起きない。

// イベントオブジェクトからアップロード
input(
  type="file",
  @change="setImageRight($event)"
)
v-img(:src="office.right_image.image", type="submit")

// イベントオブジェクトから、base64に変換して取得
setImageRight(e) {
  const file = e.target.files[0] || e.dataTransfer.files;
  const reader = new FileReader();
  reader.onload = (e) => {
    this.office.right_image.image = e.target.result;
  };
  reader.readAsDataURL(file);
},
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsならではの、いろんなやり方の画像プレビュー方法

ref属性から画像を取得して、表示する

// refからアップロード
input(type="file", ref="file", @change="leftSetImage")
v-img(:src="image.source")
// refから一時的なURLに変換して取得
leftSetImage() {
  const files = this.$refs.file;
  const fileImg = files.files[0];
  if (fileImg.type.startsWith("image/")) {
    this.image.source = window.URL.createObjectURL(fileImg);
  }
},

イベントオブジェクトから画像を取得して、表示する

一時的なURLでプレビュー表示

// イベントオブジェクトからアップロード
input(
  type="file",
  @change="setImageLeft($event)"
)
v-img(:src="office.left_image.image", type="submit")

// イベントオブジェクトから、一時的URLに変換して取得
setImageLeft(e) {
  const file = (e.target.files || e.dataTransfer)[0];
  this.LeftUploadedImage = file;
  if (file.type.startsWith("image/")) {
    this.office.left_image.image = window.URL.createObjectURL(file);
  }
},

base64に変換して、プレビュー表示

今までのやり方だと、ブラウザを放置して更新したり、そのデータを画像を保存したりした時の再表示の動作ができなくなるということが起きる。

base64に変換する場合は、そのような事象は起きない。

// イベントオブジェクトからアップロード
input(
  type="file",
  @change="setImageRight($event)"
)
v-img(:src="office.right_image.image", type="submit")

// イベントオブジェクトから、base64に変換して取得
setImageRight(e) {
  const file = e.target.files[0] || e.dataTransfer.files;
  const reader = new FileReader();
  reader.onload = (e) => {
    this.office.right_image.image = e.target.result;
  };
  reader.readAsDataURL(file);
},
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firefox/Chrome拡張機能のポップアップ内ではOnclickは働かないので他の方法を使おう

概要

拡張機能アイコン(ツールバーにあるやつ)をクリックして出てくるポップアップは、browser_action内のdefault_popupにHTMLファイルを指定することによって作る。

manifest.json
"browser_action":{
    "default_icon": {
        "48" : "48.png"
    },
    "default_popup": "popup.html"
}

だがこのHTMLファイル内においては、Onclickは使えない。
ポップアップ内において、インラインのJavaScriptは許可されていないのだ。

対処法

DOMContentLoadedを使う。
まずHTMLファイル内のOnclickを動作させたかったタグにIDを割り振る。
以下に例を示す。

<!--置換前-->
<a Onclick="hoge()">hoge</a>

<!--置換後-->
<a id="hoge">hoge</a>

JavaScriptのコードは外部ファイル(ここではhoge.jsとする)に以下のように書く。HTMLファイル内に<script src="hoge.js"></script>を書くのを忘れないよう注意。

hoge.js
document.addEventListener("DOMContentLoaded", function(){
    var hoge = document.getElementById("hoge");
    hoge.addEventListener("click", function(){
        // ここに処理を書く
    });
});

ちなみにこのJSファイルはmanifest.json内のcontent_scriptsに書かなくても動く。

参考

https://stackoverflow.com/questions/13591983/onclick-or-inline-script-isnt-working-in-extension
https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Content_Security_Policy

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

JavaScriptの魅力をなるべく簡単にまとめてみた!

この記事は、初心者である私が「jsの魅力、伝えたいなぁ」と思って書いた記事です。
一応初心者にも理解できるようにはしていますが、多分以下の知識を持っている人じゃないと完全に理解できないと思います。

  • HTMLをある程度記述できる
  • ある程度ITについての知識がついている(P検3級程度)

これがわからなくても、できる限りわかるようにはしているので、わからない部分はネットで調べてより詳しい情報をみて理解していってください()


さて、先ほども言ったように、私は初心者な訳です。
初めてプログラム言語を触ったのは、今から1年半ぐらい前になりますかね。まぁ、プログラム言語を学ぶにあたって、自分が使うプログラム言語を決める必要があるのですが、ネット上では
「JavaScript(以下、js)がおすすめだよ!」
という声が多いと感じましたし、私も一番最初はjsをいじりました。


まぁしばらくはいろんなプログラム言語を触った訳なんですが、やっぱりjsに戻ってきましてね。せっかくだし、jsの参考書でも購入するかということで、買ってみたんですが、その参考書が結構当たりだったんです。今まで当たり前だと思ってたものが違ってたり、その逆もあったり。また、コードの行数を減らすためのテクニックとか、色々なことが書いてあったんです。


そこで今回は、その参考書からも多少抜粋して、jsの代表的な特徴について紹介してきます。

1,書いてすぐに動かせる

よく、「jsは初心者向けプログラム言語だよ」と言われるのですが、その根拠のひとつがこれだと感じます。
例として、C言語はコンパイラ言語ですので、一度書いたコードはコンパイルして、その後、コンパイルし終わったファイルを実行する訳ですが、ここまでの作業がめんどくさいという人もいるかも知れません。
もっと極端な話、C言語はコンパイラとかデバッガとか、IDE(統合開発環境)が必要になってくるんですが、jsはこれといったものは必要ありません(厳密には違いますが。後で説明が入ります)。まぁ、エディタぐらいは必要でしょうけど。


じゃあjsはどうやってコードを動かしてるんだ、って話になると思うんですよね。
答えは、ブラウザの中のエンジンで動かしているんです。
詳しく言っちゃうと、例えば皆さんが使っているであろうsafariやgoogle Chrome、fire foxなど、俗にいう「Webブラウザ」がありますよね?実は、そのブラウザの中にはjsを実行するためのエンジンがあるのです。それはブラウザによって変わってくるので、例えばsafariでできることが、google Chromeになるとできないなど、ちょっとした差はあります。
まぁ要するに、ブラウザの中でjsが動くから、コンパイラとかも必要ないよということです。
さらにいっちゃえば、jsにとってのIDEはブラウザということになります。

2,動的型付け言語

初心者からしてみたら、「何その単語」となりますよね。
簡単にいうと、動的型付け言語とは、
プログラムを動かす時にデータ型決めておくよ
という言語を指します。
つまり、データ型をつける必要がないということです。これが、多分「初心者におすすめのプログラム言語」と言われる所以なのではないでしょうか。


ここからは自論なのですが、正直この機能については初心者向けとは思えません。というのも、データ型を指定しないということは、その変数はどんなデータ型にもなれるということです。それが論理エラーを招く可能性もありますし、プログラマーからしてみても、「この変数はこのデータ型を持っているのか」というのが視覚的に判断できないので、混乱が生じる可能性もあります。


初心者だからこそ、データ型について詳しく触れる必要があるのではないのか、と思ったり思わなかったり

3,HTMLの操作が非常に楽

先ほども言ったように、jsはWebブラウザで動作するプログラム言語です。
HTML1とはWebブラウザにおいて、文書を生成するためのマークアップ言語なのですが、同じWeb上で動作するので、HTMLに対する操作が非常に楽です。
例えば、
「ボタン"userButton"が押されたら、id="Text"のpタグの文章を変える」
という処理は、以下の通りで実現できます。

test1.js
document.getElementById("userButton").onclick = function{    //ボタンを押したら...?
    //id"Text"のpタグに文字列を代入
    document.getElementById("Text").innerHTML = "Hello JavaScript!";
}

これで完了です。至って簡単ですね。
「は?簡単じゃないし?」と思う方もいるとは思いますが、しばらくjsに触れればこれも理解できます。

4,イベント処理

そもそもイベントとは、ユーザがサイトに対して何かをしたアクションのことを指します。
例えば、
「"A"のキーが押されたらメッセージを出力したい」
という処理があるとすると、そのコードは以下の通り。

test2.js
document.addEventListener("keydown",event =>{
    let key = event.key;        //キーの値を取得
    if(key == "A") console.log("Aが押されました!!");    //キーがAならば
});

こんな感じで、簡単にイベント処理を行うことができます。ついでに、test1.jsの、「ボタンが押されたら....」という処理も、イベント処理の一種になります。

まとめ

jsは様々な機能がありますが、基本はWeb環境で動くことのできる、Web特化のプログラム言語です。ただ、Web以外にも、様々な場所で活躍している、プログラム言語の代表的な言語になります。また、ここで紹介できなかったjsの魅力もあります。詳しくは下にある参考文献を購入して見てみてください。


昔は、「toy language(おもちゃの言語)」と言われていたらしいですが、今はそんなことないので、ガシガシ勉強しましょう!

参考文献:初めてのJavaScript 第3版 | オライリー


  1. HTMLとは、「Hyper Text Markup Language」の略で、Webの文書の書式を定義する為の、一種のマークアップ言語です。簡単にいうと、Web上の文書を作成する為の言語です。jsをいじるならばこの言語も知っておいた方がいいですね 

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

Javascript データ型の基本

はじめに

Javascript初心者です。
データ型を学んでいて、重要だと思ったことをアウトプットしていきます。
なお、筆者はJava,c#,rubyをちょっとだけ学んでいるので、そういった言語との
違いなどについても述べていければと思います。

※ 注意
記事の内容は誤っている箇所がある可能性があります。
お気づきになられた方はご指摘頂けると大変ありがたいです。
何卒よろしくお願いします。

Javascriptのデータ型の種類

※ECMAScript6~の型

  • string
  • number
  • boolean
  • null
  • undifined
  • symbol
  • object

string~symbolはプリミティブ型、基本型と呼ばれています。
objectはオブジェクト型、参照型と呼ばれています。
プリミティブ、オブジェクトの違いについてはほとんどのプログラミング言語で
定義が似ていると思うのでググってみてください。
それぞれ解説していきます。

string

文字列を扱う文字列型です。
ダブルクォーテーション(""),シングルクォーテーション('')で囲んで表現されます。

number

数値を扱う数値型です。

ちなみに、'数値'と'数字'は多くの場合、別物とみなされます。

boolean

正か負か、又は0か1かを表す真偽値型です。(論理型とも呼ばれます)
true,falseという名前で表現されます。

null

「何も存在していない」ことを表現します。
※多言語では、”存在しないオブジェクトへの参照”などと表される場合もありますが
Javascriptにおいてnullはそうではありません。

undifined★

「定義されていない」を表現します。
Javascript固有の型となっています。"宣言したけど値は未定義"
のような変数に対してundifinedが割り当てられます。

symbol★

ユニークで不変な「識別子」を表現します。
Object型の中のデータ構造に使用されたりします。
Rubyでいうsymbolとは異なります。
※ECMAScript6より登場しました。

object

データおよびそのデータを操作する命令が入ったデータ構造です。
その名の通り、現実の「モノ」に例えられます。
上記のプリミティブ型以外でnewキーワードで変数に格納されるものがこれにあたります。

ちょっとしたTips

string ⇄ numberのデータ型変換
paizaとかでも意外と変換忘れがちなので注意したいところです...

// ■ string → number
let str = "1000"; 
let num1 = +str;  // 方法①
let num2 = Number(str); // 方法②
console.log(typeof(num1),typeof(num2));
//=> "number" "number"

// ■ number → string
let num = 1000;
let str1 = num + "";
let str2 = String(num);
console.log(typeof(str1),typeof(str2));
//=> "strng" "string"

参考

javascript.info_プリミティブのメソッド
MDN web docs_データ型とデータ構造

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

カウントダウン機能の作成

カウントダウン機能の作成

初期値120秒から1秒ずつカウントダウンする機能の作成。

<body onload="init()">
  <p>COUNT DOWN: <span id="time"></span></p>
</body>

ページを読み込んだときにperformance.nowで変数startTimeにその時点の時間をミリ秒で代入します。
setIntervalメソッドで1秒ごとに関数tickを呼び出します。

  <script>
    let startTime;

    function init() {
      startTime = performance.now();
      setInterval(tick, 1000);
    }

tick関数内は、まず変数nowTimeに関数tickが呼び出されたときの時間をperformance.nowで代入します。
body要素内のspan要素を取得してページを読み込んだときから経過した時間を120秒からカウントダウンする形式で表示します。
performance.nowはミリ秒で取得するので、1秒ごとにカウントダウンするように差分の時間を1000で割ります。

    function tick() {
      const nowTime = performance.now();
      const time = document.getElementById("time");
      time.textContent = 120 + Math.floor((startTime - nowTime) / 1000);
    }
  </script>

下記のコードをコピーしてファイルに貼り付ければ試せます。

<!DOCTYPE html>

<html>

<head>
  <meta charset="utf-8">

  <script>
    let startTime;

    function init() {
      startTime = performance.now();
      setInterval(tick, 1000);
    }

    function tick() {
      const nowTime = performance.now();
      const time = document.getElementById("time");
      time.textContent = 120 + Math.floor((startTime - nowTime) / 1000);
    }
  </script>
</head>

<body onload="init()">
  <p>COUNT DOWN: <span id="time"></span></p>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mapbox で点をスムースに移動させる

Mapbox で移動する点を描く では、連続した点を単純に描画してみました。
が、パカパカ移動するので何だかつまらないしありきたりです。
なので、Uber アプリのようにもっとスムースにシンボルを移動させます。

完成形は↓です。

cap.gif

はじめに

で例が示されているので、これを改造します。

また、この機能を実現させるのに、turf.js という地理空間図形や座標の計算を行ってくれるライブラリを使用します。

index.js

処理の要点は2つです。

1.requestAnimationFrame を使ってフレーム毎に描画を行う
2. ルートの「開始点からnメートル進んだ点」を turf.js で計算する

移動する速度(km/h)はここでは固定とします。
requestAnimationFrame で描画のハンドラ関数を再帰で繰り返し呼び出し、前回の描画から今回までにかかった時間(ミリ秒)で何m進むのかを計算します。
進んだ距離を加算しておき、turf.js を使って「開始点から Nm 進んだ座標」を取得して、DataSource を更新→画面を更新します。

const waypoints = [
  [136.53890132904053,34.844644908488974],
  [136.54332160949704,34.840242190008105],
  // 中略 //
  [136.5387511253357,34.84480340196252]
];

// Symbol Layer
const symbolData = {
  'type': 'FeatureCollection',
  'features': []
};
map.on('load', function() {
  map.loadImage(
    'https://img.icons8.com/material/32/0000FF/marker--v1.png',
    (error, image) => {
      // 中略 //

      time = new Date().getTime();
      animate();
    }
  );
});

const line = turf.lineString(waypoints);
const lineLength = turf.length(line, { units: 'meters' });

const kph = 600; // 600km/h
let time = null;
let distance = 0;

function animate() {
  // 1フレームで移動する距離を算出
  const now = new Date().getTime();
  const offset = now - time;
  time = now;
  const movingInMeters = ((kph / 60 / 60 / 1000) * offset) * 1000;

  // 移動距離加算
  distance = distance + movingInMeters;

  // 移動した点を計算
  const options = {units: 'meters'};
  const movedPoint = turf.along(line, distance, options);

  // 更新
  symbolData.features.splice(0);
  symbolData.features.push({
    'type': 'Feature',
    'geometry': {
      'type': 'Point',
      'coordinates': movedPoint.geometry.coordinates
    }
  });
  const dataSource = map.getSource('point');
  dataSource.setData(symbolData);

  // repeat
  if (distance >= lineLength) {
    distance = 0;
  }

  requestAnimationFrame(animate);
}

DEMO

時速600km の設定なので F1 よりも相当速いです。

See Also

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

Mapbox で移動する点を描く

よくある「スマートフォンなどの位置情報を受信して、その位置を地図上に示す」を Mapbox で行います。

Mapbox でのピン表示について考える で書いたとおり、「頻繁なアニメーションが伴う場合は Symbol Layer」がふさわしいと考えるので、今回はそちらを使用します。

とは言っても、ここではダミーデータとして緯度経度のリストを走査してシンボルを移動させているだけなので、ただのシンボルの再描画の例になります。

index.js

事前準備が長いですが、重要なのは最下部 setInterval の中で行っている dataSource.setData() で、これを行うと地図(上のレイヤ)が更新されます。

ところどころ省略しているので、完全版は DEMO を見てください。

const map = new mapboxgl.Map({
  container: 'map',
  center: [136.53890132904053,34.844644908488974],
  zoom: 14,
  style: {
    //  中略 //
  },      
});

const waypoints = [
  [136.53890132904053,34.844644908488974],
  [136.54332160949704,34.840242190008105],
  // 中略 //
  [136.5387511253357,34.84480340196252]
];

// Symbol Layer
const symbolData = {
    'type': 'FeatureCollection',
    'features': []
};
map.on('load', function() {
  map.loadImage(
    'https://img.icons8.com/material/32/0000FF/marker--v1.png',
    (error, image) => {
      if (error) {
        throw error;
      }
      map.addImage('iconA', image);
      map.addSource('point', {
        'type': 'geojson',
        'data': symbolData
      });
      map.addLayer({
        'id': 'points',
        'type': 'symbol',
        'source': 'point',
        'layout': {
          'icon-image': 'iconA',
          'icon-size': 1,
          'icon-allow-overlap': true,
          'icon-ignore-placement': true,          
        }
      });

      let counter = 0;
      setInterval(() => {
        symbolData.features.splice(0);
        symbolData.features.push({
          'type': 'Feature',
          'geometry': {
            'type': 'Point',
            'coordinates': waypoints[counter % waypoints.length]
          }
        });

        const dataSource = map.getSource('point');
        dataSource.setData(symbolData);

        counter = counter + 1;
      }, 1000);      
    }
  );
});

DEMO

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

Mapbox でのピン表示について考える

Mapbox で地図上にいわゆる「ピン」を表示する、またピンをクリックすると「吹き出し」を表示させる、Google Maps SDK ではマーカー と呼ばれる機能を実現する方法がいくつか(2つ)あります。それぞれの特徴と使い分けについて考えます。

1. マーカー(Marker)

一つ目は Mapbox でも Marker と呼ばれる機能です。

以下は、Mapbox のドキュメントへのリンクです。

Mapbox の「マーカー」は、Google Maps などのそれと同じく、色や形状が変更でき、吹き出し(ポップアップ)を関連付けることができ、ドラッグ可能にすることができます。

2. シンボルレイヤー(Symbol Layer)

二つ目の選択肢は Symbol Layer です。
が、実際には Symbol Layer という機能名があるわけではありません。

まず Mapbox には Layer という PhotoShop などのペイントソフトと似たような概念があります。複数の Layer が積み重なって、「地図」が描画されます。

背景地図も「ひとつの Layer」であり、Mapbox で OpenStreetMaps を表示するだけindex.js を見ると styles: [] であることが分かります。

Layer には "fill", "line", "symbol", "circle", "heatmap", "fill-extrusion", "raster", "hillshade", "background" の種類( type )があり、上記の背景地図は "raster" です。

Symbol Layer とは、「type が symbol である Layer」のことです。

Layer に表示するデータを示すものが「ソース(Source)」です。ソースにも様々な種類がありますが、最もよく使われるが GeoJSON です。

Symbol Layer を表示するのに必要なデータは、GeoJSON.Point の地物群ということになります。

以下は、Mapbox のドキュメントへのリンクです。

一見すると、Marker と同じことが行えるように見えます。
「同じことが行えるかどうか?」ならば Yes ですが、その実装には多くのコード量を伴うものもあります。

例えば、「シンボルをクリックしたらポップアップを表示させる」を行いたい場合、Marker では .setPopup() の1行で済みますが、Symbol Layer の場合 は、「Symbol がクリックされたというイベントハンドラで、Popup を表示する」というコードをわざわざ記述しなくてはなりません。「クリック可能を示すマウスカーソルの変更」も同じくです。

ドラッグ可能なシンボル(Create a draggable point) も、そのコードを見れば相当に面倒なことを行っていることがわかるでしょう。

以上から、Symbol Layer は、Marker よりは自由度が少なく、より「地図データ」に近いものと言えそうです。

Marker と Symbol Layer の使い分け

選択状態を示すなら Marker

最も一般的なユースケースだと思います。
「地図上のスポットをクリックしたら吹き出しが表示される」という事を簡単に実現したいならば、Marker がよいです。

データ量が多い場合は Symbol Layer

地図データに近い存在である「Symbol Layer」は、Marker よりも大量のレンダリングを高速に行うことができます。

これは Marker と Symbol Layer のレンダリングのされ方の違いに起因します。

Marker は、HTML要素がそこに配置されます。つまり Marker の追加=DOM Tree への任意のHTMLタグの追加、となります。

一方で Symbol Layer では、HTML要素は生成されません。Symbol は canvas に直接描画されています。

DOM Tree を変更するよりも Canvas に WebGL で描画する方が一般的には高速になりますから、Symbol Layer の方が大量のデータを高速に描画できるということになります。

次の DEMO は、大量の Marker または Symbol を追加して描画する例です。
2000個くらいになると、Marker の方がモタつきを感じるようになりますが Symbol はあまり変化を感じません。
10000個以上では、Marker はほぼフリーズ状態ですが、Symbol の方はまだ耐えられる印象です。

頻繁なアニメーションが伴う場合は Symbol Layer

複数の移動体をリアルタイムトラッキングするなど、画面の書き換えなアニメーションが頻繁に発生するユースケースでも、描画コストの低い Symbol Layer が適しています。

複雑かつ動的なピンを表示したいなら Marker

前項で示した通り、Marker は HTML要素 であり、Symbol Layer は canvas への直接描画です。
Symbol Layer へ描画するピンの形状や色は、事前に map.addImage() で登録しておく必要があります。また、登録できるのは基本的には画像のみです。

Marker は HTML要素ですから表現の自由度は高いです。例えば地図上に直接グラフのようなものを表示したい場合は、Marker の方が適しているでしょう。ただし、前述のように描画コストには注意する必要があります。

次の DEMO は、Marker の見た目に HTML の Table を使用した例です。

データを種類毎に管理したいなら Symbol Layer

「コンビニ群」「レストラン群」「ガソリンスタンド群」など、ピンデータを何らかのカテゴリ毎に管理したい場合は、Symbol Layer が向いていると思います。
それぞれを Layer/Source とすれば、Layer 単位での表示・非表示切り替え、描画順序(手前/奥)の切り替えが Style により容易にできます。
ここまで来るとデータ量は多くなりそうですから、必然的に Symbol Layer にせざるを得ないとも言えます。

One more thing...

さらに大量の地点情報を表示したい場合、「タイル画像を生成してしまう」という選択肢もあります。この場合、静的なデータを生成することになるので頻繁に変化するコンテンツには向きませんが、一定間隔でデータを更新するバッチ処理などで済む場合は有用と思われます。

MapBox では、Mapbox Tiling Service(MTS)」というサービスを提供しており(現在は public beta)、タイル画像、しかもベクトルタイルデータを作成するツールキットが提供されるようです。筆者もまだ使ったことがないので、使う機会があったら記事を書きたいと思います。

まとめ

「データとしての地点情報」ならば、Symbol Layer にしておくのが良いかと思います。
その中で、特定の機能(選択状態や強調状態)を示すものだけを Maker とするのが良いと思います。
Google Maps SDK には Symbol Layer(というか Layer) という概念がないので、知らないと大量の Marker を登録してしまう事になりそうですが、それは避けた方が良いと思います。

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

Mapbox で初期表示位置(中心とズームレベルまたは範囲)を指定

初期位置を指定したい、しかもクエリパラメータなどで動的に、「中心とズームレベル」または「指定した範囲」を初期位置としたい場合は、new mapboxgl.Map({...}) で直接指定するのではなく、変数にしておいて center, zoom のペアまたは bounds を設定するとよいです。

centerLngLatLike であり [経度, 緯度] という degree の配列、boundsLngLatBoundsLike であり、LngLatLike の配列([[西南], [東北]])です。

index.js

const opt = {
  container: 'map',
  style: {
    // 省略 //
  },      
};

// ランダム(時刻)で、富士山(中心とズームレベル) か 琵琶湖(範囲) を切り替え
if (new Date().getTime() % 2 == 0) {
  opt.center = [138.73072, 35.36286];
  opt.zoom = 13;
} else {
  opt.bounds = [[135.856934, 34.981346], [136.282654, 35.531947]]; // [[west, south], [east, north]]
//  opt.bounds = [135.856934, 34.981346, 136.282654, 35.531947]; // [west, south, east, north] でも ok
}

const map = new mapboxgl.Map(opt);

DEMO

See Also

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

Mapbox で OpenStreetMaps を表示するだけ

Mapbox のアクセストークンを取得して埋め込むのも面倒なアナタに。

import する js/css

https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.js
https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css

index.html

<div id="map"></div>

index.js

const map = new mapboxgl.Map({
  container: 'map',
  center: [134, 35],
  zoom: 10,
  style: {
    version: 8,
    sources: {
      OSM: {
        type: "raster",
        tiles: [
          "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
        ],
        tileSize: 256,
        attribution:
        "OpenStreetMap",
      },
    },
    layers: [{
      id: "BASEMAP",
      type: "raster",
      source: "OSM",
      minzoom: 0,
      maxzoom: 18,
    }],
  },      
});

DEMO

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

Rubyの条件分岐(case,while,無限ループ,break)

Rubyの条件分岐

case文

条件分岐を表現するための文法。複数の条件を指定する時に、if文のelsifを重ねるよりもシンプルにコードを書くことができる

sample
case 対象のオブジェクトや式
when 値1
 # 値1に一致する場合に実行する処理
when 値2
 # 値2に一致する場合に実行する処理
when 値3
 # 値3に一致する場合に実行する処理
else
 # どれにも一致しない場合に実行する処理
end

while文

繰り返し処理を行うためのRubyの構文。指定した条件が真である間、処理を繰り返す

sample
number = 0

while number <= 10
 puts number
 number += 1
end

# ターミナル出力結果
# 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
# 10

無限ループ

処理が永遠に繰り返されること

sample
number = 0

while true
 puts number
 number += 1
end

# ターミナル出力結果
# 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
# 10
# .
# .
# .

上記のコードは条件式の部分にはじめからtrueと書くことによって意図的に無限ループを発生させている

break

eachメソッドやwhile文などのループから脱出するために使われる

sample
number = 0

while number <= 10
 if number == 5
   break
 end
 puts number
 number += 1
end

# ターミナル出力結果
# 0
# 1
# 2
# 3
# 4

このようにif文などの条件分岐とbreakを使うと、特定の条件のときにループを脱出することができる

現場からは以上です!

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

vue.jsを初めて学ぶ ② [テンプレート構文]

前回までの記事

① hello world

目次

1.テンプレート構文とは?
・newVueテンプレート
3. thisを理解する
4. v-から始まるディレクティブを理解する
5. v-bindとは?
6. v-bindのオブジェクト化
7. v-onとは?
8. イベント修飾子とは?
9. キー修飾子とは?
10. v-onの省略記法
11. v-modelによる双方向データバインディング
12. computedプロパティで動的なデータを扱う
13. methodとcomputedプロパティの違い
14. watchプロパティで、非同期処理を行う
15. ()の使い方
16. class要素をデータにバインディングする(オブジェクト)
17. class要素をデータにバインディングする(配列)

テンプレート構文とは

3行のテンプレート構文。vueがこれを読み込み、htmlを出力。ブラウザがhtmlを見る。
new Vue({
    el: '#app',
    data: {
      message: 'Hello World'
    }

各プロパティの説明

  • el: マウント先の要素を指定するブロック
  • data: 変数を保持するブロック
  • methods: 関数を保持するブロック

テンプレート構文の種類

newVueテンプレートとは

index.js
new Vue({
    el: '#app',
    data: {
<!-- [1]文字列を出力 -->
      message: 'Hello World'

<!-- [2]和を出力 -->
      number: 3

<!-- [3]三項演算子 -->
      ok: true
    }
<!-- [4]メソッドを出力 -->
  methods: {
    sayHi: function() {
     return: 'Hi';
    }
   }
  })
index.html
<div id = "app">
<!-- [1]文字列を出力 -->
  <p>{{message}}</p>

<!-- [2]和を出力 -->
  <p>{{number + 3}}</p>

<!-- [3]三項演算子 -->
  <p>{{ok ? 'yes' : 'no'}}</p>

<!-- [4]メソッドを出力 -->
  <p>{{ sayHi() }}</p>
</div>

三項演算子とは?
唯一の、3 つのオペランドをとる演算子です。条件に続いて疑問符 (?)、そして条件が真値であった場合に実行する式、コロン (:) が続き、条件がfalsyであった場合に実行する式が最後に来ます

  • ただし素のjavascriptが出力されるのは、単一の式だけに注意。

thisを理解する。

  • newVueテンプレート内の変数message
  • sayHiメソッド内で、呼び出してみる。
index.js
new Vue({
  el: '#app',
  data: {
    message: 'Hello,World!!'
},
methods: {
  sayHi: function() {
<!--これはエラーになる-->
      return message;
<!--正しくは、thisが必要-->
      return this.message;
    }
  }
})
index.html
<div id = "app">
  <p>{{ sayHi() }}</p>
</div>

thisとは、Vue.jsのインスタンスにアクセスするために必要なプロキシ

v- から始まるディレクティブを理解する。

v-bindを使用して、Googleへのリンクを作成する

  • v-bindを理解する。
index.html
<div id = "app">
    <a href="url">Google</a>
</div>
index.js
new Vue ({
  data: {
    url: 'https://google.com',
  }
})

これでは、404エラーとなってしまう。

  • v-bind使用例 text:index.html <div id = "app"> <a v-bind:href="url">Google</a> </div>
index.js
new Vue ({
  data: {
    url: 'https://google.com',
  }
})
  • v-bindを省略可能
index.html
<div id = "app">
    <a :href="url">Google</a>
</div>
index.js
new Vue ({
  data: {
    url: 'https://google.com',
  }
})

v-bindをオブジェクト化して使用する。

  • 要素が[href][class][id]といった複数付与したい場合。オブジェクト化が最適
index.html
<div id = "app">
    <a v-bind="objectGoogle">Google</a>
</div>
index.js
new Vue ({
  data: {
    objectGoogle: {
      href: 'https://google.com',
      class: 'sample',
      id: 31
    }
  }
})

検証からaタグリンクを参照すると、
スクリーンショット 2020-10-27 13.40.08.png
無事適用されている。

v-onでイベント発生時に、処理を実装。

ボタンクリックで数字がカウントされる処理を実装してみる。

v-on:に指定するのは、clickイベント

index.html
<div id = "app">
    <p>現在{{number}}回クリックされている</p>
    <button v-on:click="countUp">カウントアップ</button>
</div>
index.js
new Vue ({
      el: '#app',
      data: {
        number: 0
      },
      methods: {
        countUp: function() {
          this.number += 1
        }
      }
    })

v-on:DOMイベント="関数名"とhtml側の記載はシンプルにしておくのが良い。

マウスをかざすた所の、XとY軸を表示する

v-on:に指定するのは、mousemoveイベント

index.html
<div id = "app">
    <p v-on:mousemove="changeMousePosition">マウスを載せてください</p>
    <p>X:{{x}},Y{{y}}</p>
</div>
index.js
new Vue ({
      el: '#app',
      data: {
        x:0,
        y:0,
      },
      methods: {
        changeMousePosition: function(event) {
          this.x = event.clientX;
          this.y = event.clientY;
        }
      }
    })

関数changeMousePositionの引数には、event(testなど他のテキストでも動作するが、)を指定するのがルール。

イベント修飾子

  1. stopPropagationだったが、stop
  2. preventDefaultだったが、prevent  で良い。

v-onイベントで発火する関数処理をstopさせる。

  • 関数changeMousePositionをstopさせる。
index.html
<div id = "app">
    <p v-on:mousemove="changeMousePosition">マウスをかざしてください
      <span v-on:mousemove="noEvent">マウスをかざしても反応させない。</span>
    </p>
    <p>X:{{x}},Y{{y}}</p>
</div>
index.js
new Vue ({
      el: '#app',
      data: {
        x:0,
        y:0,
      },
      methods: {
        changeMousePosition: function(event) {
          this.x = event.clientX;
          this.y = event.clientY;
        },
        noEvent: function(event) {
          event.stopPropagation()
      }
    })
  1. spanタグにnoEvent関数を追加
  2. noEvent関数に、stopPropagationという処理をstopさせる命令を追加

vue.jsのすごい所

  • vue.jsでは、処理を止めるstopPropagationを簡易的に設定できる。
  • html側のv-on:mousemove以降の関数指定で、stopを命令するだけ!
index.html
<div id = "app">
    <p v-on:mousemove="changeMousePosition">マウスをかざしてください
      <span v-on:mousemove.stop>マウスをかざしても反応させない。</span>
    </p>
    <p>X{{x}},Y{{y}}</p>
</div>
index.js
new Vue ({
      el: '#app',
      data: {
        x:0,
        y:0,
      },
      methods: {
        changeMousePosition: function(event) {
          this.x = event.clientX;
          this.y = event.clientY;
        }
    })

stop便利!

v-onイベントで発火するデフォルト処理をpreventさせる。

  • googleへのリンクタグをクリックしても飛ばないように設定。
index.html
<div id = "app">
    <a v-on:click="noEvent" href="https://google.com">google.com</a>
</div>
index.js
new Vue ({
      el: '#app',
      methods: {
        noEvent: function(event) {
          event.preventDefault();
       }
    })

vue.jsのすごい所

  • vue.jsでは、処理を止めるpreventDefaulを簡易的に設定できる。
  • html側のv-on:mousemove以降の関数指定で、preventを命令するだけ!
index.html
<div id = "app">
    <a v-on:click.prevent href="https://google.com">google.com</a>
</div>
index.js
new Vue ({
      el: '#app',
    })

つなげる事も可能
v-on:mousemove.stop.prevent

キー修飾子

キーボードに対する修飾子
1. エンターキーを入力時点で動作させる。.enter
2. スペースキー入力時点で動作させる。.space

index.html
<div id = "app">
    <input type="text" v-on:keyup.enter="myAlert">
</div>
index.js
new Vue ({
      el: '#app',
      methods: {
        myAlert() {
          alert('アラート');
        }
      }
    })

つなげる事も可能
v-on:keyup.enter.space

v-onディレクティブ省略記法

  • カウントアップ関数v-onを省略する。
index.html
    <p>現在{{number}}回クリックされています</p>
    <button @click="countUp">countUp</button>

省略が可能な記法は、プロジェクト内で統一すること。

v-model

  • 双方向データバインディングに用いる。
index.html
<div id = "app">
    <input type="text" v-model="message">
    <h1>{{message}}</h1>
  </div>
index.js
new Vue ({
      el: '#app',
      data: {
        message: 'こんにちは',
      }
    })

双方向データバインディング
- inputにテキストを入力すると、変数messageの内容が置き換わる。
- data内のmessageはもちろん、h1タグのmessageも変更される。

動的なデータを扱う

  • vue.jsのdataプロパティでは、動的なデータは扱えない。(プレーンなテキストのみ)
  • computedプロパティを使用する
index.html
<div id = "app">
    <p>{{ counter }}</p>
    <button v-on:click="countUp">+1</button>
    <p>{{ lessThanThree }}</p>
</div>
index.js
new Vue ({
      el: '#app',
      data: {
        counter: 0
      },
      methods: {
        countUp: function() {
          this.counter += 1
        }
      },
      computed: {
        lessThanThree: function() {
          return this.counter > 3 ? '3より上' : '3以下'
        }
      }
    })

computedプロパティの関数lessThanThreeには
- 引数が必要ではない
- returnで値を返す必要がある

dataプロパティでは
- データの初期化をする

関数lessThanThreeは、methodsプロパティでは実現できないのか?

結論:できるけど、最適化するならComputedメソッド一択!

  • computedプロパティは依存関係に基づきキャッシュされる特徴がある。
  • methodsプロパティの関数は、{{二重カッコ内のメソッド}}は再表示される度に実行される。
  • 他の第3のメソッドによる更新にもmethodプロパティのメソッドは実行されてしまう。
  • そのため、computedプロパティを使用すると最適化できる。
index.html
  <div id = "app">
    <p>{{ counter }}</p>
    <button v-on:click="countUp">+1</button>
    <p>{{ lessThanThreeMethod() }}</p>
    <p>{{ lessThanThreeComputed }}</p>
  </div>
index.js
    new Vue ({
      el: '#app',
      data: {
        counter: 0
      },
      methods: {
        countUp: function() {
          this.counter += 1
        },
        lessThanThreeMethod: function() {
          return this.counter > 3 ? '3より上' : '3以下'
        }
      },
      computed: {
        lessThanThreeComputed: function() {
          return this.counter > 3 ? '3より上' : '3以下'
        }
      }
    })

watchプロパティによる、非同期処理

  • データが変わったときに非同期をする
  • 非同期処理の中では、thisが使用不可
  • computedプロパティでは、returnが必要(同期処理)になるので、非同期処理したい場合はwatchプロパティ!

3秒でカウントの値(counter)が0にリセットされる非同期処理
text:index.html
<div id = "app">
<p>{{ counter }}</p>
<button v-on:click="countUp">+1</button>
<p>{{ lessThanThreeComputed }}</p>
</div>

index.js
    new Vue ({
      el: '#app',
      data: {
        counter: 0
      },
      methods: {
        countUp: function() {
          this.counter += 1
        }
      },
      computed: {
        lessThanThreeComputed: function() {
          return this.counter > 3 ? '3より上' : '3以下'
        }
      },
      watch: {
        counter: function() {
          var vm = this;
          setTimeout(function(){
            vm.counter = 0
          }, 3000)
        }
      }
    })

()の使い方

  • computedプロパティでは、メソッド名のみを{{二重カッコ内}}に記載
    computedプロパティに()を足すと、エラーとなり表示されなくなる。

  • methodプロパティでは、メソッド名+()を{{二重カッコ内}}に記載
    methodプロパティから()を除くと、function () { [native code] }と表示される。

  • v-ディレクティブの引数としてメソッド名を指定する場合
     ()は付与してもしなくてもどちらでもOK。
    引数には、javascriptコードを書いてもOK。

classをデータにバインディングする方法(オブジェクトver)

  • {オブジェクト内}に、bg-colorなど、-(ハイフン)がある場合それを'bg-color'をクォーテーションで括る必要あり。
index.html
  <div id = "app">
    <h1 :class="{red: true, 'bg-blue': false}">はろう</h1>
  </div>
index.js
    new Vue ({
      el: '#app'
})
sample.css
.red {
  color: red;
}

.bg-blue {
  background-color: blue;
}
  • ボタンを設置して、クラスの付与を分岐する。
index.html
  <div id = "app">
    <h1 :class="classObject">はろう</h1>
    <button @click="isActive = !isActive">切り替えボタン</button>
  </div>
index.js
    new Vue ({
      el: '#app',
      data: {
        isActive: true
      },
      computed: {
        classObject: function() {
          return {
            red: this.isActive,
            'bg-blue': !this.isActive
          }
        }
      }
    })
sample.css
.red {
  color: red;
}

.bg-blue {
  background-color: blue;
}

classをデータにバインディングする方法(配列ver)

index.html
  <div id = "app">
    <h1 :class="[color, bg]">はろう</h1>
  </div>
index.js
    new Vue ({
      el: '#app',
      data: {
        color: 'red',
        bg: 'bg-blue',
      }
    })
text.sample.css
.red {
  color: red;
}

.bg-blue {
  background-color: blue;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クリックイベントでMouseかEnterキーかを判定する

動機

button要素はクリックした時にfocusがあたった状態になるので、
デザインの再現の際に、不都合があることがあります。

button:focus {
    outline: none;
}

上記のようにfocus時のスタイルを消してしまえば、いったんは良いのですが、
キーボードでアクセスした時に、そのbuttonがfocusされているのか視覚的に分からないため、アクセシビリティ的には良くありません。

ですので、マウスでクリックした時はfocus時のスタイルを消し、キーボードアクセスでfocusされた時は、スタイルを残せるようにJavaScriptで判定したいと思います。

実装例

const button = document.querySelector('button');

const onClickButton = (event) => {
    if (event.detail !== 0) {
        event.currentTarget.style.outline = 'none';
    }
}

const onFocusButton = (event) => {
    event.currentTarget.style.outline = '';
}

button.addEventListener('click', onClickButton, false);
button.addEventListener('focus', onFocusButton, false);

解説

onClickButtonの箇所

clickイベントの時、引数eventにはMouseEventオブジェクトが入ります。
MouseEventには(自分が確認した限り)、keydown時のevent.keyのように押下されたキーを判定できるプロパティが無かったため、event.detail !== 0で、マウスのクリックなのか、Enterキーなのかを判定しました。

event.detailが何かと言うと、クリック数を読み取り専用で返してくれるプロパティのようです。

UIEvent.detail - Web API | MDN

ですので、今回はクリック数がゼロかどうかで、MouseかEnterかを判定しています。
つまり、

event.detail !== 0は、クリック数が0以外の時なので、
Mouseでクリックされた時の挙動を記述します。

Mouseでクリックされた時はevent.currentTarget.style.outline = 'none';でfocus時のスタイルを消します。もっと詳細にスタイルを指定したい場合は、classList.add('className')でクラス制御でスタイルを変更しても良いかもしれません。

onFocusButtonの箇所

onFocusButtonでは、focusが当たった時に、onClickButtonで消したスタイルを元に戻す記述をしています。

event.currentTarget.style.outline = '';で空文字を代入しているのは、既存のスタイル(ブラウザのデフォルトスタイルや、CSSで指定したスタイル)が再度適用できるようにです。

まとめ

以上、clickイベント時にMouseかEnterキーかを判定して、
focus時のスタイルをコントロールする方法を紹介しました。
より良い方法がありましたら、是非コメントを頂けたら幸いです。

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

JavaScriptでのオブジェクト指向について学んでみた(DOM)についても少し

3つの重要すぎる用語

オブジェクト

データと機能が合わさったもの
データとは? 例で言うと人間、野球
機能とは?  例で言うと食べる、寝る、打つ、走る

プロパティ

オブジェクトの中のデータの部分

メソッド

オブジェクトの中の機能の部分

考え方としてはオブジェクトの中にメソッドとプロパティがあるという感じでいいと思います!

私がファンのイチロー選手とローラさんで例で例えると

オブジェクト  プロパティ   メソッド
イチロー 男性、愛知県出身、野球選手   打つ、走る、勇気を与える    
ローラ   女性、モデル、20代   元気を与える 筋トレを教える

オブジェクト自体をさらにまとめてプロパティの中に入れるという表現ができます!

オブジェクトの作り方

hoge.js
let model = {}

今回はモデルという大きなオブジェクトの中にローラさんがいる感じにします。ここにプロパティを追加すると

hoge.js
let model = { 
    name: 'rola',
}

さらにメゾットも

hoge.js
let human = { 
    name: 'rola',
    teachWorkout: function(){},
}

こんな感じです。
ここにプロパティを追加と変更をしてみます!

hoge.js
model.age = 25
model.name = 'magi-'

この記述を先ほどの記述の下に入れてあげるだけです!
超簡単なやり方としてはこんな感じです。メゾットのfunctionの後に呼び出したい記述を入れてあげるとコンソールで呼び出せるので試してみてください!書き方は以前の記事でも紹介した${}を使うと楽です!

ブラウザで用意された最上位オブジェクトWindowについて

windowオブジェクトとはブラウザの情報を持っているオブジェクトです。ブラウザの情報をもつので、ブラウザオブジェクトと呼ばれます。
JavaScriptで予め定義されているメソッドやオブジェクトは全てwindowオブジェクトのプロパティであると言えます!
例えばこのように書くとブラウザにアラートを表示できます。

hoge.js
window.alert("エンジニアになるぞ!")

ちなみにwindowは省略できます!

hoge.js
alert("エンジニアになるぞ!")

これでも結果は一緒です!
console.logや次に紹介するdocumentもwindowというのがくっついていますがたいてい省略されています!

最も頻繁に使用するdocumentオブジェクトについて

このオブジェクトはブラウザ上で表示された情報(HTML)を操作する事ができるオブジェクトです。
このdocumentオブジェクトは非常に多くのプロパティやメソッドを持っています。したがって、documentオブジェクトはこれからHTMLに対して何か処理をする際は頻繁に使用するオブジェクトとなるので覚えておきましょう!
下記ではHTMLに処理する上での考え方と重要な"DOM"についてアウトプットしています!

DOM(HTMLの要素をJavaScript上で取得し操作)

DOMとはDocument Object Model(ドキュメントオブジェクトモデル)の略です。HTMLを解析し、データを作成する仕組みです。
HTMLやCSSがWebページとして閲覧されるまでの流れ
       ⬇️
1, HTMLで書かれた単なる文字情報を解析してDOMに変換
2, そのままだととても見にくいのでCSSやJavaScriptによる装飾を行う
3, ユーザー(私たち)がみる画面に映す。

HTMLは階層構造になっていて、DOMによって解析されたHTMLは、階層構造のあるデータとなります。これを、DOMツリーやドキュメントツリーと呼びます!
JavaScriptのメソッドを使うと、DOMツリーを操作することができます!
HTMLの要素名や、「id、class」といった属性の情報を元にDOMツリーの一部を取得し、CSSを変更したり、要素を増やしたり、消したりできます。するとそれがブラウザに反映され、描画も変わります!これがJavaScriptの面白いところかなと思っています!

まとめ

オブジェクトについての考え方はRubyに比べてわかりやすいかなと感じました!
これからもっとJavaScriptの奥深くに入り込んでいきます!

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

Vuejsでフラッシュカードを作りましょう(音声でもコントロール可能)

やりたいこと

今回、Vuejsを勉強のため、簡単なフラッシュカードのウェッブアプリを作成します。
フラッシュカードは質問と答えの面が2つあって、その二つ面を交換すために、「フリップ」があります。

image.png

実装

データを準備する

まず、HTMLとVuejsの要素を作って、連携しましょう。

<div id="flashCardApp">
const flashCardApp = new Vue({
    el: '#flashCardApp',
    data: {},
    methods: {}

dataのほうはcardsとshowedCardがあります。cardsは全部アプリのカードです。showedCardは表示されるカードになります。で、アプリ起動する時、表示されるカードは最初のカードになります。

cards = [{カード1の情報}, {カード1の情報}, {カード3の情報},...];
data: {
    cards: cards,
    showedCard: cards[0]
}

カード情報は:

id: カードのID(例: 0, 1, 2)
frontText: 質問の言葉(例: BANK, CHURCH)
backText: 言葉の意味(例:銀行, 教会)
colorClass: カードの色(責任)
flipped: どんな面が表示されるか(例: true, false)

です。

前と次のカードのボータン

表示されるカードは一つだけなので、全部のカードを見えるように、カード間に移動するボータンは必要になります。また、最後の次のカードは最初のカードです。逆に最初のカードの前は最後のカードになります。

JS側

methods: {
    previosCard: function() {
        let showedId = this.showedCard.id - 1;
        if(showedId < 0){
            showedId = cards.length - 1;
        }
        this.showedCard = cards[showedId];
    },

    nextCard: function() {
        let showedId = this.showedCard.id + 1;
        if(showedId >= cards.length){
            showedId = 0;
        }
        this.showedCard = cards[showedId]
    }
}

HTML側

<button v-on:click="previosCard" class="button-right"></button></div>
<button v-on:click="nextCard" class="button-left"></button>

カードのフリップのボータン

ユーザはカードのフロント面とバック面を交換するボータンを押すと、現在表示されるカードのflippedの状態を変更して、Vuejsはその状態の変更に対して自動的にカードのフロントとバック面を交換してもらいます。
js側

methods: {
    flipCard: function() {
        this.showedCard.flipped = !this.showedCard.flipped;
    }
}

html側

<transition name="flip">
    <div v-show="!showedCard.flipped" @click="flipCard" class="card-text card-front-text">
        <div class="pretext">
            単語
        </div>
        <h2>
            {{showedCard.frontText}}
        </h2>

        <button class="button-white button-clear button-large">
            フリップ
            <i class="fas fa-sync-alt"></i>
        </button>
    </div>
</transition>

<transition name="flip">
    <div v-show="showedCard.flipped" @click="flipCard" class="card-text card-back-text">
        <div class="pretext">
            意味
        </div>
        <h2>
            {{showedCard.backText}}
        </h2>

        <button class="button-white button-clear button-large">
            フリップ
            <i class="fas fa-sync-alt"></i>
        </button>
    </div>
</transition>

音声でフラッシュカードをコントロールしましょう。

annyangは、ユーザが音声コマンドでサイトを制御できるようにする小さなjavascriptライブラリです。
annyangの使い方は本当に簡単です。

if (annyang) {
  // コマンドを定義する
  const commands = {
    '': () => { flashCardApp.nextCard(); },
    'フリップ': () => { flashCardApp.flipCard(); },
    '': () => { flashCardApp.previosCard(); },
  };

 //入力の言語を日本語に変更する
  annyang.setLanguage("ja");
  // ↑の定義したコマンドを追加しする
  annyang.addCommands(commands);

  // マイクロからの声が聞こえ始めます
  annyang.start();
}

それで、「次」と「前」と「フリップ」のコマンドを言ってもらうと、フラッシュカードをコントロールできます。ただ、音声認識はクラウドで処理するので、少しだけディレイ感じがあります。

以上です。

結果と参考

デモ: https://codepen.io/tardigrades2/full/oNLZovL
コード: https://codepen.io/tardigrades2/pen/oNLZovL)
参考したコード: https://codepen.io/mgnmrt/pen/pKZVYg

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

【kintone】お絵描きして添付ファイルに保存する(改)

先日、こんな記事をUPしました。
【kintone】お絵描きして添付ファイルに保存する

それのお絵かき改良版です。

drawingboard.jsを使う

こちらのお絵かきライブラリを使ってみました。
drawingboard.js

ソースをとりあえずZIPでダウンロードして解凍します。
image.png

distフォルダー内の

  • drawingboard.min.js
  • drawingboard.min.css

を、kintoneの「JavaScript / CSSでカスタマイズ」にアップしておきましょう。
また、JavaScriptも書き換えます。

JavaScriptを書き換える

↓のようにJavaScriptを書き換えて、「JavaScript / CSSでカスタマイズ」にアップしておきましょう。

(function () {

    'use strict';

    function draw_start(){
       let canvas = new DrawingBoard.Board('canvas'); 
    }

    //canvasの画像を保存
    function saveCanvas(canvas_id)
    {
        let canvas_ary = document.getElementsByClassName("drawing-board-canvas");
        let canvas = canvas_ary[0];

        let title = document.getElementById("title").value;
        // Brobを取得
        canvas.toBlob( function ( blob ) {
            // FormDataにファイルを格納
            let formData = new FormData();
            formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());
            formData.append('file', blob, title+'.png');    

            //ファイルをアップロード
            let xmlHttp = new XMLHttpRequest();
            xmlHttp.open('POST','https://サブドメイン.cybozu.com/k/v1/file.json');
            xmlHttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
            xmlHttp.send(formData);

            //200が帰ってきたらレコード登録
            xmlHttp.onload= function(){
                if(xmlHttp.status===200){
                    let key = JSON.parse(xmlHttp.responseText).fileKey;

                    let body={
                        app:101,
                        record:{
                            "タイトル":{value:title},
                            "添付ファイル":{value:[
                                {fileKey: key}
                            ]}
                        }
                    }

                    //fileKeyを設定
                    kintone.api("/k/v1/record","POST",body).then(function(resp){
                        console.log(resp);
                        location.reload();
                    },(err)=>{
                        console.log(err,record.$id.value);
                        alert(err);
                    });
                }
            }
        });
    }

    kintone.events.on(['app.record.index.show'], event => {
        if(event.viewId == 一覧のID){
            draw_start();
            let btn_save = document.getElementById("btn_save");
            btn_save.onclick =()=>{saveCanvas('canvas');};
        }
        return event;
    });
})();

CSSを書こう

こんな感じでキャンバスのサイズを設定するCSSです。サイズはご自由に!

「JavaScript / CSSでカスタマイズ」にアップしておきましょう。

#canvas{
    width: 1000px;
    height: 600px;
}

絵がかける!

クリアボタンは動かなくなるけど、カスタマイズビューから消しておくといいと思います。

image.png

まとめ

ApplePencilでも絵を描くことができるのでぜひためしてみてね^0^
メモするのにいいかもしれないです^^

現在のところ、一度保存した画像の編集には対応していないので、
今後はもうちょっと研究して、編集にも対応させてみたいと思います^0^

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

node-sassがDeprecatedになるとのことでNuxt.jsのsassをアップデートした

node-sass(LibSass)って長らく使ってたイメージありますね。

LibSass is Deprecated

こちらに記事にあるようにLibSassっていうC++で書かれたライブラリを使った非推奨になるとのことで、これを使ってるnode-sassも非推奨になり、Dart製のSassを使ってねという感じになる模様です。

今回Node.jsのv15系にアップデートしたのでnode-sassのバージョンも上げようかと思って調べてたら発見しました。

環境

  • Node.js v15.0.1
  • Nuxt.js v2.12.2

元々v14系で動かしてたけどアップデートさせてみました。

node-sassからsassに移行

削除して(nuxtのバージョンは上げなくても良かったけどついでに)

yarn remove nuxt node-sass sass-loader

インストール

yarn add nuxt sass sass-loader

アプリケーションを起動させてみる

yarn dev

無事に起動しました。

package.jsonはこんな感じ

  • 元々
package.json
  "dependencies": {
    省略
    "nuxt": "^2.12.2",
    "node-sass": "^4.14.0",
    "sass-loader": "^8.0.0"
  },
  • アップデート後
package.json
  "dependencies": {
    省略
    "nuxt": "^2.14.7",
    "sass": "^1.27.0",
    "sass-loader": "^10.0.4"
  },

所感

少し触ってみてるけど特に挙動は変わらない模様です。もともとのsassってすごくビルドが遅くてnode-sassが流行ってきたとかだった気がしたけど(嘘かもしれない)知らぬ間にDartで作り替えられてたんですね。

僕はそのまま移行できた感じですが、そのままスッと移行してプロジェクトに影響でないかは各自の判断でお願いします。

おまけ

これ完全にタイミング悪かった問題だと思うんですけど、node-sassのGitHubリポジトリだとNode.js15系に対応した5.0.0がリリースされていて......

GitHub

npm側ではまだ対応されてないという感じで

npm

これによってnode-sassを入れ直してもバージョン指定して入れてもうまくいかず、
Node.js v15系でnode-sassのインストールエラーがずっと変わらなかったです苦笑

結果的に今回はこれを回避する話ではなかったけど出てたエラーもメモ

・
・
・
[3/4] ?  Linking dependencies...
warning "@nuxtjs/markdownit > raw-loader@4.0.2" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning "nuxt > @nuxt/components@1.1.1" has unmet peer dependency "webpack@^4.0.0".
warning " > sass-loader@10.0.4" has unmet peer dependency "webpack@^4.36.0 || ^5.0.0".
[4/4] ?  Building fresh packages...
error /Users/n0bisuke/Documents/hogehoge/node_modules/node-sass: Command failed.
Exit code: 1
Command: node scripts/build.js
Arguments: 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-transition-group

react-transition-group

react-transition-groupドキュメント

react-transition-groupはReactでCSSアニメーションを扱う為のライブラリです。
見栄えのするモーション自体を提供してくれるわけではなく、CSSを適用するタイミングを提供してくれるので、自分でアニメーションのCSSを書いてアニメーションさせます。

下記の4つのコンポーネントが提供されます。

  • Transition
  • CSSTransition
  • SwitchTransition
  • TransitionGroup

今回はcreate-react-appを使い、CSS(SCSS)はCSSModulesを使っていきます。
※レンダリング回数削減などパフォーマンス面については扱いません。

インストール

terminal
//create-react-appのインストール
npx create-react-app プロジェクト名

//プロジェクト直下に移動
cd プロジェクト名

//node-sassのインストール(scssではなくcssを使う場合は不要です。)
npm i -D node-sass

//react-transition-groupのインストール
npm i -S react-transition-group

//起動
npm start

src/App.jsの不要な部分を削除します。

src/App/js
import React from "react";
import "./App.css";

function App() {
  return (
    <div className="App">

  {/* ここにこれから作るコンポーネントを配置 */}

    </div>
  );
}

export default App;

Transition

シンプルなトランジション

どのタイミングでinと状態が変化しているか視覚化しています。

transition.gif

src\components\singleTransition\SingleTransition.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./singleTransition.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
   exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};

//SingleTransitionコンポーネント
const SingleTransition = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (

    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>

          <Transition in={mount} timeout={1000} >

            {(state) =>
              <div className={Style.circleShape} style={transitionStyle[state]} >
                <div>
                  <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                  <p className={Style.circleText}> {state}</p>
              </div>
             </div>}

          </Transition>

        </div>

      </div>

    </div>

  );

}

export default SingleTransition;
src\components\singleTransition\singleTransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import SingleTransition from "./components/singleTransition/SingleTransition";

function App() {
  return (
    <div className="App">
     <ChainTransition />
    </div>
  );
}

export default App;

classNameでstateを含むクラス名を指定することで、transitionの状態によって独自のクラスを適用する事もできます

必須のProps

in

inの状態 結果
inがtrueになる マウント開始
inがfalseになる アンマウント開始

timeout

entering、exitingのトランジションを使う場合で、addEndListenerを設定しない場合は必須です。

timeoutの指定による状態の変化
状態 初期 timeoutで指定した時間経過後
マウント時 entering entered
アンマウント時 exiting exited

各transitionに個別にタイムアウトを指定することもできます。

sample
timeout={{
  appear: 500,
  enter: 300,
  exit: 500,
}}

addEndListener

entering、exitingのトランジションを使う場合、timeoutを設定しない場合は必須です。
カスタムのtransition終了トリガーを追加して、動的にtimeout時間を設定したい場合に使用します。

sample
<Transition in={mount} {...callBacks} timeout={1000} >
//↓
<Transition in={mount} {...callBacks} addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}>

inがtrueになる度に追加されるので、毎回doneを呼ぶタイミングでイベントの解除が必要です。

timeoutとaddEndListener

timeoutoとaddEndListenerをどちらも指定しないとコンポーネントのトランジションはenteredとexitedだけを繰り返します。
両方指定した場合はaddEndListenerはフォールバックとして使用されます。

4つの状態

inとtimeoutの組み合わせで、コンポーネントに4つの状態が提供されます。
使用しないものについては、省略可能です。

  • entering
  • enterd
  • exiting
  • exited

enteringとexitingのtransitionの時間の長さについては、通常コンポーネントのtimeoutの値とそろえますが、あえて違う値にすることもできます。

inとtimeout以外のProps

otherprops.gif

src\components\otherProps\OtherProps.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./otherProps.module.scss";

//アニメーションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
  exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};


const OtherProps = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (
    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>
          <p>Normal</p>
          <Transition in={mount} timeout={1000} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>mountOnEnter</p>
          <Transition in={mount} timeout={1000} mountOnEnter >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>unmountOnExit</p>
          <Transition in={mount} timeout={1000} unmountOnExit >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
              </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>enter=false</p>
          <Transition in={mount} timeout={1000} enter={false} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>exit=false</p>
          <Transition in={mount} timeout={1000} exit={false} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

        <div className={Style.circleMember}>
          <p>nodeRef</p>
          <Transition in={mount} timeout={1000} nodeRef >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {mount ? "in=true" : "in=false"}</p>
                <p className={Style.circleText}> {state}</p>
              </div>
            </div>}
          </Transition>
        </div>

      </div>

    </div>

  );

}

export default OtherProps;
src\components\otherProps\otherProps.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import OtherProps from "./components/otherProps/OtherProps";

function App() {
  return (
    <div className="App">

     <ChainTransition />
    </div>
  );
}

export default App;

in、timeout以外のProps

Props 未指定時(暗黙的に指定されている) 未指定時から変更する場合 変更時内容
enter enter、enter={true} enter={false} enteringにならない
exit exit、exit={true} exit={false} exitingにならない
mountOnEnter mountOnEnter={false} mountOnEnter、mountOnEnter={true} 遅延マウント(初回のみ)
unmountOnExit unmountOnExit={false} unmountOnExit、unmountOnExit={true} exitedでアンマウント
nodRef nodeRef={false} nodeRef、nodeRef={true} entering、exitingにならない
appear appear={false} appear={true} appearの動作(in=trueと一緒に指定)

appear

各コンポーネントのinの初期値をtrue、appear=trueにします。

変更箇所のみ

src\components\otherProps\OtherProps.js
//mountの初期値をtrueにして、in=trueにする
const [mount, setMount] = useState(true);

//各コンポーネントにappear={true}を追加
<Transition in={mount} timeout={1000} {...callBacksNormal} appear={true} >

コールバック

状態の変化時に処理を行う事ができます。

sample
//状態変化時のコールバック
const callBacks = {
  onEnter: () => console.log("enterです"),
  onEntered: () => console.("enteredです"),
  onExit: () => console.log("exitです"),
  onExited: () => console.log("exitedです"),
};


//Transitionに{...callBacks}を追加
<Transition in={mount} {...callBacks} timeout={1000} >

Transitionを連鎖させる

chain.gif

src\components\chainTransition\ChainTransition.js
import React, { useState } from "react";
import { Transition } from "react-transition-group";
import Style from "./chaintransition.module.scss";

//アニメーションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"red"
  },
  entered: {
    transition: "all 1s ease",
    transform: "translateY(220px) ",
    backgroundColor:"green"
  },
  exiting: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "blue",
  },
  exited: {
    transition: "all 1s ease",
    transform: "translateY(0)",
    backgroundColor: "gray",
  },
};

//ChainTransitionコンポーネント
const ChainTransition = () => {

  //マウントの状態を管理
  const [firstCircle, setFirstCircle] = useState(false);
  const [secondCircle, setSecondCircle] = useState(false);
  const [thirdCircle, setThirdCircle] = useState(false);
  const [fourthCircle, setFourthCircle] = useState(false);
  const [fifthCircle, setFifthCircle] = useState(true);
  const [sixthCircle, setSixthCircle] = useState(true);
  const [seventhCircle, setSeventhCircle] = useState(true);

  //マウントのオンオフを切り替える
  const changer = () => {
    setFirstCircle(!firstCircle);
  };

  const callBacks = {

    onEnter: () => {
      setSecondCircle(true);
    },
    onEntering: () =>{
      setThirdCircle(true);
    },
    onEntered: () => {
      setFourthCircle(true);
    },
    onExit: () => {
      setFifthCircle(false);
    },
    onExiting: () => {
      setSixthCircle(false);
    },
    onExited: () => {
      setSeventhCircle(false);
    },

  };

  return (
    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup}>

        <div className={Style.circleMember}>
        <p>(trigger)</p>
        <Transition in={firstCircle} timeout={1000} {...callBacks}>
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {firstCircle ? "in=true" : "in=false"}</p>
              <p className={Style.circleText}> {state}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEnter</p>
        <Transition in={secondCircle} timeout={1000}  >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {secondCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEntering</p>
        <Transition in={thirdCircle} timeout={1000}>
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {thirdCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onEntered</p>
        <Transition in={fourthCircle} timeout={1000} >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {fourthCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onExit</p>
        <Transition in={fifthCircle} timeout={1000}  >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {fifthCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
        </Transition>
      </div>

      <div className={Style.circleMember}>
        <p>onExiting</p>
        <Transition in={fifthCircle} timeout={1000} >
          {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
            <div>
              <p className={Style.circleText}> {sixthCircle ? "in=true" : "in=false"}</p>
            </div>
          </div>}
        </Transition>
      </div>


      <div className={Style.circleMember}>
        <p>onExited</p>
          <Transition in={seventhCircle} timeout={1000} >
            {(state) => <div className={Style.circleShape} style={transitionStyle[state]} >
              <div>
                <p className={Style.circleText}> {seventhCircle ? "in=true" : "in=false"}</p>
              </div>
            </div>}
          </Transition>
      </div>

    </div>

  </div>

  );
}

export default ChainTransition;
src\components\chainTransition\chaintransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  color: red;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";
import ChainTransition from "./components/chainTransition/ChainTransition";

function App() {
  return (
    <div className="App">

     <ChainTransition />
    </div>
  );
}

export default App;

コールバックのタイミング

nodeRef Propが渡されると、コールバックを使ってもnodeは渡されません。

コールバックの種類 適用タイミング
onEnter entering適用前
onEntering entering適用時
onEntered entered適用時
onExit exiting適用前
onExiting exiting適用時
onExited exited適用時

CSSTransition

Transitionとの大きな違いは、親要素の名前がになっていることと、CSSTransition用のPropsとしてclassNameが使える事です。

csstransition.gif

src\components\singleCSSTransition\SingleCSSTransition.js
import React, { useState } from "react";
import { CSSTransition } from "react-transition-group";
import Style from "./singleCSSTransition.module.scss";


//SingleCSSTransitionコンポーネント
const SingleCSSTransition = () => {

  //マウントの状態を管理
  const [mount, setMount] = useState(false);

  //マウントのオンオフを切り替える
  const changer = () => {
    setMount(!mount);
  };

  return (

    <div className={Style.wrapper}>

      <button onClick={changer}>inの切り替え</button>

      <div className={Style.circleGroup} >

        <div className={Style.circleMember} >

          <CSSTransition
            in={mount}
            timeout={1000}
            classNames={{
              appear:Style.testAppear,
              appearActive:Style.testAppearActive,
              appearDone:Style.testAppearDone,
              enter:Style.testEnter,
              enterActive: Style.testEnterActive,
              enterDone:Style.testEnterDone,
              exit:Style.testExit,
              exitActive: Style.testExitActive,
              exitDone: Style.testExitDone,
            }}
            >

            {(state) =>
              <div className={Style.circleShape} >
                <div>
                  <p className={Style.circleText} > {mount ? "in=true" : "in=false"}</p>
                  <p className={Style.circleText} > {state}</p>

              </div>
             </div>}

          </CSSTransition>

        </div>

      </div>

    </div>

  );

}

export default SingleCSSTransition;
src\components\singleCSSTransition\singleCSSTransition.module.scss
.circleGroup {
  display: flex;
  justify-content: space-between;
  margin: 100px auto;
  width: 1000px;
}

.circleMember {
  text-align: center;
  width: 100px;
}

.circleShape {
  align-items: center;
  background-color: red;
  border-radius: 50px;
  display: flex;
  height: 100px;
  justify-content: center;
  width: 100px;
}

.circleText {
  color: white;
}

button {
  margin: 0 auto;
  display: block;
}

//apperaの最初のフレーム瞬間の状態
.testAppear {
  transition: all 1s ease;
  border-radius: 10px;
  transform: rotateZ(30deg) scale(2);
  opacity: 0;
}

//.testAppearの直後の状態
.testAppearActive {
}

//appearの最終フレームの状態
.testAppearDone {
  transition: all 1s ease;
  border-radius: 50px;
  opacity: 1;
}

//enter中の最初のフレーム瞬間の状態
.testEnter {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: red;
}
//enter中の最終フレームの状態
.testEnterActive {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: red;
}

//enter完了の状態
.testEnterDone {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: green;
}

//exitの初期状態
.testExit {
  transition: all 1s ease;
  transform: translateY(220px);
  background-color: green;
}

//exit中の最終フレームの状態
.testExitActive {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: blue;
}
//exit完了
.testExitDone {
  transition: all 1s ease;
  transform: translateY(0);
  background-color: gray;
}
src\App.js
import React from "react";

import SingleCSSTransition from "./components/singleCSSTransition/SingleCSSTransition";

function App() {
  return (
    <div className="App">

     <SingleCSSTransition />
    </div>
  );
}

export default App;

className

CSSTransitionのclassName Propsでは、各状態にクラス名を付けられます。

sample
<CSSTransition
  classNames={{
    enter:Style.testEnter,
    enterActive: Style.testEnterActive,
    enterDone:Style.testEnterDone,
    exit:Style.testExit,
    exitActive: Style.testExitActive,
    exitDone: Style.testExitDone,
}}>  

今回はCSSModulesを採用しているので、使用する各状態用用のクラスに個別に名前をつける必要がありますが、他の方法でCSSを指定している場合接頭辞を指定すれば、自動的にクラス名が生成されます。
※上記のように任意のクラス名に変更することも可能です。

table:接頭辞をtestにした場合

- active done
test-appear test-appear-active test-appear-done
test-enter test-enter-active test-enter-done
test-enter test-exit-active test-exit-done

※appearクラスの追加タイミングを追ってみると、enterクラスと同時に適用されてしまうので使い方には注意が必要です。

appear

コンポーネントのinの初期値をtrue、appear=trueにします。

変更箇所のみ記載

src\components\singleCSSTransition\SingleCSSTransition.js
//mountの初期値をtrueにしてin=trueにする
const [mount, setMount] = useState(true);

//CSSTransitonにappear={true}を追加
<CSSTransition
  in={mount}
  {...callBacks}
  timeout={1000}
  classNames={{
    appear:Style.testAppear,
    appearActive:Style.testAppearActive,
    appearDone:Style.testAppearDone,
    enter:Style.testEnter,
    enterActive: Style.testEnterActive,
    enterDone:Style.testEnterDone,
    exit:Style.testExit,
    exitActive: Style.testExitActive,
    exitDone: Style.testExitDone,
  }}
  appear={true}
>

SwitchTransition

SwitchTransitionでTransition、CSSTransitionを囲みます。
SwitchTransition独自のPropsとしてmodeがあります。

switch.gif

src\components\transitionSwitching\TransitionSwiching.js
import React, { useState } from "react";
import { SwitchTransition, Transition } from "react-transition-group";
import Style from "./transitionSwitching.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {
  entering: {
    transition: "all 0.5s ease",
    transform: "translateX(-150px) ",
    backgroundColor:"red",
    opacity:"0",
  },
  entered: {
    transition: "all 0.5s ease",
    transform: "translateX(150px)",
    backgroundColor:"green",
    opacity:"1",
  },
  exiting: {
    transition: "all 0.5s ease",
    transform: "translateX(-150px)",
    backgroundColor: "blue",
    opacity:"1",
  },
  exited: {
   transition: "all 0.5s ease",
    transform: "translateX(150px)",
    backgroundColor: "gray",
    opacity:"0",
  },
};

const TransitionSwitch=()=> {
  const [name, setName] = useState(false);

  return (
    <div>
      <button onClick={() => setName(!name)}>Switching</button>
      <div className={Style.squareWrapper}>
      <SwitchTransition mode="in-out">

        <Transition
          key={name ? "aaa" : "bbb"}
          timeout={500}
          unmountOnExit
          mountOnEnter
          >

          {state => <div state={state} style={transitionStyle[state]} className={Style.square}>
  {name ? <p>AAA</p> : <p>BBB</p>}
          </div>}

        </Transition>

      </SwitchTransition>
      </div>

    </div>

  );
}

export default TransitionSwitch;
src\components\transitionSwitching\transitionSwitching.module.scss
/* 簡易リセット */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* スクロールバー常時表示 */
html {
  overflow-y: scroll;
}
.squareWrapper {
  margin-top: 100px;
  position: relative;
}
.square {
  background-color: blue;
  display: block;
  width: 200px;
  padding: 10px 20px;
  margin: 0 auto;
  color: #fff;
  text-align: center;
  position: absolute;
  left: calc(50% - 100px);
}

button {
  margin: 0 auto;
  display: block;
}
src\App.js
import React from "react";

import TransitionSwitching from "./components/transitionSwitching/TransitionSwiching";

function App() {
  return (
    <div className="App">
     <TransitionSwitching />
    </div>
  );
}

export default App;

mode

mode 内容
out-in 先に現在の要素がアウトし、完了後に新しい要素がインする
in-out 先に新しい要素がインし、完了後に現在の要素がアウトする

TransitionGroupとSwitchTransitionの使い分け

古い子要素のoutと新しい子要素のinを同時に行う場合は、TransitionGroupを使用します。

TransitionGroup

TransitionGroup は、Transition または CSSTransition のリストを管理する為のコンポーネントです。

TransitionGroupでTransitionやCSSTransitionをラップします。
各TransitionやCSSTransitionにはinは不要で、代わりにユニークなKeyを設定します。

group.gif

上記サンプルを操作する場合はこちら

src\components\transitionList\TransitionList.js
import React, { useState,useRef } from "react";
import {TransitionGroup,Transition} from 'react-transition-group';
import Style from "./transitionList.module.scss";

//トランジションのスタイル4種類を定義(使わないものは省略可能)
const transitionStyle = {

  entering: {
    transition: "all 0.2s ease",
    opacity:"0"
  },
  entered: {
    transition: "all 0.2s ease",
    opacity:"1"
  },
  exiting: {
    transition: "all 0.2s ease",
    opacity:"0"
  },
  exited: {
    transition: "all 0.2s ease",
    opacity:"0"
  },

};

//TransitionGroupコンポーネント
const TransitionList = () => {

  //inputに入力中の文字
  const [inputting,setInputting]=useState("");

  //input関連付け用のref
  const inputRef =useRef();

  //最後のID番号管理用
  const [lastId,setLastId]=useState(3);

  //初期アイテムリスト
  const initialList=[
    {id:0,word:"React"},
    {id:1,word:"Hooks"},
    {id:2,word:"Transition"},
  ];

  //アイテムリストに初期アイテムをセット
  const [items,setItems]=useState(initialList);

  //アイテムの追加処理
  const adder = () => {
    if(inputting){
      setItems(
      items=>[
        ...items,
       {id:lastId,
        word:inputting}
      ]
    )};

    //IDのインクリメント
    setLastId(prevId=>prevId+1);

    //inputのクリア
    setInputting("");
    inputRef.current.value="";
  };

  //リセット
  const reseter=()=>{
    setInputting("");
    inputRef.current.value="";
    setItems(initialList);
  }

  //form送信防止
  const stopSubmit=(e)=>{
    e.preventDefault();
  }

  return (

    <div className={Style.wrapper}>

      <form action=""
        onSubmit={stopSubmit}
        className={Style.controller}
      >

        <div className={Style.controllerAddGroup}>
          <input
            className={Style.controllerInput}
            ref={inputRef}
            type="text"
            onChange={
              (e)=>setInputting(e.target.value)
            }
          />

          <button
            className={Style.button}
            onClick={adder}
            disabled={!inputting ? true:false}
          >追加</button>
        </div>

        <button
          className={Style.button}
          onClick={reseter}>
          初期化
        </button>

      </form>

      <div className={Style.cardGroup}>

        <TransitionGroup className={Style.cardInner}>

          {items.map(({id,word})=>(

            <Transition key={id} timeout={200} >
              {(state) =>
                <div className={Style.cardShape} style={transitionStyle[state]} >
                  <button
                    className={Style.button}
                     onClick={() =>
                      setItems(items =>
                        items.filter(item => item.id !== id)
                      )
                    }
                  >
                  削除
                  </button>

                  <p className={Style.cardWord}> {word}</p>
                  <p className={Style.cardTransition}> {state}</p>
             </div>}

          </Transition>
              )
            )
          }
        </TransitionGroup>

      </div>

    </div>

  );

}

export default TransitionList;

src\components\transitionList\transitionList.module.scss
/* 簡易リセット */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* スクロールバー常時表示 */
html {
  overflow-y: scroll;
}

@media screen and (min-width: 651px) {
  .wrapper {
    margin: 0 auto;
    max-width: 600px;
  }

  .controller {
    background-color: #ffebee;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    margin-top: 40px;
    padding: 20px;
  }

  .controllerAddGroup {
    display: flex;
  }

  .controllerInput {
    border: none;
    padding: 10px;
    background-color: #ffcdd2;
    transition: all 0.5s ease;

    &:focus {
      background-color: #ffffff;
      outline: none;
    }
  }

  .button {
    appearance: none;
    background-color: #ec407a;
    border: none;
    color: #ffffff;
    cursor: pointer;
    font-family: inherit;
    font-size: 1rem;
    margin: 0;
    outline: none;
    padding: 10px 20px;
    transition: all 0.5s ease;
    width: 100px;

    &:hover {
      background-color: #ad1457;
    }

    &:disabled {
      opacity: 0;
      cursor: default;
    }
  }

  .cardGroup {
    display: flex;
    justify-content: space-between;
    margin-top: 40px;
    max-width: 1000px;
  }

  .cardInner {
    width: 100%;
  }

  .cardShape {
    background-color: #e3f2fd;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    padding: 20px;
    max-width: 600px;

    & + .cardShape {
      margin-top: 20px;
    }
  }

  .cardWord {
    color: #212121;
    display: inline;
    font-size: 2rem;
    font-weight: bold;
    line-height: 1.2;
    overflow: hidden;
    padding: 0 20px;
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    width: 400px;
  }

  .cardTransition {
    background-color: #1e88e5;
    color: #ffffff;
    border-radius: 4px;
    padding: 10px;
  }
}

@media screen and (max-width: 650px) {
  .wrapper {
    margin: 0 auto;
    max-width: 600px;
    padding: 0 10px;
  }

  .controller {
    background-color: #ffebee;
    border-radius: 4px;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    margin-top: 20px;
    padding: 15px;

    & > .button {
      margin-left: auto;
      margin-right: auto;
      display: block;
      margin-top: 10px;
    }
  }

  .controllerAddGroup {
    display: flex;
    width: 100%;
    justify-content: space-between;
  }

  .controllerInput {
    border: none;
    padding: 10px;
    background-color: #ffcdd2;
    transition: all 0.5s ease;
    width: 100%;

    &:focus {
      background-color: #ffffff;
      outline: none;
    }
  }

  .button {
    appearance: none;
    background-color: #ec407a;
    border: none;
    color: #ffffff;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.5rem;
    margin: 0;
    outline: none;
    padding: 10px;
    transition: all 0.5s ease;
    width: 100px;

    &:hover {
      background-color: #ad1457;
    }

    &:disabled {
      opacity: 0;
      cursor: default;
    }
  }

  .cardGroup {
    display: flex;
    justify-content: space-between;
    margin-top: 20px;
    max-width: 1000px;
  }

  .cardInner {
    width: 100%;
  }

  .cardShape {
    background-color: #e3f2fd;
    border-radius: 4px;
    display: flex;
    filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6));
    justify-content: space-between;
    padding: 15px;
    max-width: 600px;

    & + .cardShape {
      margin-top: 10px;
    }
  }

  .cardWord {
    color: #212121;
    display: inline;
    font-size: 1rem;
    font-weight: bold;
    line-height: 1.2;
    overflow: hidden;
    padding: 0 20px;
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    width: 400px;
  }

  .cardTransition {
    background-color: #1e88e5;
    color: #ffffff;
    border-radius: 4px;
    padding: 5px;
  }
}

src\App.js
import React from "react";

import TransitionList from "./components/transitionList/TransitionList";

function App() {
  return (
    <div className="App">
     <TransitionList />
    </div>
  );
}

export default App;

リポジトリ

https://github.com/takeshisakuma/rtg

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

javascript 正規表現で置換 str replace

javascriptで正規表現をやりたい
text.replace("/<p>/","");
マッチング部分を " で囲むとバグるので注意

var text = 'youryour';


//文字列置換 (各最短マッチ)
var tmp = text.replace(/<p>|<h3>|<blockquote.*?<\/blockquote>|<img.*?>|<iframe.*?<\/iframe>|<code.*?<\/code>|<pre.*?<\/pre>/gsim, '');

//</p> もしくは </h3> を改行に変換
var tmp = tmp.replace(/<\/p>|<\/h3>/gsim,"\n");

修飾子の意味

m : 複数行検索
s : を改行文字と一致するようにします。
i : 大文字、小文字の区別をしない
g : 最初にマッチした時点でマッチする文字列を探すのを終了せず全てのマッチする文字列を探します。複数マッチングするかを調べることができます。

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

REACTチュートリアルメモ

REACTのチュートリアルについてのメモです

1. 開発環境を作る

公式チュートリアルに沿って進めていきます
https://ja.reactjs.org/tutorial/tutorial.html

ブラウザ上で開発するかローカルで開発するか

ブラウザ上で開発出来るスターターコードを使えば開発環境の整備は不要ですが、今回僕はローカルでやりました。

ローカル環境を作る

以下の手順でローカルのプロジェクトを作成してサーバーを起動します

1. Nodejsをインストールする

公式サイトからダウンロードしてインストールします
https://nodejs.org/ja/

2. Create React Appでプロジェクトフォルダを作る

適当なディレクトリで以下を実行します

terminal
npx create-react-app my-app
3. サーバーを起動する

上記を実行するとmy-appというディレクトリが作成されて、デフォルトで必要なファイルが生成されていますので、my-appに移動してnpm startすればサーバーが待機します。

terminal
cd my-app
npm start

http://localhost:3000/ を開くと以下のように表示される筈です

チュートリアルでは作成されたファイルを使用しないので/src/の中のファイルを全部消して作り直します

LinuxまたはMacの場合
cd my-app
cd src
rm -f *
cd ..
Windowsの場合
cd my-app
cd src
del *
cd ..

2. Gameクラスを作成して表示する

チュートリアルでは以下のような3目並べのゲームボードを作成します
https://codepen.io/gaearon/pen/gWWZgR?editors=0010
image.png
これを実現するため、1マスに該当するSquareクラスを9個並べてBoardクラスにして、これをGameクラスのプロパティとしてもたせた上で、ReactDOM.render()に表示してもらいます。

/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

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

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

  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>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);
/src/index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

これで先程と同様にnpm startして http://localhost:3000/ にアクセスすると、以下のように表示される筈です。
image.png

3. クラスに値を持たせる/値を参照する

以下のようにしてやればSquareクラスに文字を表示できます

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

classに持たせた値を参照して表示する場合には以下のようにします。Square.render()内でthis.props.valueを参照しているので、Squareクラスのprops(プロパティ)のvalueを参照していることになります。

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

BoardクラスからSquareクラスを作る際に引数としてvalueプロパティを作るようにします。

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

Boardクラスの引数通りに0~8の番号を表示するようになりました
image.png

4. インタラクティブなコンポーネントを作る

buttonにonClick={}を追加することでSquareクラスのオブジェクトがクリックされたときにalertを出すようにしてみましょう。以下のように関数を渡してやることでクリックされたときにreactがalert()を出してくれます。

index.js
class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={function() { alert('click'); }}>
        {this.props.value}
      </button>
    );
  }
}

なお、上記のようにfunctionが重なるとthisがどれを指しているのか分かりにくくなるので下のようにarrow関数で書く方が良いそうです。

index.js
class Square extends React.Component {
 render() {
   return (
     <button className="square" onClick={() => alert('click')}>
       {this.props.value}
     </button>
   );
 }
}

5. クリックしたらXを表示するようにする

まず、コンストラクタ(クラスが最初に呼ばれた時に実行される関数)を追加して状態を記憶させるthis.stateを初期化するようにします。最初にsuper(props)としてるのは、ES2015(ES6)のJavaScriptクラスではsuper()クラスを呼ぶまでthisもsuperも使えなくなる仕様になっているからだそうで、コンストラクタを書くときは常に最初にsuper(props)するように推奨されています。

続いて、buttonに表示される値をthis.state.valueに変更し、onClickでthis.state.valueを'X'に変更するようにします。

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

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

クリックするとコマにXが表示されるようになりました
image.png

6. React DevTools

ChromeまたはFirefoxの拡張機能として提供されているReact DevToolsを使うとReactアプリケーションのpropsとstateを確認できるようになります。

インストール手順は以下を参照してください
https://ja.reactjs.org/docs/optimizing-performance.html

拡張機能を有効にすると、ブラウザの開発者向けツールにreactのcomponentsタグとProfilerタグが追加されて、各クラスのプロパティが確認できるようになります。
image.png

7. 子コンポーネントの値を親コンポーネントに監理させる

ここまでのコードではSquareクラスが自分で表示するための値を保持していましたが、親クラスに監理させた方がコードが分かりやすく、より壊れにくく、リファクタリングしやすくなるそうですので、そのように書き換えていきます。

まず、親クラスにコンストラクタ関数を書いてBoard.state.squaresを初期化、この値を使ってSquareオブジェクトを表示してやります。また、先程までSquareクラスに書いてあったonClickで呼び出す関数もBoardクラスにhandleClick()として書いてやります。これにより値の監理をやりやすくなります。

また、handleClick()内でthis.state.squaresの値を一旦slice()してから書き換えているのはコードをイミュータブルにする為です。元の値を直接いじらないイミュータブルな書き方にする事で変更の有無を検出しやすくしたり、変更の履歴を保存したり、render()すべきタイミングを把握しやすくする効果が期待できるそうです。

index.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();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

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

Squareでやっていた値の監理がなくなったのでコンストラクタは消し、onClickでは親クラスから受け取ったSquare.props.onClick()関数(中身はBoard.handleClick())をonClickで実行するように書いてやります。

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

クリックするとBoardクラスにstateの値が変わり、それに合わせてXが表示されていくようになりました。
image.png

これによりSquaredはクリックされたことを親クラスに伝えるだけのコンポーネントになって、ロジックが書きやすくなりました。

8. 関数コンポーネント

上記の変更によりSquareクラスに値を持たなくなったのでクラスでなく関数で書いたほうが簡潔になります。{() => this.props.onClick()}の代わりにprops.onClickと書き換えるので、以下のようにかなり短く書くことができます。

index.js
function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
        {props.value}
    </button>
  );
}

9. 手番の処理

Xの手番の次はOにならないといけないので、次の手番がXOのどちらなのか書いてやります。BoardクラスにxIsNextプロパティを設定して、trueだったら次はXの手番、falseだったら次はOの手番として処理します。

index.js
function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
        {props.value}
    </button>
  );
}

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

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

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

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

image.png

10. 勝者を判定する

勝者を判定する関数を作成します。squaresは初期値がnullなのでif文からするとfalseと扱われるのを利用してnullではない値が何れかのlineすべてに入っていたら勝者は入っていた値であるという関数ですね。

index.js
function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

Board.render()内でcalculateWinner()を使って勝者判定を行い、勝者を表示するようにします。

render() {
  const winner = calculateWinner(this.state.squares);
  let status;
  if (winner){
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
  }

また、勝者が決まった後に手番が進められるとおかしいので、Board.handleClick()内でcalculateWinner()がnullでないときは手番の更新を行わないようにします。

handleClick(i) {
  const squares = this.state.squares.slice();
  if (calculateWinner(squares) || squares[i]) {
    return;
  }
  squares[i] = this.state.xIsNext ? 'X' : 'O';
  this.setState({
    squares: squares,
    xIsNext: !this.state.xIsNext,
  });
}

image.png

11. 履歴の保存

履歴を残しておいて手番を戻ることができるようにします。具体的には、以下のようなフォーマットで履歴を保存しておいて、戻れるようにしてやります。

こんな風に保存したい
history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

上記のhistoryをGame.props.statesとして管理するように書き換えます。

handleClick()でhistoryに最新のsquaresを追加して上書きすることで1つずつ追加される挙動を作っています。

チュートリアルではこれに並行してBoardクラスで管理してた値をまとめてGameクラスで管理するように変更する作業もやってますので、以下のようにごっそり書き換えてしまってますが、自分でやるときはまず値の管理を置き換えて、正常に動いてから機能追加した方が良いと思います。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
        {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i) {
    return (
      <Square 
        value={this.props.squares[i]} 
        onClick={() => this.props.onClick(i)} 
      />
    );
  }

  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>
    );
  }
}

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    }
  }

  handleClick(i) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status;
    if (winner){
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.props.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board 
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

これで、Gameクラス内に履歴が残るようになりました。
image.png

12. 履歴の表示

とりあえず表示

ここでJavaScriptの標準機能である配列のmapメソッドを使います

mapメソッドの動作例
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

Game.render()内でhistoryの値からbutton表示させるhtmlを生成し、olタグ内に貼り付けることで戻るためのbuttonを作ります。

index.js
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      )
    })

    let status;
    if (winner){
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.props.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board 
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }

この時点ではbuttonタグのonClick()で呼ばれるjumpTo()を書いていないのでbuttonを押すとエラー停止になります。
image.png

historyにkeyを追加

この時点で実行するとconsoleに以下のような警告がでます
image.png
リスト項目に固有のkeyが置かれていないので良くないですよ、と警告されています。順番だけで管理してると項目が増えた時に混乱するのでチュートリアルにも「動的なリストを構築する場合は正しい key を割り当てることが強く推奨されます」とありますから、素直に従ってkeyを追加してやります。

具体的には

{hoge(move)}を{hoge(move)}と書き換えます。reactがkey={move}を認識してそこに書かれたhoge(move)にGame.state.moveを渡せば良いんだと理解してくれます。
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      )
    })

警告が出なくなってくれました
image.png

jumpTo()を書く

戻るための関数を書いていきます。まずconstructorに現在表示している手番が何番目かを示すstepNumberを設定し、jumpTo()するとstepNumberとxIsNextをその時点での値に戻し、その後render()する際に表示する盤面を最新のものhistory[history.length - 1]ではなく指定されたものhistory[stepNumber]に変更、handleClick()されたら手戻りしたところまでのhistoryに追加して書き加えていくようにします。

jumpTo()の時点でhistoryを書き換えてしまうと履歴をウロウロすることが出来なくなっちゃいますので新たに手番が指されたときに更新するようにしてるわけですね。

index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    }
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      )
    })

ということで完成したコードがこちらです
https://codepen.io/gaearon/pen/gWWZgR?editors=0010

13. buildする

アプリケーションが完成しましたのでデプロイ用にbuildしましょう。

まず、相対パスで書けるようにpackage.jsonに"homepage": "./"を追記します。

package.json
{
  "name": "my-app",
  "version": "0.1.0",
  "homepage": "./",

あとはプロジェクトフォルダでnpm run buildするだけです

terminal
cd my-app
npm run build

プロジェクトフォルダ内にbuildフォルダが生成されています
image.png
このindex.htmlをブラウザで開くとアプリケーションが表示されます
image.png

14. 次に何をやれば良いか

この先、ドキュメントはreact.jsの主なコンセプトの解説へと続いていきます。JSXやstateのライフサイクル、イベント処理やReactの流儀など重要な項目にフォーカスして説明しているので続いて読んでいくのが良さそうです。
https://ja.reactjs.org/docs/hello-world.html

  1. Hello World
  2. JSX の導入
  3. 要素のレンダー
  4. コンポーネントと props
  5. state とライフサイクル
  6. イベント処理
  7. 条件付きレンダー
  8. リストと key
  9. フォーム
  10. state のリフトアップ
  11. コンポジション vs 継承
  12. React の流儀

感想

チュートリアルをやる前にREACTは学習コストが高くて云々という記事も多くみかけたんですが、classベースで書くやり方も情報を上位クラスで保持する書き方もそこそこ複雑なコードを書く時の作業性に効いてきそうですし、デバッグがやりやすいようにブラウザ拡張ツールでエラーコードが確認できるのもありがたいです。少なくとも、生のJavaScriptよりは大幅にコードが書きやすいのでreact.jsを食わず嫌いしてるウェブ開発ビギナーはチュートリアルだけでもやってみてほしいです。

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

初心者のプログラミング3

勉強の記録

勉強した内容

  • ツイッターなどでよくある診断ツールの作成。
  • 入力した結果を表示させる
  • 表示させたものを新しく入力した場合に削除する

何をベースに勉強してるか

内容の詳細

わかったことについて

  • functionは省略できる、アロー関数なるものがでてきた。
  • ifに似たwhileなる物も登場、違いがまだよく分かってない!
  • HTMLのbodyにjavascriptから挿入する方法。

今回書いたコード

'use strict';
const userNameInput = document.getElementById('user-name');
const assessmentButton = document.getElementById('assessment');
const resultDivided = document.getElementById('result-area');
const twwetDivided = document.getElementById('tweet-area');

/**
 * 指定した要素の子供をすべて削除する
 * @param {HTMLElement} element HTMLの要素
 */
function removeAllChildren(element) {
    while (element.firstChild) { // result-area に、何かタグがある限りループ
        element.removeChild(element.firstChild);
    }
}
/**
 * 指定した要素に診断結果用のタグを指定する。
 * @param {HTMLElement} element HTMLの要素
 */
function createAssessmentResult(element, result){
        // result-areaにh3タグで'診断結果'という文字を表示
        const h3 = document.createElement('h3'); // h3タグを作る
        h3.innerText = '診断結果'; //h3タグに'診断結果'の文字列を設定
        element.appendChild(h3); // result-area に h3 変数を設定

        // result-areaにpタグで診断結果を表示  
        const p = document.createElement('p');
        p.innerText = result;
        element.appendChild(p);

};

assessmentButton.onclick = () => {
    let userName = userNameInput.value;
    if (userName.length === 0) {
        // 名前の入力がなかったので処理を中断
        return;
    }

    removeAllChildren(resultDivided);
    const result = assessment(userName);
    createAssessmentResult(resultDivided, result);
}


const answers = [
    '{userName}のいいところは声です。{userName}の特徴的な声は皆を惹きつけ、心に残ります。',
    '{userName}のいいところはまなざしです。{userName}に見つめられた人は、気になって仕方がないでしょう。',
    '{userName}のいいところは情熱です。{userName}の情熱に周りの人は感化されます。',
    '{userName}のいいところは厳しさです。{userName}の厳しさがものごとをいつも成功に導きます。',
    '{userName}のいいところは知識です。博識な{userName}を多くの人が頼りにしています。',
    '{userName}のいいところはユニークさです。{userName}だけのその特徴が皆を楽しくさせます。',
    '{userName}のいいところは用心深さです。{userName}の洞察に、多くの人が助けられます。',
    '{userName}のいいところは見た目です。内側から溢れ出る{userName}の良さに皆が気を惹かれます。',
    '{userName}のいいところは決断力です。{userName}がする決断にいつも助けられる人がいます。',
    '{userName}のいいところは思いやりです。{userName}に気をかけてもらった多くの人が感謝しています。',
    '{userName}のいいところは感受性です。{userName}が感じたことに皆が共感し、わかりあうことができます。',
    '{userName}のいいところは節度です。強引すぎない{userName}の考えに皆が感謝しています。',
    '{userName}のいいところは好奇心です。新しいことに向かっていく{userName}の心構えが多くの人に魅力的に映ります。',
    '{userName}のいいところは気配りです。{userName}の配慮が多くの人を救っています。',
    '{userName}のいいところはその全てです。ありのままの{userName}自身がいいところなのです。',
    '{userName}のいいところは自制心です。やばいと思ったときにしっかりと衝動を抑えられる{userName}が皆から評価されています。',
];
/**
 *  名前の文字列を渡すと診断結果を返す関数
 *  @param {string} userName ユーザーの名前
 *  @return {string} 診断結果
 */

function assessment(userName) {
    //userName(文字列) を数値に変換
    //全ての文字を足し算する
    var userNameNumber = 0;
    for (let i = 0; i < userName.length; i++) {
        userNameNumber += userName.charCodeAt(i);

    }
    //5桁の数値を回答結果の範囲(0~15)に返還
    var answerNumber = userNameNumber % answers.length;
    //診断結果
    var result = answers[answerNumber];
    return result.replace(/\{userName\}/g, userName);
};
console.assert(
    assessment('太郎') === '太郎のいいところは決断力です。太郎がする決断にいつも助けられる人がいます。',
    '診断結果の文言の特定の部分を名前に置き換える処理が正しくありません。'
);

むずかしかったよ

  • 授業の途中までは理解し進めたつもりだけど、コードをまとめてスマートにしようってなったあたりから???が増えてきた。 課題としてはこの辺かなって思いました。
  • あと未だに自分で考えて作ってみようってなると無理ですね。

次回やる予定のこと

  • 次回はツイート機能の開発みたいです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

querySelectorの使い方

querySelector

JavaScriptには「getElementById()」とか「getElemetnsByClassName()」などHTML要素を取得できるメソッドはありますが、

「querySelector()」を使うとid属性値・class属性値などを意識せずにjQuery感覚でHTML要素をセレクタ指定することができます。

index.html
<p>全ての色が混ざり合うと白くなるものは何か?</p>
<button onclick="result1()">答え</button>
<p id="ans1">ここに答えを表示</p>
index.js
let list1 = document.querySelector('#ans1');
function result1() {
  list1.innerHTML = '<h4>加法混色</h4>'+'<img src="img/rgb.png">';
}

スクリーンショット 2020-10-27 8.18.18.png

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

memo? useCallback? パフォーマンスが気になる JSXer には SolidJS がオススメ

パフォーマンスなんか気にしたくない

Give a man a bug and he'll work for a day.
Give a man a benchmark and he'll work for a lifetime.1
https://twitter.com/awesomekling/status/1318615899910307850

パフォーマンスなんかに気をとられながら実装したくないんですよ。
Reactmemo()useCallback() のような最適化のためだけの API を呼ぶ呼ばないで 1 ミリ秒も悩みたくないんです。そんな API は存在しないでほしい。

でも気になっちゃうんです。というか、まったく最適化せずに React でアプリを構築していくと、カクつきを体感するぐらいには遅くなりますよね。気にせざるを得ません。

つい最近も React のパフォーマンスチューニング記事がバズってましたね。

早すぎる最適化は害悪か

早すぎる最適化は諸悪の根源とまで言われています。とても強烈な言葉です。本当にそうでしょうか? React でもそうでしょうか?

memo() をガチで導入しようと思ったら、 MobX のようなリアクティブなグローバルデータストアを各コンポーネントから直接参照するか、あるいは状態ツリーを不変にするか(そのために Immutable.jsImmer を導入しようか)、みたいな話になるかと思います2実装が進んでから最適化しようと思ったら、状態の持ち方から変えることになるかもしれません。これはさすがにつらい。

パフォーマンス問題は得てして発見が遅れがちです。普段の開発用 PC ではサクサク動くのに、 2 年前に出たローエンドスマホでテストしてみたら全然動かない、みたいな形で姿を現します。手元では再現しません。ローエンドスマホでのデバッグで遅い原因を突き止めて最適化するのはなかなかの試練です。

そして 1 度でもパフォーマンスが課題になるとソースコードのあっちもこっちも気になって気になってムズムズしてきます。最適化はどれだけ時間があっても足りません。(ムズムズしても、極端に遅い箇所だけの最適化にとどめて他の新機能開発に時間を使うのがオトナというもの。最適化に時間を費やしすぎることこそが害悪なのです。...なかなかオトナにはなれないものですけどね...)

最初からなるべくパフォーマンスが問題にならないような仕組みを導入しておくのが無難なのです。
早すぎる最適化はリスク回避です。 少なくともこの文脈では。

可能なら意識しなくても十分にパフォーマンスが出るようなライブラリを選びたいところですよね。

ということで、 React に似た API と JSX で、勝手に爆速になる UI ライブラリ Solid を紹介します。

Solid

Solid is a declarative JavaScript library for creating user interfaces. It does not use a Virtual DOM. Instead it opts to compile its templates down to real DOM nodes and wrap updates in fine grained reactions. This way when your state updates only the code that depends on it runs.

[DeepL 翻訳]
Solid は、ユーザー インターフェイスを作成するための宣言型 JavaScript ライブラリです。仮想 DOM は使用しません。その代わりに、テンプレートを実際の DOM ノードまでコンパイルして、更新をきめ細かいリアクションでラップします。これにより、状態が更新されたときに、それに依存するコードのみが実行されます。

Solid は、
React のように JSX とフックライクな API で宣言的な関数コンポーネントを記述でき、
lit-html のように効率的にテンプレートから DOM を構築・更新し、
Svelte のようにコンパイル時に再レンダリングを最適化し、
SSR もサポートする、軽量で非常に高速な UI ライブラリです。

公式コード例(TSX)

import { createState, onCleanup, Component } from "solid-js";
import { render } from "solid-js/dom";

const App: Component = () => {
  const [state, setState] = createState({ count: 0 }),
    timer = setInterval(() => setState("count", c => c + 1), 1000);
  onCleanup(() => clearInterval(timer));

  return <div>{state.count}</div>;
};

render(() => <App />, document.getElementById("app"));

https://codesandbox.io/s/8no2n9k94l?file=/index.tsx

なんとなく読めるのではないでしょうか。

React と特に大きく異なるのは、関数コンポーネント内の処理が要素初期化時に 1 度だけ呼ばる点です。 React のようにレンダリングのたびに呼ばれるわけではありません。ここは挙動の違いを明確に意識する必要があると思います。

React の memo()useCallback() のような最適化用の API はありません。それでいて React や Vue.js (3.0) はもちろん、 lit-html や Svelte よりも高速に動作します。ベンチマークを見ると vanillajs にかなり近いです。

js-framework-benchmark.png

TSX で書けて型チェックできるし、 API もわりと扱いやすいし、この実行速度。

(私の観測範囲では) 控えめに言って最強です (2020 年 10 月現在)。

注意点

どんなライブラリにもクセはあります。私は React の onChange"input" イベントで発火するのが許せない人です。私は Mr. Complain です3Angular のこともいろいろ書きました。 まだぜんぜん使い込んでいませんが Solid に対しても不満はあります。

state や props をデストラクチャリングしちゃダメ

createState() で作成する状態やコンポーネントの引数 props はフィールドアクセスが変更監視のトリガーになっていて、リアクティブにしたい式の中でアクセスする必要があります。デストラクチャリングすると、プロパティの変更に対してリアクティブに動作しなくなってしまいます。

変更監視系 API をコンポーネントの外で使うと警告

Solid では createSignal() API によってリアクティブでアトミックな状態、 createState() API によってリアクティブな状態ツリーを作ります。これらは React の useState() と異なり、グローバルスコープで普通に作ればコンポーネント間で状態を共有できます。(フックと違ってコンポーネント内で呼ぶ順番も関係ありません。)非常に便利です。

これら状態を監視して変更に反応する、たとえば createMemo() (MobX でいう computed()、 Vue.js でいう computed()、 Recoil でいう selector())などの API もグローバルスコープで期待通りに動作します。が、警告が出ます。

computations created outside a createRoot or render will never be disposed

この警告の意図は理解できます。が、現実のユースケースに照らして考えると、この警告は親切すぎるというか、大きなお世話だと感じます。
Solid の作者によれば、グローバルな状態には Context API がオススメとのことですが、めちゃくちゃ単純で直感的な createSignal() + createMemo() に比べると Context API は複雑で扱いづらいです。
グローバルな状態を作る方法として、変更監視 API の実際の呼び出しをルートコンポーネントの初期化まで遅延させる手は使えそうです。
https://codesandbox.io/s/lmrb9?file=/index.tsx

ためしに作ってみたもの

ライフゲームの例では React + Recoil、 Preact + preact-shared-state-hook、 Vue.js 3.0、 Solid でライフゲームロジックを共有して FPS を比較してみました。 Solid はバンドルサイズが最小になり、かつ、 FPS も他と比べると群を抜いて良い結果になりました(ベンチマークに適した例ではないかもしれませんが)。(参考に手元の PC で React 12 fps、 Preact 12 fps、 Vue.js 8 fps、 Solid 28 fps 程度です。)

おわりに

Solid は React に近い API と Vanilla JS に近い実行速度を持つ優れた UI ライブラリです。パフォーマンスのことばかりに気をとられる日々は終わりを迎えるかもしれません。

とはいえ、まだ歴史の浅いライブラリですのでエコシステムが未成熟です。コンポーネントフレームワーク(Material-UI とか Element UI みたいなの)は見当たらないし、ネット上で見つかる事例も少ないです。日本語の記事ぜんぜんないし。いま使うなら多少の苦労は覚悟しなきゃいけないかもしれません。

私はその苦労の価値があるんじゃないかと踏み、 Solid に期待を込めて、この記事を書いている次第です。

参考


  1. 「魚を与えればその人は一日の糧を得、魚の釣り方を教えればその人は一生の糧を得る」という老子の言葉のオシャレなパロディですね。 

  2. Redux を導入しようか、という話にはまずならないと私は思います。 

  3. Yeah my life is shit 

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

Js/CSSのキャッシュをコントロール

js/cssをブラウザでキャッシュさせない方法

Cahche Bustingって言うらしい。。。

その方法は、以下の通り

対策前のコード↓

sample.html
<!-- CSS -->
<link href="style.css" rel="stylesheet" type="text/css">
<!-- JavaScript -->
<script src="script.js" type="text/javascript" ></script>
<!-- 画像 -->
<img src="sample.jpg" alt="Sample">

対策後コード↓

sample.html
<!-- CSS -->
<link href="style.css?ver=1.0.0" rel="stylesheet" type="text/css">
<!-- JavaScript -->
<script src="script.js?ver=1.0.0" type="text/javascript" ></script>
<!-- 画像 -->
<img src="sample.jpg?ver=1.0.0" alt="Sample">

「?」の後に任意の文字列を付与すると、ブラウザは対策前のURLとは違うものだと認識する。
(これをクエリ文字列という)

クエリ文字列はほかにも
・更新日時 (例:src=”style.css?date=20201027053000″)
・バージョン (例:src=”style.css?ver=1.2.3″)
・ランダム数

jsやcss側での加工はと言うと、必要ない

これでjs/cssを更新しても、ブラウザでキャッシュされなくなる。
しかし、htmlがキャッシュされたら元も子もないので、htmlには以下のmetaを記述する。

sample.html
<head>
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Cache-Control" content="no-cache">
    <meta http-equiv="Expires" content="0">

3つともキャッシュしちゃダメよ!ということらしい。

以上。

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

モダンな技術のReactを勉強しよう

最近現役エンジニアの方とお話しさせていただく機会があったのですが、やっぱり現場はReact一択!!とういう声が大きかったので、どんな技術なのか、どうやって導入するのか調べて実際に簡単なアプリを作成していこうと思うので、これからReactをアウトプットして行こうと思います。

Reactとは

まずは簡単にReactってどういうものなのかざっと調べた情報を記述します

  • ReactはFacebookが作った
  • JSのライプラリ
  • 記述の仕方はJSX(ほぼHTML)
  • {}内はJSの記述できる
  • SPA(Single Page Application)を作ることに最適
    • ちなみにSPAはページ推移せず、一つのWebページでコンテンツを切り替えられるやつのこと いちいちリロードが必要ないやつ
  • TypeScriptと相性が良い

ほんとに簡単に調べた結果を羅列しただけですが、こんな感じ

モダンな技術を学ぶのはわくわくしますね! オラわくわくすっ(ry
まあよく意味わからないところとかあるんですけど、それはアプリ作りながらやっていけばわかる様になっていくのかなと思います
じゃけん、環境構築やっていきましょうか!(埼玉県出身者より)

環境構築

create-react-appという開発環境を簡単に構築できるツールを使っていくのですが、どうやらそれを使うためには
node
npm
が必要らしいです

またそれをインストールするためにはHomebrewnodebrewをインストールしなければいけない様です、、ややこしい、、

なんでまずはこのコマンド

ターミナル
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 

その後、Macのパスワードと
Press RETURN to continue or any other key to abort
と出てくるのでreturn押してあげてください
これでHomebrewのインストール完了

つぎはこのHomebrewを使ってnodebrewをインストールします

ターミナル
brew install nodebrew

これでnodebrewのインストール完了
そんで次にnodebrewを使ってnodeをインストールします

ターミナル
nodebrew install stable

ディレクトリがないとエラー出る場合はこれを先に

ターミナル
mkdir -p ~/.nodebrew/src

インストールが完了したら

ターミナル
nodebrew ls

でバージョンを確認しましょう
多分今の安定バージョンというのはv14.4.0になっていて、それをインストールした状態ですが
current: none
とターミナルに記載があると思います。
これをv14.4.0にするために

ターミナル
nodebrew use v14.4.0

これで再度バージョンを確認すると
current: v14.4.0
になっているはず

そして次はこれ(最新macOSなら)

ターミナル
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile

これでやっとnodeがインストールできました!!
nodeがインストールされたらnpmもインストールされていますので、これでcreate-react-appを使う環境が整ったとういう感じですね!

そしてこれが最後のアプリケーションを作成するときにコマンド

ターミナル
npx create-react-app 任意のアプリケーション名

これでアプリケーションが作成されました!!
Finderから確認できればアプリケーションは作成されています
長かった?ですね

あっ、アプリケーションの作成の際はちゃんと自分が作りたいディレクトリに移動して作成してくださいね!

まとめ

とりあえずcreate-react-appの環境構築までをずらずら記述していったのですが、ネットワークの基礎的な部分が圧倒的に不足していますね。。。
なんでかわからないけどできたという状態から抜け出すためにはどのくらい勉強すればいいのかトホホ、、、

参考

https://www.youtube.com/watch?v=P5lDbRxp9sI&t=140s

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

カレンダーから日付を選択するフォーム作成方法

普通の入力フォーム(変更前)

ビューファイル
<%= form.text_area :day, placeholder: "day" %>

前.png

カレンダー選択フォーム(変更後)

text_area → datetime_fieldに変更する。

ビューファイル
<%= form.datetime_field :day, placeholder: "day" %>

後.png

まとめ

変更前は色々と記述しないといけなくて難しいのかと思うかもしれませんが、
実際は記述を少し変えるだけでできちゃうんです!!
ぜひ、やってみて下さい。

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

GoToトラベルのクーポン種別を絞込む

はじめに

金持ちほど得をする素敵なキャンペーン、GoToトラベルですが、クーポン種別が紙と電子の二種類あることをご存知でしょうか?
紙クーポンは、その名の通り実体のある紙形のクーポン、電子クーポンはスマホから使用できるバーチャルなクーポンです。

普通に考えれば、同じキャンペーンで発行されたクーポンなので同じように使えるはずですが、そこは流石日本というべきか、
電子クーポンは紙クーポンと比較して約半数の店鋪でしか使用できません。

「まあ、最初に調べてから旅行すればいいじゃん」と思いますが、GoToトラベルのサイト上ではクーポン種別による絞り込みが出来ません。

これをUserScriptを使用して改善してみます。

Screen Shot 2020-10-27 at 0.51.07.png

UserScriptって?

UserScriptとは、Webサイト上でユーザー任意のJavascriptを割り込む仕組み、またはそのスクリプトのことです。
たいていのブラウザで拡張機能として動作します。Tampermonkeyあたりが有名でしょうか。
今回はこれを用いることによって、GoToトラベルの公式Webサイト上で絞り込み機能を有効化しています。

スクリプト

// ==UserScript==
// @name         GoToRefine
// @namespace    https://satoh.dev/
// @version      0.1
// @description  Refine your search results by coupon type for GoTo Travel. This is how it should be.
// @author       Soh Satoh
// @match        https://map.goto.jata-net.or.jp/?st=result&*
// @require      https://jpillora.com/xhook/dist/xhook.min.js
// @grant none
// ==/UserScript==

(function () {
    // Add a button to choose between electronic or paper coupons
    // ref: https://www.lisz-works.com/entry/tampermonkey-add-selector
    var selectable_elements = ['デフォルト', '電子クーポン', '紙クーポン'];
    var selectbox = $('<select>')
        .attr({
            name: 'sel',
            id: 'selector',
            style: 'color: white; font-size:' + $('.reset-btn').css('font-size'),
        })

        .on('change', async function () {
            localStorage.setItem('mode', this.selectedIndex);
            location.reload(true);
        })

        .appendTo($('.reset-btn'));

    $.each(selectable_elements, function (i, v) {
        selectbox.append($('<option>').val(v).text(v));
    });

    // Apply the saved setting
    var selector = $('#selector')[0];
    selector.selectedIndex = localStorage.getItem('mode');

    if(selector.selectedIndex == 0) return;

    // Hook XHR
    xhook.after(function (request, response) {
        if(request.url.indexOf('api/point') == -1) return;

        var modified = JSON.parse(response.data);

        var items = modified.items;
        var i = items.length;
        while (i--) {
            if (items[i][selector.value] != '1') {
                items.splice(i, 1);
            }
        }
        response.data = JSON.stringify(modified);
    });
})();

最初にJQueryでページにセレクトボックスを追加します。
デフォルト、電子クーポン、紙クーポンの間で選択可能にし、

localStorage.setItem('mode', this.selectedIndex);

でローカルストレージに設定を保存するようにしています。
本来はGM.setValueを使用するべきなのですが、これを使用するとなぜかXHRにHookできなくなるため断念しました。理由がわかる方はコメントをいただけると幸いです。

次にXHookを使用してresponseを書き換えています。
このスクリプトを使用することで、かんたんにXHR(Ajax)を傍受、改変することが可能です。
本来は外部スクリプトを利用する予定ではなかったのですが、responseの書き換えがどうやってもうまく行かず断念しました。(responseTextの改変は簡単なのですが)

最後に

最初は容易に検索できるよう、Webサイトを作成しようと思ったのですが怒られたくないのでやめました。
「初めから絞込めるようにしろよ」とは思うのですが、まあそれやられるとGoToキャンペーン本来の意味を失う部分もあるのかもしれません。
電子クーポンのほうが不便だなんて意味がわからないですけどね。それならどちらかに統一してくれたほうが便利なんですが。

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

lazyloadの使い方

lazyload

1. GitHubからファイルをダウンロード

lazyloadのダウンロード先

2. ファイルを読み込む

<script src="lazysizes.min.js"></script>

3. クラスをつける

<img data-src="画像パス" class="lazyload">
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuへデプロイする手順

Herokuへデプロイする手順

ターミナル
brew tap heroku/brew && brew install heroku
ターミナル
heroku --version
ターミナル
# Herokuへログインするためのコマンド
% heroku login --interactive
  => Enter your Heroku credentials.
# メールアドレスを入力しエンターキーを押す
  => Email:
# パスワードを入力してエンターキーを押す
  => Password:
Gemfile
# ファイルの一番下の行に追記する
group :production do
  gem 'rails_12factor'
end
ターミナル
# Gemをインストール
% bundle install
ターミナル
% git add .
% git commit -m "gem rails_12factorの追加"
ターミナル
% heroku create アプリケーション名
ターミナル
% git config --list | grep heroku
ターミナル
% heroku addons:add cleardb
Creating cleardb on  アプリケーション名... free
Created cleardb-vertical-00000 as CLEARDB_DATABASE_URL
Use heroku addons:docs cleardb to view documentation
ターミナル
% heroku_cleardb=`heroku config:get CLEARDB_DATABASE_URL`
ターミナル
% heroku config:set DATABASE_URL=mysql2${heroku_cleardb:5}

# 以下コマンドの実行結果
Setting DATABASE_URL and restarting  アプリケーション名... done, v◯◯
DATABASE_URL: mysql2://000000000000:0aa0000@us-cdbr-east-02.cleardb.com/heroku_aaa00000000?reconnect=true
ターミナル
% EDITOR="vi" bin/rails credentials:edit
ターミナル
% heroku config:set RAILS_MASTER_KEY=`cat config/master.key`
ターミナル
% heroku config
ターミナル
% git push heroku master
ターミナル
% heroku run rails db:migrate

公開を確認

ターミナル
% heroku apps:info

===ajax-app-123456
Addons:         cleardb:ignite
Auto Cert Mgmt: false
Dynos:          web: 1
Git URL:        https://git.heroku.com/アプリケーション名.git
Owner:          sample@sample.com
Region:         us
Repo Size:      165 KB
Slug Size:      56 MB
Stack:          heroku-18
Web URL:        https:/アプリケーション名.herokuapp.com/

現場からは以上です!

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