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

三点リーダーを❤に置換するJavaScript

TLに流れてきたので。

replace.js
function rplc(){
  h=[...''][0];
  document.body.innerHTML = document.body.innerHTML.replace(/…/gi, h);
}
rplc();

Unicode絵文字だからすんなり行けるかと思ったけど、「❤」を出力するのは一手間いる感じだった。

おまけ。
ブックマークレット

中身
javascript:{function r(){h=[...''][0];document.body.innerHTML=document.body.innerHTML.replace(/…/gi, h);};r();}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでジャイロセンサー(方向センサー)つかって遊んでみた

ジャイロセンサ使ってみた。色々遊べそう。
(※センサを搭載していないデバイスで閲覧している場合、何も面白くないものが表示されていると思います。)

センサを搭載してても、Qiita記事への埋め込みだとセンサが取れないっぽい。
codepenのサイトから直接閲覧する必要あり。

See the Pen 3D Sensor test by kob58im (@kob58im) on CodePen.

three.jsを使用しているのは手軽に可視化したかったからであって、ジャイロセンサの取得は素のJavaScriptで書けます。(参考サイト参照)

なお、記事作成時点(2020.11.24)で、参考サイトによると

これは実験的な機能です。本番で使用する前にブラウザー実装状況をチェックしてください。

および

警告: Chrome と Firefox では角度の扱い方が異なり、一部の軸の向きが逆になっています。

だそうです。

参考サイト

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

JavaScript勉強日記#1

①確認ダイアログボックスの表示

・目的:下記のダイアログボックスを表示する
https://gyazo.com/3da5a0c50544d85af25c1c7cd4adce3b
https://gyazo.com/f9808943633140799071c34fea626e74

index.html
  <script>
   if(window.confirm('ゲームスタート!準備はいいかい?')) {
    console.log('ゲームを開始します!');
  } else {
    console.log('ゲームを終了します。');
  }  
  </script>

window.confirm('message')で表示できた。
alertメソッドとの違いはT/Fでリターンを返してくる事。

②テキストを持ったダイアログを表示
・目的:下記のダイアログの表示
https://gyazo.com/c24d08dedc4a761b0f2faea00317ae3e
https://gyazo.com/67b10557bbb964c2fbcf729268dc9fb0

window.confirm('message')で表示できた。
これは変数を用いる事で保存が可能になる。
例えばlet answer(変数名) = window.confirm('message')と書けばこれ以降answerと書く事でmessageを取得できる。

③入力内容で動作を変更する
・目的:動作を条件分岐させたい

index.html
const answer = window.prompt('ヘルプを見ますか?');
if(answer === 'yes') {
  window.alert('タップでジャンプ、障害物をよけます。');
}

constで生成された変数は後から上書きできない。

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

初プルリクで心がボロボロにならないためのセルフチェック観点をまとめてみた

この記事はモチベーションクラウドシリーズアドベントカレンダー2020の3日目の記事です。

自己紹介

初めまして!こんにちは!19年新卒の小宮と申します!
エンジニア歴は約1年程度で、バックエンドやQAも関わりつつ、
現在メインはフロントエンドエンジニアとして日々開発業務に勤しんでおります!

この記事を書こうと思った背景

この話は本当にあった怖い話です...
image.png
上のキャプチャは自分が初めて出した大きめのPRです。ご覧いただけますでしょうか?
Conversation 73という恐ろしい文字列が・・・

自分がこの73コメント分(一部CodeCovなどの自動コメントも含まれますが)、
先輩エンジニアの時間を奪っているんだと勝手に責任感を感じて、
いそいそといただいたコメントに対応していたのが1年前だと思うと、時の流れを早く感じます。

上記のような現象、「自分も同じ経験をしたことがあるなあ・・・」という方も
一定数いらっしゃるのではないでしょうか?
この現象が起きる理由として、パッと思いつくだけでも

  • その開発現場のコードのお作法を知らない
  • 動けばいいや!という精神でコーディングしてしまう
  • ここまで考慮できてますよ!というアピールで不必要なコードを書いてしまう

などと、他にもいっぱいあるんじゃないかなあと思っております。

今回、アドベントカレンダーで執筆の機会をいただけたので、一年前の自分のように
先輩の時間を奪って申し訳ねえ!というやるせない気持ちになる駆け出しエンジニアが
少しでも減る一助になれれば良いなあと思い、本記事を執筆しました!

具体的なセルフチェックチェック観点

言語共通

命名

コードというのは「書くこと」より「読む/読まれること」の方が圧倒的に多いのです!
命名に関してチェックするべき観点のほんの一例を紹介します!

naming.js
// 略語について
number // Good! なるべく略語は使わない!
no // More! 「いいえ」のNoと勘違いする可能性も0じゃないですね

// 真偽値について
isParent // isで始めたり、
hasChild // 動詞で始めたり、
alreadyAppeared // 副詞+動詞の形だと真偽値を返してくれることが察しやすいですね!

// メソッドや変数名について(例えばECサイトでカートへ追加するメソッド名)
const add = function() { return ** } // 何をどこに加えるのかが分かりにくい。
const addToCart = function() { return ** } // ToCartを入れるだけでも何やっているか分かりやすくなったですね!

本当はまだまだあると思いますが、いったんこれくらいで!
ちなみに僕のエンジニアの師匠のさらにその師匠が
エンジニアは、名前をつけるのがお仕事です。」と言っていたようです。

あと、命名の際はDeepL(Google翻訳よりも精度高い気がします)を使って、
日本語で何をしたいメソッドなのか言語化してから翻訳かけたりしています!

まとめられる箇所をまとめられていない

コピペを何度も繰り返しているところはメソッドに切り出してまとめましょう!
よくDRY(Don't Repeat Yourself)って言われたりしますね!

dry.js
console.log('hi! taro')
console.log('hi! hanako')
console.log('hi! hiroshi') // こんな風にいちいち呼び出さずに

const nameCall = function(name) { console.log(`hi! ${name}`) } // メソッドでくくっちゃいましょう!
nameCall('taro')
nameCall('hanako')
nameCall('hiroshi') 

DRYを徹底することのメリットとして、

  • コード量が減る
    • 上記の例だと実感しづらいかもしれませんが、これは本当!笑
  • 後から修正が入った時に修正が必要な箇所が少なくなる
    • 上記の例だと「hi!」と挨拶していたのが、「hello!」になった時、nameCallだけ編集すればOK!
  • コードが読みやすくなる
    • 上記の例だと「あ、これは名前を呼んでいるんだな!」とメソッド名から推察しやすくなりますね!

不必要なコードも書いてしまう

よほどのことがない限り、今後これを使うことになるだろうから、今やっちゃおう!と
今必要ないコードを加えることはお節介になる可能性が高いです!
よくYAGNI(You ain't gonna need it)って言われたりしますね!

yagni.js
// 例えば多言語対応することになり、先に英語版からリリースして、来年に中国版をリリースしよう!となったとき、
// 先んじて中国語の定数も定義しようというのはお節介になる可能性が高いです!!
export const LANGUAGE_TYPES = {
  JA: {
    KEY: 'ja',
    TEXT: '日本語'
  },
  EN: {
    KEY: 'en',
    TEXT: '英語'
  },
// ZH: { いらない!!
//   KEY: 'zh',
//   TEXT: '中国語'
// },
}

YAGNIを徹底することのメリットとして、

  • 負債になる可能性のあるコードを残さない
    • 上記の例だと中国語版のリリースが白紙になった場合、完全に負債になりますね!
  • 読み手の「?」を減らす
    • 上記の例だと中国語版のリリースが白紙になってから1年ぐらい経ち、あなたがそのプロジェクトを外れていた場合、「え、これって必要なコードなんですか?」と後から読む人の混乱を招く可能性がありますね!
  • 後から実装する人がバグを出してしまう可能性を減らす
    • 上記の例だと違う人が中国語を担当したときに、「あ、中国語も一緒にやってくれたんだ!」と安心してしまい、対応するべき箇所全てに対応するのを失念してしまう可能性がありますね!

スコープを意識しましょう

スコープとは、あなたが定義した変数や関数が参照される範囲のことです!
無駄に広いスコープで変数を定義すると、読み手に不親切な上に、不具合の温床になったりするので要注意です!

scope.js
const scope = "Wide Scope"
function getNarrowScope() {
  const scope = "Narrow Scope"
  return scope
}
function getWideScope() {
  return scope
}
console.log(getNarrowScope()) // Narrow Scope
console.log(getWideScope()) // Wide Scope

スコープを意識することのメリットとしては、

  • コードを疎結合に保てる
    • 上記の例だと"Wide Scope"の値を持つ方の変数scopeとgetWideScopeが相互に依存関係にありますね!コードは疎結合に保つことが正義です!
  • 読みやすくなる
    • 上記の例だとまだ読めますが、これが数百行のコードになった時、"Wide Scope"の値を持つ方の変数scopeを100行下で呼び出していたりしたら、読み手にかける負担が大きくなりますね!
  • 書きやすくなる
    • 実際にコードを書くときも、意識するべきことが減るので、実装する際においても考えることが減ると思われます!

マジックナンバー

マジックナンバーとは、意味を持つ数字を、直接書いて、読み手が
「なんだこれ?」となってしまうような数字のことです。

magicNumber.js
// 例えば100円ショップでお買い物をする時
const price = 100 * 1.10 // 1.10が何のことだか分からない!

const tax = 1.10
const price = 100 * tax

マジックナンバーを撲滅することのメリットとしては、DRYの説明で書いたのと同じで、

  • コード量が減る
    • 上記の例でも実感しづらいかもしれませんが、これは本当!笑
  • 後から修正が入った時に修正が必要な箇所が少なくなる
    • 上記の例だと消費税が20%になった時(やだなぁ...)、taxだけ編集すればOK!
  • コードが読みやすくなる
    • 上記の例だと「あ、これは消費税のことだな!」と定数名から推察しやすくなりますね!

インデントやtypo

これらは基本的なことでかつ、すぐ見つけられることなので、もう一度見直しましょう!
インデントを見やすくしたり、typoらしきものを色付けしてくれる拡張機能も多くのエディターで備えてくれているので、
それらも是非活用しましょう!(以下例はVSCodeのCode Spell Checker)
image.png

JavaScript

console.logの消し忘れ

開発中に書いていたconsole.logを消し忘れていないかは要チェックですね!
これをやったときはだいぶ死にたくなりました!

変数の定義

こちらはES6で新しくできるようになったことですが、変数の宣言にvar以外が使えるようになりました
参考 : var/let/constの使い分けのメモ
(良書でも古いものだと未だにvarが使われたりしますが、今やvarは使わない!と言っても過言ではないです)
他にもES6は便利なので、極力活用しましょう!(ここで書くとボリューミーなので、今回は省略)

variables.js
var name = "taro" // varは使わない!!!

const name = "taro" // 真っ先に使うべきはconst

// 名前が「田中ジェイソン太郎」というミドルネーム持ちの人がいて、海外ではジェイソンと呼ばれていて、
// どうしても再代入が必要な場合だけletを使いましょう!
let name
if (country === "japan") {
  name = "taro"
} else {
  name = "Jason"
}

変数定義にこだわることで、以下のメリットがあります!

  • ファイル内で変数にスコープを持たせられる
  • 読み手にとって親切
    • 例えばletで変数が定義されていた場合、読み手は「あ、これはどこかで再代入されるのか!」と予測しながら読み進められる

CSS

カラーパレット

文字の色や、背景色を定義するとき、
カラーパレットを使える場合はそちらを活用しましょう!!

background-color: #bf2926; // 直接カラーコードを書かない!

$PASSIONATE_RED: #bf2926; // 共通で使えるカラーパレットがどこかで定義されているかどうか確認して
background-color: $PASSIONATE_RED; // ある場合はそのカラーパレットに割り当てられた定数を使いましょう!

カラーパレットを一貫して使うことのメリットとしては以下が挙げられると思います

  • プロダクトに使われているそれぞれの色に役割を持たせやすくなる
    • レッドはエラー、グリーンは成功、オレンジは注意などと、命名と共に定義すると色に役割を割り当てやすくなりますね!
  • どんな色だかイメージしやすくなる
    • 例えば#bf2926;が何の色だかぱっと見で判るほど記憶力の良い人はいないと思いますが、$PASSIONATE_REDなら「情熱的な赤い色なんだろうな〜」とイメージできやすくなりますね!

8の倍数ルール

marginやheight、font-sizeなどの大きさを8や4の倍数で統一している可能性があります!
メリット等は以下の記事などを読むとわかりやすいので、是非ご一読を!
参考 : 8の倍数ルールでデザインする理由とメリット・デメリット

最後に

夢中で思いつく限りのことを書いてみました。
(本当はvue.js特有のことなども書こうと思ったんですが、力尽きましたw)

コードレビューに出す内容って相手への敬意の現れだなと思っていて、
正直、上記の内容を一回指摘されるぐらいなら全然OKだと思う(新人だし仕方ないよ!ってなると思う)んですが、
一度指摘されたことは次のPRでは絶対指摘されないぞ!という心意気が一番大事かなと思います!!

ではみなさん良い12月を!!
僕は月末までに彼女を作ります!!!

おまけ(参考資料)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

これは自分がエンジニアになったときに職場の先輩から真っ先に読め!と言われた本です。
当時は「はいはい、なるほどね〜」くらいだったんですが、今改めて読み返すと、
「当時の自分全然わかってなかったんやな...」と奥の深さを知れるエンジニアの登竜門的な本

プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則

一番最初にあげたリーダブルコードより新しいもので、今回自分が挙げたYAGNIとかDRYとか、
そのほかにもエンジニアの道標?心得?的なのがたくさん載ってます。
リーダブルコードと違って、具体的なコードが一切載っていなかったり、
日本人が書いたものなので、初心者にも読みやすいのかなと思います。

改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで

そもそものJavaScriptへの入門として、こちらの教材は少し難解な部分もあるのですが、
一通りしっかりやると、かなりの実力がつくのではないかなあと思います。
JavaScriptの古い記法から、新しいES6の記法までをさらっているため、
よりES6のメリットが分かったり、現場によっては割と古い記法で書かれているところもあるので、
そういった方にもお勧めできますね!

やっぱりプログラミング(今回はvue)の学習といえばアプリ作成!ということでカレンダー!

学習のために、最初の方は色々と教材を買ってやってみたのですが、
やっぱり自分でアプリを作るのが一番学習になります。
ということで私はこちらの記事を参考に、カレンダー的なものを作りました!
追加機能として例えばモーダルを自作してみたりしてもいいかもしれませんね!

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

【JavaScript ~カウントダウンタイマー応用~】勉強メモ14

JavaScriptちゃんと学習中。
今回はカウントダウンタイマーの応用です。
残り時間が刻一刻と変化する実装をします。
ほぼ自分の勉強メモです。
過度な期待はしないでください。

前回の記事を参照の元作成してます。
必要が有れば参照下さい。
【JavaScript ~カウントダウンタイマーの作成~】勉強メモ13

完成図

image

実装手順

  • 残り時間を再計算をする関数を作成
  • その関数を1秒ごとに繰り返す為の関数を作成

前回迄のコード

JavaScript
'use strict';

function countdown(set) {
  const now = new Date();

  const rest = set.getTime() - now.getTime();
  const sec = Math.floor(rest / 1000) % 60;
  const min = Math.floor(rest / 1000 / 60) % 60;
  const hours = Math.floor(rest / 1000 / 60 / 60) % 24;
  const days = Math.floor(rest / 1000 / 60 / 60 / 24);
  const count = [days, hours, min, sec];

  return count;
}

let goal = new Date();
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);

const counter = countdown(goal);
const time = `${counter[1]}時間${counter[2]}${counter[3]}`;
document.getElementById('timer').textContent = time;
HTML
<p>後、<span id="timer"></span>で1日が終わります。</p>


再計算する関数作成

  • functionを使って新たな関数作成

下記の3行を関数にして、その関数を呼び出す記載をする

const counter = countdown(goal);
const time = `${counter[1]}時間${counter[2]}分${counter[3]}秒`;
document.getElementById('timer').textContent = time;

 
関数名は(なんでも良いのだが)、recalcという名で作成。
関数の定義は、「function( )」と書き、その後ろの中括弧「{ }」の中にまとめたい処理を
書くことで関数を用意することができ、「関数名( )」とする事で関数を呼び出す。

JavaScript
function recalc() {
  const counter = countdown(goal);
  const time = `${counter[1]}時間${counter[2]}${counter[3]}`;
  document.getElementById('timer').textContent = time;
}

recalc();

 
ここで、プログラムの処理の流れですが、

JavaScript
'use strict';

// ④
function countdown(set) {
  const now = new Date();

  const rest = set.getTime() - now.getTime();
  const sec = Math.floor(rest / 1000) % 60;
  const min = Math.floor(rest / 1000 / 60) % 60;
  const hours = Math.floor(rest / 1000 / 60 / 60) % 24;
  const days = Math.floor(rest / 1000 / 60 / 60 / 24);
  const count = [days, hours, min, sec];

  return count;
}

// ①
let goal = new Date();
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);

// ③
function recalc() {
  const counter = countdown(goal);
  const time = `${counter[1]}時間${counter[2]}${counter[3]}`;
  document.getElementById('timer').textContent = time;
}

// ②
recalc();

先ず、変数goalが設定された後に、関数recalcが実行される。
その後、関数recalcは、関数countdownを呼び出し残り時間を計算するという流れ。

そして、関数recalcの中に、関数recalcを1秒ごとに繰り返す為の関数を作成する。

繰り返し処理の関数を作成

関数recalcの中に関数recalcを1秒ごとに繰り返す為の関数を呼び出す記載をし、
また、1秒ごとに繰り返す為の関数を作成。

これ実装する為にsetTimeoutメソッドを使用する。

書き方:setTimeout(処理内容,実行タイミング)
setTimeoutメソッドは、第二引数に与えられた実行タイミング(ミリ秒)で、第一引数に定義された
処理内容を1度実行する。
つまりは、ある処理を一定時間後に実行するように命令することができる、すなわち繰り返して実行してくれるメソッド。

繰り返して実行してくれる関数の名をrepeatという名で作成。
そして、setTimeoutメソッドの第一引数には、再計算する関数recalcを、
第二引数には、1秒ごとに繰り返すので、1秒はミリ秒単位で1000なので、1000を指定する。
ちなみに、引数に関数を指定する際には、関数の後ろの( )は付けない。

JavaScript
function recalc() {
  const counter = countdown(goal);
  const time = `${counter[1]}時間${counter[2]}${counter[3]}`;
  document.getElementById('timer').textContent = time;
  // 関数repeatの呼び出す
  repeat(); 
}
// 1秒ごとに繰り返す為の関数repeatを定義
function repeat() {
  setTimeout(recalc, 1000);
}

recalc();


これで、残り時間が刻一刻と変化するカウントタイマーの完成。
下記が完成図

JavaScript
'use strict';

function countdown(set) {
  const now = new Date();

  const rest = set.getTime() - now.getTime();
  const sec = Math.floor(rest / 1000) % 60;
  const min = Math.floor(rest / 1000 / 60) % 60;
  const hours = Math.floor(rest / 1000 / 60 / 60) % 24;
  const days = Math.floor(rest / 1000 / 60 / 60 / 24);
  const count = [days, hours, min, sec];

  return count;
}

let goal = new Date();
goal.setHours(23);
goal.setMinutes(59);
goal.setSeconds(59);

function recalc() {
  const counter = countdown(goal);
  const time = `${counter[1]}時間${counter[2]}${counter[3]}`;
  document.getElementById('timer').textContent = time;
  repeat();
}

function repeat() {
  setTimeout(recalc, 1000);
}

recalc();


補足

さらにちょっとだけ手を加えて、
下記のようなもの作成(ちょっと悪ふざけが過ぎる気がするが。。。)
image

これを実装するにあたって、
前回のコードと異なる部分は、

-Dateオブジェクトのよる日時設定方法
-padStartメソッドを使用
-Stringオブジェクトを使用

  • Dateオブジェクトのよる日時設定方法

今までは、setHoursメソッド等を使用して日時の設定をおこなってきたが、
今回は、日時を設定した状態でDateオブジェクトを初期化する。

new Date( )の( )内にパラメータを含めておくと、予め日時を設定した状態で初期化出来る。
ただし、パラメータのうち、「年」、「月」は必須なので、ここの2つの設定しない場合はこのやり方は出来ない。
また、注意する点として、月の設定は、「実際の月−1」とする必要があるので、
今回で有れば7月で設定する場合は6を指定しなければならない点に注意。

書き方は、

new Date(年、 月、 日、 時、 分、 秒、 ミリ秒 )

実際に書くコードはこちら

JavaScript
const goal = new Date(2999, 6, 31);


今度は数字の桁数を揃える為に下記の事をする。
一番の上の完成図を見て貰えれば分かるが、秒数が一桁の時と二桁の時では、桁数が異なる為
ずれてしまい、ちょっとカッコが悪いので、それを修正する。

  • padStartメソッド

padStartメソッドとは、指定した長さに合わすため、前方に文字列を加えるメソッド。
文字列を加えるので、第二引数にはクォテーションが付いている。
言葉だけだと理解しにくいので、例を交えて説明。

先ずは、書き方

文字列.padStart(揃える文字数, 埋め合わせ用の文字)

次に、padStartメソッドで、頭にゼロをつけるサンプル

JavaScript
const str = "90"
  console.log(str.padStart(3, "0")); 
  // 出力結果 → 090
  console.log(st1.padStart(5, "0"));
  // 出力結果 → 00090
  console.log(st1.padStart(2, "0"));
  // 出力結果 → 90


  • Stringオブジェクト

Stringオブジェクトは、文字列を扱うオブジェクト。
なので、String( )は、( )の内のパラメータとして渡されたデータを文字列に変化する。

今回、padStartメソッドを使用して揃えたい文字列の対象は、counter[2]の分とcounter[3]の秒
という数値のデータです。数値のデータのままだとpadStartメソッドが使用出来ない為、Stringオブジェクトを
使用して文字列に変換する。

書き方はこんな感じになる

JavaScript
String(counter[2]).padStart(2, '0');
String(counter[3]).padStart(2, '0');



これで異なる実装は完了、
後は完成コードを下記に記載

JavaScript
'use strict';

function countdown(set) {
  const now = new Date();

  const rest = set.getTime() - now.getTime();
  const sec = Math.floor(rest / 1000) % 60;
  const min = Math.floor(rest / 1000 / 60) % 60;
  const hours = Math.floor(rest / 1000 / 60 / 60) % 24;
  const days = Math.floor(rest / 1000 / 60 / 60 / 24);
  const count = [days, hours, min, sec];

  return count;
}

const goal = new Date(2999, 6, 31);

function recalc() {
  const counter = countdown(goal);
  document.getElementById('day').textContent = counter[0];
  document.getElementById('hour').textContent = counter[1];
  document.getElementById('min').textContent = String(counter[2]).padStart(2, '0');
  document.getElementById('sec').textContent = String(counter[3]).padStart(2, '0');
  repeat();
}

function repeat() {
  setTimeout(recalc, 1000);
}

recalc();
HTML
<h2>大予言です<br>
2999年7月世界が滅亡します</h2>
<p>あと<span id="day"></span><span id="hour"></span>時間<span id="min"></span><span id="sec"></span></p>


過去投稿記事

【JavaScript ~変数・定数、if文・switch文~】勉強メモ
【JavaScript ~for文、配列、オブジェクトについて~】勉強メモ②
【JavaScript ~関数について~】勉強メモ③
【JavaScript ~クラスやインスタンス、メソッドについて~】勉強メモ④
【JavaScript ~ファイルの分割について~】勉強メモ⑤
【JavaScript 読み込み】勉強メモ⑥
【JavaScript ~配列のメソッド~】勉強メモ⑦
【JavaScript ~コールバック関数~】勉強メモ⑧
【JavaScript ~HTMLを置き換え、ダイアログボックス~】勉強メモ⑨
【JavaScript ~イベント~】勉強メモ⑩
【JavaScript ~イベント(入力内容を取得)とDateオブジェクト~】勉強メモ11
【JavaScript ~Mathオブジェクト~】勉強メモ12
【JavaScript ~カウントダウンタイマーの作成~】勉強メモ13

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

ドラクエ5の結婚イベントを考える

結婚する相手毎にメッセージを変更する。

index.html.rb
<script>
  'use strict';
  if(window.confirm('ビアンカと結婚しますか?')) {
    window.alert('偽善者が!!!');
  } else {
    if(window.confirm('フローラと結婚しますか?')) {
    window.alert('金の亡者が!!!!');
  } else {
    if(window.confirm('デボラと結婚しますか?')) {
    window.alert('性欲猿が!!!');
  } else {
    window.alert('一生独身決定!!!');
  }}}
</script>  

コードの内容はwindow.confirm()とwindow.alert()、条件式if(){}だけなのでとてもシンプルです。
ドラ○エらしくYes/Noで作りたかったのでこの形を取りました。
windows.prompt()を使えばもっと短く表現できるのであとで追記しようかと思っています。

ちなみに私はフローラを選びました(笑)

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

Udemy フルスタック・Webエンジニア講座 セクション5.jQuery



jQueryとは


jQueryとは、JavaScriptで作成されたライブラリで、JavaScriptの書き方などを簡単にすることが出来る。実際、私がJavascriptを勉強した後にjQueryを学習してみると、Javascriptよりコードを書く量が少なく、簡単に書くことが出来た。

clickの使い方

clickのメソッドは、要素をクリックすることでイベントを呼び出すことが出来る。

test.js
$("#circle").click(function(){
    alert("円がクリックされました!");
});

上のようなコードを書くと、円("#circle")をクリックすることで『円がクリックされました!』というアラート(alert)を呼び出すことが出来る。

test.js
$("#circle").click(function(){
    $("#circle").fadeOut("slow");
});

上のようなコードを書くと、円が徐々に消えていくイベントをつくることが出来る。"slow"の他に"nomal"や"fast"を使うことで文字の消えていく速さを変えることが出来る。

jQuery UI

jQuery UIでは、ユーザーインターフェイスの相互作用、効果、ウィジェット、およびテーマのセット参考にできる。起こしたいイベントをjQuery UIから探して参考にすれば、効率的にコードを書くことが出来るだろう。
jQuery UIのページ

他にも、jQuery日本語リファレンスというページもjQueryの構文が書いてあるので参考にしてみよう。
jQuery日本語リファレンスのページ

よく出たエラー

要素の中身を()や{}で囲い忘れてしまい、エラーがよく出た。しかし、前回の反省でインテンドをそろえていたのですぐに修正することが可能であった。やはりコードを書く際は綺麗さを重視することが大事なのだと再認識した。

最後に

プログラミングの勉強をしていく中で、これから自分は将来どうしていくかどうかのキャリアを考えてみた。私は現在大学三年生であり、周りの同年代の友人はインターンシップなどの就職活動を本格的に始めている。とりあえず私は現在のインターン先で提示されている課題を取り組み、プログラミング技術の向上を図りたい。そして、大学四年生でWeb系企業に内定をもらえるよう努力する。そして就職した後、そこの企業で実務を経験し、より自らのスキルを強化する。これを実現。
また、どのプログラミング言語を中心にやっていくかどうかは、まだ、プログラミングを深く学んでいないので、現時点でははっきりとは言えない。なのでこれから勉強を進めていく中で自分がなにがしたいのか決定できるようにしたい。

最後に
現在ここでインターンしています。
まだまだ駆け出しですが頑張ります!やる気は人一倍です!
https://senren.work/

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

Vue歴5日でポートフォリオサイトを作成する 1

JavaScriptはRailsでアプリを作成する際に数行しか書いたことがありませんでしたが、Vueを触り始めて5日ほどで簡単なポートフォリオサイトなら作成することが出来ました。その過程について書いていきます。

私のポートフォリオサイト

事前準備

手始めにVueで開発する為に必要なものをインストールしていきます。

コマンドラインインタフェースをインストールします。

$ npm install -g @vue/cli

次にプロジェクトを作成します。今回はリファレンスの多さからVue2で書いています。

$ vue create my_portfolio

プロジェクトの作成が完了した後、一旦立ち上げてみます。

$ cd my-portfolio
$ npm run serve

以下のメッセージが表示されましたら、localhost:8080にアクセスします。

Your application is running here: http://localhost:8080

EmsM2m2UcAAgVq3.jpeg

導入成功です。

次にVue.js Devtoolsを導入していきます。これはChromeブラウザの拡張機能で、Vueで書かれたコード内のデータを手っ取り早く確認することが出来る便利な機能です。導入した後Devtoolを開くとVueと書かれたタブが表示されているはずです。Vueファイルが読まれている時はアイコンがアクティブになります。
スクリーンショット 2020-11-24 20.35.10.png

これにて事前準備は完了です。

Vueと仲良くなる

publicディレクトリのindex.htmlを見てみます。

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

注目するべきはbodyタグ内の<div id="app">です。このappは一体何者でしょうか?Devtoolで確認してみると...スクリーンショット 2020-11-24 20.55.37.png

何やらdivタグの中でさらに展開されています。
この中身についてはApp.vueを見てみましょう。

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

index.htmlのappはこれを呼び出しているみたいです。
さらにApp.vueの中でHelloWorldというcomponentを読むことで部品としてHelloWorldをタグに使用していますね。
Vueではindex.htmlをそのまま表示するのではなくてbuildを挟むことで./dist/index.htmlへと変換します。これをブラウザで表示している訳です。

次にmain.jsを確認します。

main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

まずVueを動かすためのインスタンスをvueから作り出し、先ほどの<div id="app">にマウントしていることがわかります。だからindex.html内でApp要素が表示されたんですね。

Vueの基本

データバインディング

まずはデータバインディングをやってみます。
データバインディングをするには要素をマスタッシュ(二重の中括弧)で囲みます。
scriptタグにはdata()で要素をreturnしてあげます。

App.vue
<template>
  <div id="app">
    {{ msg }}
  </div>
</template>

<script>

export default {
  name: 'App',
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

Vueはホットリローディングしているので、scriptタグ内のmsgを変更するとそれにしたがってテキストが自動変更されます。上のコードの場合ページ上にData-binding!の文字だけ表示されるはずです。

コンポーネント

コンポーネントはVueファイルをそれぞれ部品のように扱うことで、分離して開発することが可能な機能です。使うには呼び出したいVueをscriptタグ内でimportしてからcomponentsに登録します。templateタグ内でタグとして使用できます。

App.vue
<template>
  <div id="app">
    {{ msg }}
    <HelloWorld></HelloWorld>
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld.vue"

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

HelloWorld.vueに書かれていた内容が表示されたかと思います。

フォームとの同期

v-modelを使うことで入力や選択をDOMに反映させることができます。

App.vue
<template>
  <div id="app">
    {{ msg }}
    <input type="text" v-model="msg">
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

フォームに入力したテキストが即座に反映されていることが確認できるかと思います。
Vueにはまだまだ機能がありますが、列挙するとキリがないので、次回からポートフォリオ作成編に入っていきます。次回もよろしくお願いします。

次回

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

JavaScriptをまともに触ったことない初心者がVueを利用したポートフォリオサイトを作成する 1

JavaScriptはRailsでアプリを作成する際に数行しか書いたことがありませんでしたが、Vueを触り始めて5日ほどで簡単なポートフォリオサイトなら作成することが出来ました。その過程について書いていきます。

私のポートフォリオサイト

事前準備

手始めにVueで開発する為に必要なものをインストールしていきます。

コマンドラインインタフェースをインストールします。

$ npm install -g @vue/cli

次にプロジェクトを作成します。今回はリファレンスの多さからVue2で書いています。

$ vue create my_portfolio

プロジェクトの作成が完了した後、一旦立ち上げてみます。

$ cd my-portfolio
$ npm run serve

以下のメッセージが表示されましたら、localhost:8080にアクセスします。

Your application is running here: http://localhost:8080

EmsM2m2UcAAgVq3.jpeg

導入成功です。

次にVue.js Devtoolsを導入していきます。これはChromeブラウザの拡張機能で、Vueで書かれたコード内のデータを手っ取り早く確認することが出来る便利な機能です。導入した後Devtoolを開くとVueと書かれたタブが表示されているはずです。Vueファイルが読まれている時はアイコンがアクティブになります。
スクリーンショット 2020-11-24 20.35.10.png

これにて事前準備は完了です。

Vueと仲良くなる

publicディレクトリのindex.htmlを見てみます。

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

注目するべきはbodyタグ内の<div id="app">です。このappは一体何者でしょうか?Devtoolで確認してみると...スクリーンショット 2020-11-24 20.55.37.png

何やらdivタグの中でさらに展開されています。
この中身についてはApp.vueを見てみましょう。

App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

index.htmlのappはこれを呼び出しているみたいです。
さらにApp.vueの中でHelloWorldというcomponentを読むことで部品としてHelloWorldをタグに使用していますね。
Vueではindex.htmlをそのまま表示するのではなくてbuildを挟むことで./dist/index.htmlへと変換します。これをブラウザで表示している訳です。

次にmain.jsを確認します。

main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

まずVueを動かすためのインスタンスをvueから作り出し、先ほどの<div id="app">にマウントしていることがわかります。だからindex.html内でApp要素が表示されたんですね。

Vueの基本

データバインディング

まずはデータバインディングをやってみます。
データバインディングをするには要素をマスタッシュ(二重の中括弧)で囲みます。
scriptタグにはdata()で要素をreturnしてあげます。

App.vue
<template>
  <div id="app">
    {{ msg }}
  </div>
</template>

<script>

export default {
  name: 'App',
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

Vueはホットリローディングしているので、scriptタグ内のmsgを変更するとそれにしたがってテキストが自動変更されます。上のコードの場合ページ上にData-binding!の文字だけ表示されるはずです。

コンポーネント

コンポーネントはVueファイルをそれぞれ部品のように扱うことで、分離して開発することが可能な機能です。使うには呼び出したいVueをscriptタグ内でimportしてからcomponentsに登録します。templateタグ内でタグとして使用できます。

App.vue
<template>
  <div id="app">
    {{ msg }}
    <HelloWorld></HelloWorld>
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld.vue"

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

HelloWorld.vueに書かれていた内容が表示されたかと思います。

フォームとの同期

v-modelを使うことで入力や選択をDOMに反映させることができます。

App.vue
<template>
  <div id="app">
    {{ msg }}
    <input type="text" v-model="msg">
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      msg: "Data-binding!"
    }
  }
}
</script>

フォームに入力したテキストが即座に反映されていることが確認できるかと思います。
Vueにはまだまだ機能がありますが、列挙するとキリがないので、次回からポートフォリオ作成編に入っていきます。次回もよろしくお願いします。

次回

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

配列内の文字列を置換する

配列内の文字列を置換したい

userNameをたけしくんにする覚えがきです

index.js
const array = ["userNameくんかっこいいよね","userNameくんどこに住んでるの" ,"userNameくん乳首透けてるよ"]
index.js
  //配列を展開
  array.forEach((text) => {
    //指定した文字列が入っているか判断する(userName)
      const stringInUserName = text.includes("userName");
      if (stringInUserName) {
  //tureならuserNameをたけしくんにする
        text = text.replace("userName", "たけし");
      }
      return text;
    });
index.js
  たけしんくんかっこいいよね たけしくんどこにすんでるの たけしくん乳首透けてるよ

以上

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

getBoundingClientRectとツールチップ

この記事は

JavaScriptのgetBoundingClientRectを用いたツールチップについての解説です。

環境

Vue2
TypeScript(decorator使用)

背景

?アイコンにカーソルを当てた時だけに表示させる汎用的なツールチップを作りたい。

課題

?アイコンの直下に表示させたい。
画面をスクロールしてもそれに追従させたい。

解決

ツールチップコンポーネントtemplate

ソース

<template>
  <span
    @mouseover="activate"
    @mouseleave="deactivate"
  >
    <slot name="activator"></slot>
    <div
      :class="{'tool-tip__content--active': isActive}"
      :style="contentStyleObj"
      ref="content"
    >
      <slot></slot>
    </div>
  </span>
</template>

解説

mouseoverとmouseleaveでツールチップの表示有無処理を発火します。

<span
  @mouseover="activate"
  @mouseleave="deactivate"
>

?アイコンは親コンポーネントからslotしてきます。

<slot name="activator"></slot>

表示フラグが立っている時にツールチップ本体を表示します。

:class="{'tool-tip__content--active': isActive}"

script部で算出した座標をスタイルに反映します。

:style="contentStyleObj"

ツールチップコンポーネントscript

TypeScriptを用いています。

<script lang="ts">
import { Component, Prop } from 'vue-property-decorator';
import BaseView from '@/views/Base';

@Component
export default class ToolTipComponent extends BaseView {
  // ツールチップ表示フラグ
  isActive = false;

  // 表示位置
  contentPosition = {
    top: 0,
    left: 0
  }

  // ツールチップを表示する基準位置
  defaultPosition = {
    top: 40,
    left: 100
  }

  // ?アイコンにマウスオーバーした時の表示処理
  activate() {
    const target = event!.target as any;
    const rect = target!.getBoundingClientRect();
    this.contentPosition.top = rect.top + window.pageYOffset + this.defaultPosition.top;
    this.contentPosition.left = rect.left - this.defaultPosition.left;

    this.isActive = true;
  }

  // マウスオーバーの解除で非表示
  deactivate() {
    this.isActive = false;
  }

  // 座標をスタイルに指定
  get contentStyleObj() {
    return {
      top: `${this.contentPosition.top}px`,
      left: `${this.contentPosition.left}px`,
    }
  }

  // 一番上の親要素の直下にツールチップを配置する
  mounted() {
    const app = document.getElementById('app');
    app!.appendChild(this.$refs.content as any);
  }

  // コンポーネント破壊時にツールチップを削除する
  beforeDestroy() {
    const app = document.getElementById('app');
    app!.removeChild(this.$refs.content as any);
  }
}
</script>

解説

表示処理では、getBoundingClientRect()を用いてビューポートからの座標を取得します。
ツールチップの表示位置をデフォルトポジションとして調節し、表示座標を算出します。
その後、表示スタイルを有効にするためのフラグを立てます。

activate() {
  const target = event!.target as any;
  const rect = target!.getBoundingClientRect();
  this.contentPosition.top = rect.top + window.pageYOffset + this.defaultPosition.top;
  this.contentPosition.left = rect.left - this.defaultPosition.left;

  this.isActive = true;
}

そして算出された座標をスタイルに渡すゲッターを定義します。

get contentStyleObj() {
  return {
    top: `${this.contentPosition.top}px`,
    left: `${this.contentPosition.left}px`,
  }
}

ツールチップは画面描画時には非表示ですが、DOM内には配置しておきます。
一番外側の要素の子要素として配置します。
画面遷移時等のコンポーネント破壊時には配置した要素も忘れず削除するようにしましょう。

// 一番上の親要素の直下にツールチップを配置する
mounted() {
  const app = document.getElementById('app');
  app!.appendChild(this.$refs.content as any);
}

// コンポーネント破壊時にツールチップを削除する
beforeDestroy() {
  const app = document.getElementById('app');
  app!.removeChild(this.$refs.content as any);
}

利用コンポーネント

親コンポーネントでは?アイコンをactivatorスロットに配置。
またツールチップ内の文章はデフォルトスロットに配置します。

<ToolTip class="tool-tip" v-show="toolTipMessage">
  <template slot="activator">
    <img src="@/assets/img/icon/icon-help.png" alt="help">
  </template>
  <div class="tool-tip__help-text">
    {{ toolTipMessage }}
  </div>
</ToolTip>

まとめ

少々トリッキーなコンポーネントとなってしまいましたが、getBoundingClientRectはこういう使い方もできる点は参考になりますね。
応用もできると思うので、座標を取って何かするような動作のコンポーネントに使うと良いかもしれません。
間違っている点、改善点あればご教授頂けますと幸いです。

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

Nuxt.js、Firebase、axiosでパパッと掲示板!

この記事の概要

超簡単な掲示板アプリをパパっと作成します。
細かいことはいいからとりあえずNuxt.jsで何かアプリを作ってみたいという方にオススメです。

以前書いた記事のNuxtバージョンです。

目標物

demo

開発環境

・macOS Catalina 10.15.7
・@nuxt/cli v2.14.7
・npm 6.9.0
・node v10.16.0

前提

・nuxtのプロジェクトが作成済み。
・firebaseのプロジェクトを作成済み。
・Cloud FirestoreのDBを作成済み。

axiosの導入

プロジェクト直下でnpm add @nuxtjs/axiosを実行

ターミナル
プロジェクト名$ npm add @nuxtjs/axios 

nuxt.config.jsに以下の様に記述

nuxt.config.js
export default {

  //・・・省略

  modules: [
    '@nuxtjs/axios'
  ],

  //・・・省略

View作成

pages/index.vue
<template>
  <div>
    <h1>掲示板!</h1>
    <br>
    名前
    <div>
      <input type="text" v-model="name">
    </div>コメント
    <div>
      <textarea v-model="comment"></textarea>
    </div>
    <br>
    <button @click="submitPosts">投稿する</button>
    <br><br>
    <h2>投稿一覧</h2>
  </div>
</template>

<script>
export default {
  deta() {
    return {
      name: '',
      comment: ''
    }
  },
  methods: {
    submitPosts() {
      console.log('submit');
    }
  }
}
</script>

とりあえず【投稿】ボタンを押したら【submit】と出力させておきましょう。
スクリーンショット 2020-11-18 12.06.48.png


データを送る

データを送る為にthis.$axios.$post()を使用します。
第一引数:サーバーのURL
第二引数:データの内容
第三引数:オプション(任意)

pages/index.vue
<script>
export default {
  deta() {
    return {
      name: '',
      comment: ''
    }
  },
  methods: {
    submitPosts() {
//----↓ここから--------------
      this.$axios.$post(
        "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/posts",
        {
          fields: {
            name: {
              stringValue: this.name
            },
            comment: {
              stringValue: this.comment
            }
          }
        }
      ).then(() => {
        this.name = '';
        this.comment = '';
      });
//---↑ここまで--------------
    }
  }
}
</script>

URLは
https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/cities/LA
↑を入れます。
こちらに乗ってあるのを持って来ただけです。


しかしこれでは不十分で、URL内のYOUR_PROJECT_IDの部分を自分のプロジェクトIDに置き換える必要があります。

プロジェクトIDはここに記載されています
スクリーンショット 2020-11-19 13.37.52.png

スクリーンショット 2020-11-19 13.36.27.png


以下、適宜YOUR_PROJECT_IDを自分のプロジェクトIDに置き換える必要があることに注意してください。
YOUR_PROJECT_IDのままだと以下のエラーが出ます。
スクリーンショット 2020-11-24 20.39.13.png


次に、URL末尾のcities/LAを任意のコレクション名(データを格納する場所の名前)にします。
今回はpostsとします。

最終的に↓になります。
https://firestore.googleapis.com/v1/projects/自分のプロジェクトID/databases/(default)/documents/posts,


URLの変更ができたらデータを送ってみましょう。
スクリーンショット 2020-11-19 14.14.56.png

スクリーンショット 2020-11-19 14.15.31.png


データが入っています!

データの取得

では今度はサーバーからデータを取ってきましょう。
データの取得はthis.$axios.$get()を使用します。
第一引数:サーバーのURL
第二引数:オプション(任意)

サーバーのURLはthis.$axios.$post()で使用したものと全く同じです。

取得するタイミングはロード時データ送信時に行いたいので、
getPostsメソッドを作り各所で呼び出しましょう。

pages/index.vue
<script>
export default {
  data() {
    return {
      name: '',
      comment: ''
    };
  },
//----↓ここから--------------
  created() {
    this.getPosts();
  },
//----↑ここまで--------------
  methods: {
    submitPosts() {
      this.$axios.$post(
        "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/posts",
        {
          fields: {
            name: {
              stringValue: this.name
            },
            comment: {
              stringValue: this.comment
            }
          }
        }
      )
      .then(() => {
        this.name = '';
        this.comment = '';
//----↓ここ--------------
        this.getPosts();
      });
    },
//----↓ここから--------------
    getPosts() {
      this.$axios.$get(
        "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/posts"
      )
      .then(res => {
        console.log(res.documents);
      });
    }
//----↑ここまで--------------
  }
};
</script>

.then(res => {
console.log(res.documents);
});

このresの中に取得したデータが入っているので確認してみます。
スクリーンショット 2020-11-19 14.46.43.png

バッチリ入っています。
あとはこの配列をv-forで順番に表示させていきます。

データの表示

dateに空配列postsを準備。
getPostsが呼ばれたタイミングでres.data.documentsを配列postsに格納。
・配列postsをリストレンダリングしています。

pages/index.vue
<template>
  <div>
    <h1>掲示板!</h1>
    <br>
    名前
    <div>
      <input type="text" v-model="name">
    </div>コメント
    <div>
      <textarea v-model="comment"></textarea>
    </div>
    <br>
    <button @click="submitPosts">投稿する</button>
    <br><br>
    <h2>投稿一覧</h2>
    <br>
<!-----↓ここから-------------------------------------------------------->
    <div v-for="post in posts" :key="post.name">
      <hr>
      <br>
      <p>名前:{{post.fields.name.stringValue}}</p>
      <br>
      <p>コメント:{{post.fields.comment.stringValue}}</p>
      <br>
    </div>
<!-----↑ここまで-------------------------------------------------------->
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: '',
      comment: '',
//----↓ここ--------------------------
      posts: ''
    };
  },
  created() {
    this.getPosts();
  },
  methods: {
    submitPosts() {
      this.$axios.$post(
        "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/posts",
        {
          fields: {
            name: {
              stringValue: this.name
            },
            comment: {
              stringValue: this.comment
            }
          }
        }
      ).then(() => {
        this.name = '';
        this.comment = '';
        this.getPosts();
      });
    },
    getPosts() {
      this.$axios.$get(
        "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/posts"
      )
      .then(res => {
//----↓ここ--------------------------
        this.posts = res.documents;
      });
    }
  }
};
</script>

完成!

スクリーンショット 2020-11-19 14.55.33.png


ここまで見て頂きありがとうございました!

とりあえず作って動かすを目的にしているので細かい解説はしていません(←できません)

コピペで動かす際はURLのYOUR_PROJECT_IDを適宜自身のプロジェクトIDに置き換えることを注意してください。

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

Javascript再び

前回の復習も兼ねてまた始めてみようと思う。
憎きjavascriptに復讐。

出力とコメント

出力って。いちいち言葉が難しい。私は「何らかのデータ(結果)を表示すること」と解釈した。

出力の種類

  • window.alert()
  • document.write()
  • innerHTMLプロパティ
  • console.log

console.logから馴染んでいこうと思う

コメントアウトのやり方

  • 1行のコメントは「//」 //1行コメントをここに書く
  • 複数行のコメントアウトの場合ははじめに/*開始~終わり*/を記載します。(cssと同じだね!

(コメントアウトって、チーム内でのコードの役割や使い方のメモの他に、自分用でも内容整理のために使っていくと良いと思った。

デバッグについて

console.logを使用したデバッグを使う方法で行っていく(Googleが提供しているChrome Developer Toolsを利用)

演習

//文字列"りんご"を出力
console.log("りんご");

//割り算の結果
console.log(10 / 5);

// 34を4で割った余り
console.log(34 % 4);

//変数の掛け算
let fu = 10;
let ba = 4;
console.log(fu * ba);

var / let / const

JavaScriptで書く「var,let,const」の違いと使い分け方法

var、let、constとは、JavaScriptで変数を宣言する際に使うキーワード。
その中で、letとconstは、ECMAScript2015から採用された、新しい宣言方法のキーワード。

var
- varでは再宣言、再代入が可能
varで宣言された変数は関数スコープ(function scope)を持つため、その変数には宣言された関数のどこからでもアクセスできる。

let
- letでは再宣言が禁止。
- letで宣言された変数はブロックスコープ(block scope)を持つ。つまり、それらの変数にアクセスできるのは、それらの変数が宣言されたブロック(またはサブブロック)の内側だけ。

const
- constでは、再宣言、再代入が禁止

const > let > var
varはもうほとんど使わないらしい?

▼参考にしたページ
https://codezine.jp/article/detail/11353
https://techacademy.jp/magazine/14872

言葉難しすぎるんよ・・・・・。このへんもう少し理解不足だなぁ;

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

TypeScriptの基本的な型

参考

超TypeScript入門 完全パック(2020)
→非常に分かりやすいです。ありがとうございます。

Typescript 型についてのメモ

udemyの講座より学んだTypeScriptの型に関するメモになります。
初学者故に意味を履き違えている箇所がある可能性があります。
お気づきになられた方はお手数ですがご指摘いただけると幸いです。

個人的にJavaやC#をかじった経験があるので、TypeScriptの書き方は
非常に馴染みやすく、スッと頭に入りました。
Java、C#のご経験のある方は一度触れてみてはいかがでしょうか!

boolean

let hasValue: boolean = true;

(因みに)
変数名をマウスホバーすることで、どの型で設定されたかがわかる。


number

numberで整数、負数、不動小数すべてを表現

let count: number = 10;
let pi: number = 3.14;
let nega: number = -50;

string

let single: string = 'hoge';
let double: string = "hoge";
let back: string = `hoge`;

型注釈と型推論

型注釈

以下の部分
let single: string = 'hoge'

型推論

型注釈を書かなかった場合、tsが推測で型を認識する。
let hasValue = true;
→ コレは多分booleanだ!

結論

  • 基本的には型推論、それ以外の初期化しない場合などに型注釈を用いる
  • anyについては型注釈を用いる

オブジェクトに型をつける

型注釈

const person: {
  name: string; //★
  age: number;  //★ コロンであることに注意!!
} = {
  name: 'jack',
  age: 25
}

or 型推論で直接オブジェクトリテラルを記述するのがよい。

const person = {
  name: 'jack',
  age: 25
} 

配列

型注釈あり(特定の型)

const fruits: string[] = ['Apple','Banana','Lemon']

型注釈あり(any型)

const fruits: any[] = ['Apple',3,'Banana','Lemon',5]

型注釈なし

// Union型になる 
const fruits = ['Apple',3,'Banana','Lemon',5]

Tuple

要素が決まったオブジェクトのような配列を定義したい場合
※明示的に型注釈をつける必要がある
※初期値にのみ制限をかける。pushする際は制限が無い

const book: [string, number, boolean] = ['business'. 1500, true]

Enum

enumのリテラルに文字列を与えない場合、値は数値の連番でinitializeされる

// enumの命名規則は以下のようにする
enum CoffeeSize {
  SHORT = 'SHORT',
  TALL = 'TALL',
  GRANDE = 'GRANDE',
  VENTI = 'VENTI'
}

const coffe = {
  hot: true,
  size: CoffeeSize.TALL
}

(enumのコンパイル後)

// オブジェクトになる
var CoffeSize;
(function (CoffeSize) {
    CoffeSize["SHORT"] = "SHORT";
    CoffeSize["TALL"] = "TALL";
    CoffeSize["GRANDE"] = "GRANDE";
    CoffeSize["VENTI"] = "VENTI";
})(CoffeSize || (CoffeSize = {}));

any

何でも入れることができる型
素のjavascriptと同じ動きになる
※なるべく使わないようにする


unknown

anyと似ているが、anyより微妙に厳しくなっている

let unknownInput: unknown = 20;
let anyInput: input = "hello";
let text: string;
text = anyInput;     // OK
text = unknownInput; // NG
// unknownを使用する際
if(typeof(unknownInput) === 'string') {
  text = unknownInput;
}

union

複数の型を受け入れる

// 変数
let unionType :number | string = 10;
unionType.toUpperCase(); // エラー
unionType = 'hello';
unionType.toUpperCase(); // エラーにならない

// 配列
let unionTypes: (number | string)[] = [21, 'hello'];

リテラル

const apple: 'apple' = 'hello' // エラー
const num: 20 = 30 // エラー

// ■ unionと同時に使う
// union + リテラル ≒ Enumのように扱える
let clothSize: 'small' | 'medium' | 'large' = 'large';

const cloth: {
  color: string;
  size: 'small' | 'medium' | 'large';
} = {
  color: 'white',
  size: 'medium' //or small or large
}

TypeAlias

型に別名をつけて扱う

type ClothSize = 'small' | 'medium' | 'large';
let clothSize: ClothSize = 'large';
// = let clothSize: 'small' | 'medium' | 'large' = 'large';

関数(宣言時)に型をつける

引数と戻り値につける

// function add(num1[: 引数の型], num2[: 引数の型])[: 戻り値の型]
function add(num1: number, num2: number): number {
  return num1 + num2
}

引数の型推論  → anyになる、やらない方がいい
戻り値の型推論 → 冗長になる場合以外は書く 

戻り値を返さない場合

function sayHello(): void {
  console.log('Hello!');
}
// しかし、上記の場合undefinedは返却される

// しかし以下のように型注釈でundefinedを記述するのはtypescriptで許されていない
function sayHello(): undefined { }

戻り値は無いが明示的にreturnするパターン

function sayHello(): void {
  console.log('Hello!');
  return;
}
// この場合であればsayHello()自体の型注釈はundefinedでもvoidでも許可される

関数(変数代入時)に型をつける

const anotherAdd: (n1: number, n2: number) => number  // ←これは戻り値
  = function (num1: number, num2: number): number {
    return num1 + num2
  };

// 左辺か右辺どちらかに型情報があればOK
const anotherAdd: (n1: number, n2: number) => number  // ←これは戻り値
  = function (num1, num2) {
    return num1 + num2
  };

アロー関数

const doubleNumber: (num: number) => num = num * 2;

関数式の戻り値には :number ではなく => number で表現する


コールバック関数に型をつける

// cb: (num: number): void 
// → 引数に1つのnumberをとりnumberの戻り値を返す関数
function doubleAndHandle(num: number, cb: (num: number) => number) {
  const doubleNum = cb(num * 2);
  console.log(doubleNum);  
}

doubleAndHandle(21, doubleNum => {
  return doubleNum;
});

// コールバック関数の戻り値宣言にvoid,nullを設定すると
// その定義した関数は機能しなくなる

never型

戻り値が無い関数につける型
ver3〜登場している。
neverを明示的に付与しない場合の型推論は「: void」

// never 
function error(message: string): never {
  throw new Error(message);     
}

console.log(error('this is an error'));
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

続・「シューティングゲームの当たり判定をQRコード読み取りでやってみた」やつをスマホで使えるようにした。

スマホでシューティングゲーム

まずは前回の記事を参照してください。

シューティングゲームの当たり判定をQRコード読み取りでやってみた

構成(前回との変更点)

image.png

射撃:Webアプリ内のボタン押下 → obnizのボタン押下
カメラ:フロントカメラ → リアカメラ(背面カメラ)

動かしてみたいかたは↓のURLをクリック

https://nervous-ardinghelli-c10e69.netlify.app
※リアカメラがない端末では動きません
※スマホの場合は横向きにしてください
※ご自身のobnizをご用意ください

■obnizについては↓を参照
https://obniz.com/ja/products/obnizboard

コード

See the Pen QRコードで当たり判定。トリガーはobniz by sawa (@sawakoshi_yy) on CodePen.

課題

・スマホでフルスクリーンにできない

<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

上のタグを入れてもブラウザのヘッダー・フッターが消えない
 →Webアプリに接続したら、フルスクリーンでカメラを表示させたい

・射撃時に音がでない。

var audio = new Audio('https://soundeffect-lab.info/sound/battle/mp3/beamgun1.mp3');
audio.play();

上を追加しても音がでない。

・射撃時にヒットしたら、LINEnotifyで通知をしたいがCORSのエラーを解消できない
↓記事参照
CORSのエラーが出たけど解消できなかったときのメモ

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

prettierの使い方とコーディングルールの登録

prettier

コードの整形ができるツール。ESLintと併用でき、ESLintよりも整形力にすぐれている。

install

npm i prettier
// ESLintと併用 + Googleのcoding rule
npm i prettier-eslint prettier-eslint-cli eslint-config-google

setting

登録したいルールを下記のように記述する。

// .prettierrc.js ( new file )

module.exports = {
  singleQuote: true,
  semi: false,
}

packageも書き加える。

// package.json

"scripts": {
  "format": "prettier --write 'src/**/*.{js,json,md,html,ts}'" // +
}

また、ESLintの設定もできる。

// eslintrc.json ( new file )

{
  "env": {
    "browser": true,
    "es6": true
  },
  "extends": ["eslint:recommended", "plugin:prettier/recommended"],
  "rules": {
    "prettier/prettier": [
      "error",
      {
        "singleQuote": true,
        "trailingComma": "es5"
      }
    ]
  }
}

run

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

JavaScriptの変数宣言 let,ver,const

何度調べても覚えられないのでざっくりまとめ

結論

const

定数
一度入力した値は変更されない

let

ローカル変数
一度入力した値を変更できる

var

グローバル変数
いつでもどこでも変更可能
なるべく使わない

使い方

上に書いたやつ優先で使う
constが最優先で、次点がlet

使用例

雰囲気で感じろ

Sample.js
if(true){
    const hogeConst = "hoge1";
    let hogeLet     = "hoge2";
    var hogeVar     = "hoge3";

    console.log(hogeConst); // hoge1
    console.log(hogeLet);   // hoge2
    console.log(hogeVar);   // hoge3

    hogeConst = "hoge4" // エラー
    hogeLet   = "hoge5" // hoge5
    hogeVar   = "hoge6" // hoge6

    console.log(hogeLet); // hoge5
    console.log(hogeVar); // hoge6
}

console.log(hogeConst); // エラー
console.log(hogeLet);   // エラー
console.log(hogeVar);   // hoge6

まとめ

値の変更 スコープ外からの参照
const 不可 不可
let 不可
var

スコープのところは、上の例で言うと、ifの中で書いたらifの中でしか使えないよってこと

参考文献

ここ見たら全部わかるよ
"巻き上げ"のところは上に書いてないから読んでおいた方がいいかも
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Grammar_and_Types

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

javascript 基礎まとめ②

①の基礎まとめの続き

1.関数
Rubyでいう「メソッド」はJavaScriptでいう「関数」
Ruby だと

def メソッド名
 処理内容
end

これがJavaScriptだと

function 関数名 引数 
 処理内容
}

function hello (){
  console.log("こんにちは!")
}

hello()

になる。

ちなみに、↑の書き方は「関数宣言」、↓が「関数式」

hello =function (){
  console.log("こんにちは!")
}

hello()
/// こんにちは!と表示される///

この2つは読み込む順番が違う。関数式は

hello()
hello =function (){
  console.log("こんにちは!")
}
///エラーになる///

としても先にhello = function()、、、の方を読み込まれるので
この書き方はエラーになる。

ちなみにこの

function (){

は無名関数と言われている。

さらに

function (){}

() =>{}

と書換が可能

書いていくうちに頭は整理されるけど、見返すと

説明わかりづらすぎる。。。

※間違い等あればいただきたいです!

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

Vue.jsとReact.js

私について

・文系
・プログラミング経験1年
・サマーハッカソン優勝
・大手IT企業を目指して就活中

なぜ記事を書くのか?

今まで楽しくてコードはたくさん書いてきたが、ほとんどが動いたら良いやと仕組みやシステムについてあまり考えてこなかった。一流のエンジニアを目指す上で、なんとなくではなく、根拠をもって技術選定ができたりエラーを解決をできるように記事を書いていく。

技術選定の仕方

・自分のやりたい事が実現しやすい
・情報量が多い

プログラミングはあくまでも手段でしかなく、プロダクトの価値を最大化させることこそが目的である。またプログラミング言語は、基礎文法は少し変わるぐらいで大した差はない。言語に縛られるよりも、自分が言語に合わせて勉強していくほうが、プロダクトの価値も開発効率も上がる。

Vue.jsとReact.jsどっちが簡単?

結論から言うと、vue.jsのほうが圧倒的に簡単です。

Vue.jsのメリット

①SFC(Single File Component)
HTML/JavaScript/CSSが区別して管理でき、Web制作上がりの僕のような人にはとっつきやすい。

②Vue CLIが便利
TypeSclipt、ESLint などの plugin の使用などをコマンドラインの指示に従って簡単に導入できる。

③HMR(Hot Module Replacement)
ブラウザ画面の再描画(リロード)すること無しにコードの変更をブラウザに適用してくれるので、手間が減って開発が楽になる。

上記3つの点が、僕のような初学者でも簡単に感覚的にコードを書くことができ、かつ多くのことが容易に実現できる。

では、React.jsのメリットって?

①主にクラスベースで実装するため、大規模開発に適している。

②typescriptとの組み合わせが良い。

最後に

手軽さという面でVue.jsを選定してきましたが、これからtypescriptがもっと流行すること、会社での大規模開発に携わるであろうことから、Reactの開発もしていこうと考える。
最後まで読んでいただき、ありがとうございました。

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

join()メソッドの区切り文字について

join()メソッドを理解する

仕事でjoin()メソッドを使った際に、「配列を文字列に変換できるんだなー」という浅い理解にとどまっていたことを痛感しました。
特に区切り文字について理解不足と感じたので、再度理解を深めるために記載しました。

公式を確認

公式を確認します。
join() メソッド 公式

きちんと読むととてもわかりやすく記載頂いてます。いつもさらっと必要な箇所だけ流し読みしてしまうので、きちんと読む癖をつけたいなと思います。

説明の抜粋

join() メソッドは、配列 (または配列風オブジェクト) の全要素を順に連結した文字列を新たに作成して返します。区切り文字はカンマ、または指定された文字列です。配列にアイテムが一つしかない場合は、区切り文字を使用せずにアイテムが返されます。

const elements = ['Fire', 'Air', 'Water'];

console.log(elements.join());
// expected output: "Fire,Air,Water"

console.log(elements.join(''));
// expected output: "FireAirWater"

console.log(elements.join('-'));
// expected output: "Fire-Air-Water"

再確認

ここで 配列.join();配列.join(''); では挙動が違うことがわかります。

配列.join(); のように、区切り文字を省略した場合
配列の要素はカンマ (",") で区切られます。

配列.join(''); のように、区切り文字を空文字にした場合、
すべての要素の間が区切り文字なしで繋がります。

このように配列を何で区切るか設定するとき、設定なし・空文字では挙動が違います。

まとめ

配列を文字列にする、そのくらいの浅い理解でしたが、区切り文字について理解不足でした。他にも理解不足な箇所もあると思うので、引き続き勉強し続けたいと思います。

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

npmを特定のポート番号で動かしたい

ご無沙汰してます、おおのんです。
一時的に指定したポート番号でnuxtアプリを立ち上げる方法。

ターミナルでnuxtアプリを5000番で動かす。
npm run dev -- --port 5000

はい。

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

お問い合わせフォーム、jsでバリデーション

contact.js
window.addEventListener('DOMContentLoaded', () => {
    // 「送信」ボタンの要素を取得
    const submit = document.querySelector('#contact-submit');

    // エラーメッセージと赤枠の削除
    function reset(input_infomation, error_message){
        const input_info = document.querySelector(input_infomation);
        const err_message = document.querySelector(error_message);
        err_message.textContent ='';
        input_info.classList.remove('input-invalid');
    };

    // 「お名前」入力欄の空欄チェック関数
    function invalitName(input_target, error_target, error_message){

        const name = document.querySelector(input_target);
        const errMsgName = document.querySelector(error_target);

        if(!name.value){
            errMsgName.classList.add('form-invalid');
            errMsgName.textContent = error_message;
            name.focus();
            name.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「ふりがな」入力欄の空欄チェック関数
    function invalitHurigana(input_target, error_target, error_message){

        const hurigana = document.querySelector(input_target);
        const errMsgHurigana = document.querySelector(error_target);

        if(!hurigana.value){
            errMsgHurigana.classList.add('form-invalid');
            errMsgHurigana.textContent = error_message;
            hurigana.focus();
            hurigana.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;

    };

    // 「郵便番号」入力欄の空欄チェック関数
    function invalitPostal(input_target, error_target, error_message){

        const postal = document.querySelector(input_target);
        const errMsgPostal = document.querySelector(error_target);

        if(!postal.value){
            errMsgPostal.classList.add('form-invalid');
            errMsgPostal.textContent = error_message;
            postal.focus();
            postal.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;

    };

    // 「住所」入力欄の空欄チェック関数
    function invalitAddress(input_target, error_target, error_message){

        const address = document.querySelector(input_target);
        const errMsgAddress = document.querySelector(error_target);

        if(!address.value){
            errMsgAddress.classList.add('form-invalid');
            errMsgAddress.textContent = error_message;
            address.focus();
            address.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「電話番号」入力欄の空欄チェック関数
    function invalitTel(input_target, error_target, error_message){

        const tel = document.querySelector(input_target);
        const errMsgTel = document.querySelector(error_target);

        if(!tel.value){
            errMsgTel.classList.add('form-invalid');
            errMsgTel.textContent = error_message;
            tel.focus();
            tel.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「メールアドレス」入力欄の空欄チェック関数    
    function invalitEmail(input_target, error_target, error_message){

        const email = document.querySelector(input_target);
        const errMsgEmail = document.querySelector(error_target);

        if(!email.value){
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = error_message;
            email.focus();
            email.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「会社名」入力欄の空欄チェック関数
    function invalitCompany(input_target, error_target, error_message){

        const company = document.querySelector(input_target);
        const errMsgCompany = document.querySelector(error_target);

        if(!company.value){
            errMsgCompany.classList.add('form-invalid');
            errMsgCompany.textContent = error_message;
            company.focus();
            company.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「お問い合わせ内容」入力欄の空欄チェック関数
    function invalitContent(input_target, error_target, error_message){

        const content = document.querySelector(input_target);
        const errMsgContent = document.querySelector(error_target);

        if(!content.value){
            errMsgContent.classList.add('form-invalid');
            errMsgContent.textContent = error_message;
            content.focus();
            content.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };


    // 「送信」ボタンの要素にクリックイベントを設定する
    submit.addEventListener('click', (e) => {
        // デフォルトアクションをキャンセル
        e.preventDefault();

        reset('#name-js', '#err-msg-name');
        reset('#hurigana-js', '#err-msg-hurigana');
        reset('#postal-js', '#err-msg-postal');
        reset('#address-js', '#err-msg-address');
        reset('#tel-js', '#err-msg-tel');
        reset('#email-js', '#err-msg-email');
        reset('#company-js', '#err-msg-company');
        reset('#content-js', '#err-msg-content');

        const focus = () => document.querySelector('#name-js').focus();

        // 「お名前」入力欄の空欄チェック
        if(invalitName('#name-js', '#err-msg-name', 'お名前が入力されていません')===false){
            return;
        };
        // 「ふりがな」入力欄の空欄チェック
        if(invalitHurigana('#hurigana-js', '#err-msg-hurigana', '入力必須です')===false){
            return;
        };

        // ひらがなチェック
        const hurigana = document.querySelector("#hurigana-js");
        const errMsgHurigana = document.querySelector("#err-msg-hurigana");
        const huriganaCheck = /[^ぁ-んー  ]/u; 
        if(hurigana.value.match(huriganaCheck)){
            errMsgHurigana.classList.add('form-invalid');
            errMsgHurigana.textContent = 'ひらがなで入力してください';
            hurigana.focus();
            hurigana.classList.add('input-invalid');
            return;
        }else{
            errMsgHurigana.textContent ='';
            hurigana.classList.remove('input-invalid');
            hurigana.blur();
        };

        // 「郵便番号」入力欄の空欄チェック
        if(invalitPostal('#postal-js', '#err-msg-postal', '入力必須です')===false){
            return;
        };

        // 郵便番号形式チェック
        const postal = document.querySelector("#postal-js");
        const errMsgPostal = document.querySelector("#err-msg-postal");
        const postalCheck = /([0-9]{7})$/; 
        // const postalCheck = /^\d{7}$/; 
        if(postal.value.match(postalCheck)){
            errMsgPostal.textContent ='';
            postal.classList.remove('input-invalid');
            postal.blur();
        }else{
            errMsgPostal.classList.add('form-invalid');
            errMsgPostal.textContent = '郵便番号の形式が違います';
            postal.focus();
            postal.classList.add('input-invalid');
            return;
        };

        // 「住所」入力欄の空欄チェック
        if(invalitAddress('#address-js', '#err-msg-address', '入力必須です')===false){
            return;
        };
        // 「電話番号」入力欄の空欄チェック
        if(invalitTel('#tel-js', '#err-msg-tel', '入力必須です')===false){
            return;
        };

        //電話番号形式チェック
        const tel = document.querySelector("#tel-js");
        const errMsgTel = document.querySelector("#err-msg-tel");
        const telCheck = /0\d{1,4}\d{1,4}\d{4}/; 
        if(tel.value.match(telCheck)){
            errMsgTel.textContent ='';
            tel.classList.remove('input-invalid');
            tel.blur();
        }else{
            errMsgTel.classList.add('form-invalid');
            errMsgTel.textContent = '電話番号の形式が違います';
            tel.focus();
            tel.classList.add('input-invalid');
            return;
        };

        // 「メールアドレス」入力欄の空欄チェック
        if(invalitEmail('#email-js', '#err-msg-email', '入力必須です')===false){
            return;
        };

        // Email形式チェック
        const email = document.querySelector("#email-js");
        const errMsgEmail = document.querySelector("#err-msg-email");
        const emailCheck = /^[-a-z0-9~#&'*/?`\|!$%^&*_=+}{\'?]+(\.[-a-z0-9~#&'*/?`\|!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)|(docomo\ezweb\softbank)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i; 
        if(email.value.match(emailCheck)){
            errMsgEmail.textContent ='';
            email.classList.remove('input-invalid');
            email.blur();
        }else{
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = 'Emailの形式で入力してください';
            email.focus();
            email.classList.add('input-invalid');
            return;
        };

        // 「会社名」入力欄の空欄チェック
        if(invalitCompany('#company-js', '#err-msg-company', '入力必須です')===false){
            return;
        };
        // 「お問い合わせ内容」入力欄の空欄チェック
        if(invalitContent('#content-js', '#err-msg-content', '入力必須です')===false){
            return;
        };

        document.customerinfo.submit();

    }, false);  
}, false);
contact.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title></title>
  <link href="https://fonts.googleapis.com/css?family=Amatic+SC" rel="stylesheet">
  <link type="text/css" rel="stylesheet" href="./contact.css">
    <script src="https://yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script>
</head>
<body>
<?php 
    // require "header.php";
?>
<main>


  <section class="container container-ornament" id="contact">
    <h2 class="container-title"><span>お問い合わせ</span></h2>
    <div class="container-body">
        <div class="container-required">
        <p class="Required-title"><span class="Required"></span>は入力必須項目になります。</p>
        </div>

      <form action="contact_db_connect.php" class="form form-m h-adr" method="post" name="customerinfo">
      <!-- <form action="" class="form form-m h-adr" method="post" name="customerinfo"> -->
          <table>

              <tr>
                  <th class="th"><span class="Required"></span>お名前</th>
                    <td class="td">
                        <span class="err_msg" id="err-msg-name"><?php if(!empty($err_msg['name'])) echo $err_msg['name']; ?></span>
                        <input class="input input-l" id="name-js" name="name" type="text" placeholder="例)神戸 太郎" value="<?php if(!empty($_POST['name'])) echo $_POST['name']; ?>" >
                    </td>
              </tr>

              <tr>
                  <th class="th"><span class="Required"></span>ふりがな</th>
                    <td class="td">
                        <span class="err_msg" id="err-msg-hurigana"><?php if(!empty($err_msg['kana'])) echo $err_msg['kana']; ?></span>
                        <input class="input input-l" id="hurigana-js" name="kana" type="text" placeholder="例)こうべ たろう" value="<?php if(!empty($_POST['kana'])) echo $_POST['kana']; ?>" >
                    </td>
              </tr>

                <span class="p-country-name" style="display:none;">Japan</span>

              <tr>
                  <th class="th"><span class="Required"></span>郵便番号</th>
                    <td class="td">
                        <span class="err_msg" id="err-msg-postal"><?php if(!empty($err_msg['zip'])) echo $err_msg['zip']; ?></span>
                        <input type="text" class="input input-l p-postal-code" id="postal-js" name="zip" size="8" maxlength="8" placeholder="ハイフン無し" value="<?php if(!empty($_POST['zip'])) echo $_POST['zip']; ?>" >
                    </td>
              </tr>

              <tr>
                  <th class="th"><span class="Required"></span>住所</th>
                    <td class="td">
                        <span class="err_msg" id="err-msg-address"><?php if(!empty($err_msg['addr'])) echo $err_msg['addr']; ?></span>
                        <input type="text" class="input input-l p-region p-locality p-street-address p-extended-address" id="address-js" name="addr" placeholder="住所" value="<?php if(!empty($_POST['addr'])) echo $_POST['addr']; ?>" >
                    </td>
              </tr>

              <tr>
                  <th class="th"><span class="Required"></span>電話番号</th>
                    <td class="td">
                        <span class="err_msg" id="err-msg-tel"><?php if(!empty($err_msg['tel'])) echo $err_msg['tel']; ?></span>
                        <input class="input input-l" id="tel-js" name="tel" type="tel" placeholder="例)09012345678 半角 ハイフンなし" maxlength="13" value="<?php if(!empty($_POST['tel'])) echo $_POST['tel']; ?>" >
                    </td>
              </tr>    

              <tr>
                  <th class="th sp-br"><span class="Required"></span>メール<br>アドレス</th>
                    <td class="td">
                        <span class="err_msg" id="err-msg-email"><?php if(!empty($err_msg['email'])) echo $err_msg['email']; ?></span>
                        <input class="input input-l" id="email-js" name="email" type="email" placeholder="例)example@.com" value="<?php if(!empty($_POST['email'])) echo $_POST['email']; ?>" >
                    </td>
              </tr>    

              <tr>
                  <th class="th"><span class="Required"></span>会社名</th>
                    <td class="td">
                        <span class="err_msg" id="err-msg-company"><?php if(!empty($err_msg['company'])) echo $err_msg['company']; ?></span>
                        <input type="text" class="input input-l" id="company-js" name="company" placeholder="例)〇〇〇〇株式会社" value="<?php if(!empty($_POST['company'])) echo $_POST['company']; ?>" >
                    </td>
              </tr>
                            <tr>
                  <th class="th">部署名</th>
                    <td class="td">
                        <input type="text" class="input input-l" name="department" placeholder=""  value="<?php if(!empty($_POST['company'])) echo $_POST['company']; ?>" >
                    </td>
              </tr>

              <tr>
                  <th class="th"><span class="Required"></span>お問い合わせ内容</th>
                    <td class="td">
                        <span class="err_msg" id="err-msg-content"><?php if(!empty($err_msg['text'])) echo $err_msg['text']; ?></span>
                        <textarea class="input input-l input-textarea mb-xxl" id="content-js" name="text" placeholder="お問い合わせ内容" value="<?php if(!empty($_POST['text'])) echo $_POST['name']; ?>" ></textarea>
                    </td>
              </tr>     

          </table>

        <button class="btn btn-corp btn-l" id="contact-submit">送信</button>
      </form>


    </div>
  </section>
</main>

<footer class="footer">
</footer>

<script
  src="https://code.jquery.com/jquery-3.3.1.min.js"
  integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
  crossorigin="anonymous"></script>
<script src="./app.js"></script>
<script src="./contact.js"></script>
</body>

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

Auth0のJavaScriptチュートリアルをシンプルな構成で試してみた

Auth0はドキュメントやチュートリアルが充実していますが、それを参考にしつつシンプルな構成でお試ししてみました。

Auth0について

下記に概要をまとめていますので、ご参考ください。

Auth0の機能を調べてみた - Qiita
https://qiita.com/kai_kou/items/b3dd64e4f8f53325020e

Auth0(オースゼロ)とはAuth0, Inc.が提供するiDaaS(アイダース)で、Identity as a Serviceの略となりクラウドでID管理・認証機能などを提供してくれるサービスです。
iDaaSが提供する機能としては認証・シングルサインオン、ID管理・連携、認可、ログなどによる監査などがあり、Auth0もそれらを提供しています。

アカウントを作成する

Auth0には無償プランがありますので、それを利用します。
image.png
Pricing - Auth0
https://auth0.com/pricing/

Auth0のサイトの右上にある「サインアップ」からサインアップ画面へ移動します。
image.png
Auth0: Secure access for everyone. But not just anyone.
https://auth0.com/jp/

メールアドレス、GitHub、Google、Microsoftアカウントを利用してサインアップできます。
image.png

サインアップできるとAuth0の設定に進みます。専用のドメインとリージョンが指定できます。サブドメイン部分は任意で変更可能です。
image.png

次に法人・個人利用か選択してサインアップ後の設定は完了となります。
image.png

アプリケーション設定

アカウントが作成できたら次にAuth0で認証ができるようにAuth0のApplicationを設定します。

Auth0のダッシュボードが表示されますので、左メニューから「Applications」を選択してApplications画面へ移動します。
image.png

Default App というApplicationがすでに用意されていますので、それを利用します。
image.png

Default App を選択するとApplicationの設定が確認できます。
image.png

今回試用するのに必要となる設定は以下となります。

  • Application Properties
    • Application Type: Single Page Application
  • Application URIs
    • Allowed Callback URLs: http://localhost:3000
    • Allowed Logout URLs: http://localhost:3000
    • Allowed Web Origins: http://localhost:3000

image.png

image.png

設定できたら画面下部にある「SAVE CHANGES」ボタンで設定を保存します。
image.png

他の設定をみてみると最初からGoogleアカウントを利用したログイン設定がすでにされていました。
image.png

クライアント実装

クライアントの実装をするのに、Applicationの「Quick Start」からサンプルを確認することができます。今回はJavaScriptのサンプルを利用します。
image.png

チュートリアルがとても丁寧でわかりやすいので本来はチュートリアルどおりに進めるのがよいと思います。実際に動作するコードはダウンロードまたはGitHubから取得することもできます。
image.png

構成

今回はサンプルを参考にして下記のような実装をしました。CSSは少なかったのでindex.html にまとめています。
Auth0のSDKはhttps://cdn.auth0.com/ で提供されているのでHTMLのscript タグで読み込めば利用できます。

  • index.html
  • js
    • app.js
  • auth_config.json
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>SPA SDK Sample</title>
    <style>
      .hidden {
        display: none;
      }
      label {
        margin-bottom: 10px;
        display: block;
      }
    </style>
  </head>

  <body>
    <h2>SPA Authentication Sample</h2>
    <p>Welcome to our page!</p>
    <button id="btn-login" disabled="true" onclick="login()">Log in</button>
    <button id="btn-logout" disabled="true" onclick="logout()">Log out</button>

    <div class="hidden" id="gated-content">
      <p>
        You're seeing this content because you're currently
        <strong>logged in</strong>.
      </p>
      <label>
        Access token:
        <pre id="ipt-access-token"></pre>
      </label>
      <label>
        User profile:
        <pre id="ipt-user-profile"></pre>
      </label>
    </div>

    <script src="https://cdn.auth0.com/js/auth0-spa-js/1.9/auth0-spa-js.production.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>

Auth0の機能はSDKに含まれるcreateAuth0Clientauth_config.json のパラメータを渡して実行することで利用できるようになります。今回利用しているメソッドは以下となります。

  • createAuth0Client: 初期化や再認証など
  • auth0.isAuthenticated: 認証済みか
  • auth0.getTokenSilently: トークン取得
  • auth0.getUser: ログインしているユーザーの情報取得
  • auth0.handleRedirectCallback: Auth0からの認証結果確認、トークン保存、セッション設定
  • auth0.loginWithRedirect: Auth0のログイン画面へ移動
  • auth0.logout: ログアウト実行
js/app.js
let auth0 = null;
const fetchAuthConfig = () => fetch("/auth_config.json");

const configureClient = async () => {
  const response = await fetchAuthConfig();
  const config = await response.json();

  auth0 = await createAuth0Client({
    domain: config.domain,
    client_id: config.clientId
  });
};

const updateUI = async () => {
  const isAuthenticated = await auth0.isAuthenticated();

  document.getElementById("btn-logout").disabled = !isAuthenticated;
  document.getElementById("btn-login").disabled = isAuthenticated;

  if (isAuthenticated) {
    document.getElementById("gated-content").classList.remove("hidden");
    document.getElementById(
      "ipt-access-token"
    ).innerHTML = await auth0.getTokenSilently();
    document.getElementById("ipt-user-profile").textContent = JSON.stringify(
      await auth0.getUser()
    );
  } else {
    document.getElementById("gated-content").classList.add("hidden");
  }
};

window.onload = async () => {
  await configureClient();
  updateUI();

  const isAuthenticated = await auth0.isAuthenticated();

  if (isAuthenticated) {
    return;
  }

  const query = window.location.search;
  console.log(query);
  if (query.includes("code=") && query.includes("state=")) {
    await auth0.handleRedirectCallback();

    updateUI();
    window.history.replaceState({}, document.title, "/");
  }
};

const login = async () => {
  await auth0.loginWithRedirect({
    redirect_uri: window.location.origin
  });
};

const logout = () => {
  auth0.logout({
    returnTo: window.location.origin
  });
};

こちらはAuth0のドメインやClient IDを保持するファイルになりますので、それぞれで内容を変更する必要があります。

auth_config.json
{
  "domain": "xxxxx.us.auth0.com",
  "clientId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

サーバ起動

Auth0のサンプルだとサーバはnode.jsを利用していましたがもっとシンプルにPythonでWebサーバを起動します。Pythonのバージョンは3前提です。

参考) Pythonの標準ライブラリでさくっとAPIサーバとWebサーバを立ち上げる - Qiita
https://qiita.com/kai_kou/items/6cf5930330b85fa583b0

> cd index.htmlがあるディレクトリ
> python -m http.server 3000
Serving HTTP on :: port 3000 (http://[::]:3000/) ...

Webサーバが立ち上がったらhttp://localhost:3000 へアクセスします。
image.png

動作確認

ブラウザに表示されたページでログイン、ログアウトを試してみます。
「Log in」ボタンをクリックすると、auth_config.json で指定したAuth0のドメインへ遷移します。
「Sign in with Google」でGoogleアカウントを利用したサインアップをします。
image.png

Googleアカウントから戻ってくるとAuth0側でGoogleから提供されるプロファイルの利用可否を確認されますので、「チェック」ボタンをクリックします。
image.png

ログインが完了すると、http://localhost:3000 へコールバックで戻ってきてトークンやアカウント情報が取得できたのが確認できます。「Log out」ボタンをクリックするとログアウトされます。
image.png

Auth0のユーザ設定をみるとユーザー登録されたことが確認できます。
スクリーンショット_2020-11-20_15_02_07.png

Auth0の設定を最低限にしていますので、このままだと、ログイン後、ブラウザのリロードをしてもログイン状態が保持されなかったりしますが、Auth0側で設定変更すれば対応可能となります。

まとめ

実装はシンプルですが、Auth0を利用した認証ができるようになりました。
この実装のまま外部向けのサービスやサイトを公開するのはセキュリティ上あまりよろしくありませんが、Auth0の調査目的として利用するのであれば、Auth0側の設定を変更しつつ挙動を確認してAuth0の仕組みを把握するのには十分利用できそうです。

参考

Auth0の機能を調べてみた - Qiita
https://qiita.com/kai_kou/items/b3dd64e4f8f53325020e

Pricing - Auth0
https://auth0.com/pricing/

Auth0: Secure access for everyone. But not just anyone.
https://auth0.com/jp/

Pythonの標準ライブラリでさくっとAPIサーバとWebサーバを立ち上げる - Qiita
https://qiita.com/kai_kou/items/6cf5930330b85fa583b0

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

React hooksを基礎から理解する (useRef編)

React hooksとは

React 16.8 で追加された新機能です。
クラスを書かなくても、 stateなどのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。

useRefとは

関数コンポーネントでは、Classコンポーネント時のref属性の代わりに、useRefを使って要素への参照を行います。
またuseRefでは、useStateのようにコンポーネント内での値を保持することが出来ます。

構文

const refObject = useRef(initialValue)

//例
const number = useRef(100);
console.log(number.current); // 100

useRefは、.currentプロパティが渡された引数(初期値はinitialValue)をrefObjectへ返します。
この引数の値が書き換え可能な.currentプロパティーの値であり、 .currentプロパティ内に保持することができます。

DOMを参照したい場合

const inputElement = useRef(null)

//例
<input ref={inputElement} type="text" />
console.log(inputElement); // current: null
DOMの参照例

useRefでrefオブジェクトを作成したものをref属性(HTML要素)に指定してDOMを参照しています。

const App = () => {
  const inputEl = useRef(null);
  const handleClick = () => {
    inputEl.current.focus();
    console.log("inputEl.current:", inputEl.current);
    //inputEl.current: <input type="text">
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={handleClick}>入力エリアをフォーカスする</button>
    </>
  );
};

ボタンクリックで<input type="text">がfocusされました。

参照:React公式サイト

useRefとuseStateをくらべてみる

useRefを使ってDOMを参照

useRefを利用するとtextのstate更新時にのみコンポーネントの再描画が発生します。

const App = () => {
  const inputEl = useRef(null);
  const [text, setText] = useState("");
  const handleClick = () => {
    setText(inputEl.current.value);
  };
  console.log("描画!!");
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={handleClick}>set text</button>
      <p>テキスト : {text}</p>
    </>
  );
};

useStateを使ってDOMを参照

入力中の文字列をステートinputElementに格納、ボタンが押された時にsetText(inputElement)textstateへ代入することでuseRefを使った時と同じ挙動にしています。この場合、textinputElementのstate更新時の両方でコンポーネントの再描画が発生しています。

const App = () => {
  const [inputElement, setInputElement] = useState("");
  const [text, setText] = useState("");
  const handleClick = () => {
    setText(inputElement);
  };
  console.log("描画!!");
  return (
    <>
      <input
        value={inputElement}
        onChange={(e) => setInputElement(e.target.value)}
        type="text"
      />
      <button onClick={handleClick}>setText</button>
      <p>テキスト : {text}</p>
    </>
  );
};

useStateを利用している場合はstateの変更される度にコンポーネントの再描画が発生しますが、useRefは値が変更になっても、コンポーネントの再描画が発生しません。useRefを利用するかどうかの判断基準のひとつは、コンポーネントの再描画が発生するかどうかで良さそうです。

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

スクロールして画面に表示されたらイベントが発生する

やりたいこと

スクロールして要素が画面に表示されると同時にイベントを発生させたい。

stylesheet.scss
$(window).scroll(function () {
  $(".slide").each(function (index, el) {
    let scroll = $(window).scrollTop();
    let offset = $(el).offset().top;
    let winH = $(window).height();
    if (scroll > offset - winH / 2) {
      $(el).addClass("in");
    }
  });
});

表示させたい要素のクラス名が「slide」で、複数の要素に当てたいので、「each」を使って繰り返し処理をかけています。

とりあえず画面中央まで来たらイベントが発生するように記述していますが、winH/2の2の部分を、小さい数字に変えていくと、早くイベントを発生させることができますし、大きい数字に変えると遅くイベントを発生させることができます。

結論

elが何者かよくわかっていないのでこれからも勉強します。。
thisのようなものだとは聞いたがイマイチ…

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

【JavaScript】クリックが要素の内側か外側か判断するプログラムを作った【addEventListener】

はじめに

addEventListenerのコールバック関数へ引数を渡す方法はいくつかありますが、
引数を渡せるようにしたコールバック関数をとるイベントリスナを、
removeEventListenerで解除するのは、結構苦労するものです。

この記事は
- addEventListenerのコールバック関数に引数を渡し
- なおかつ任意のタイミングでremoveEventListenerを可能とし、
- ひとつのプログラム上でそれらをどう実現するか
を実践してみた結果と道のりを記した記事となります
(私の以前の記事の実践編となります)

※ES6以前のJavaScriptを使用しています

目指したもの

今回目指すものはこちらです
gifmovie1.gif
※赤いサークルが現れているときにクリックしています

動画のとおり、
クリックしたら入力フォームが現れて、
入力フォームの内側をクリックしている限りはフォームはそのままですが
入力フォーム以外の何でもクリックしたらフォームが閉じる
そんなプログラムを制作します。

出来上がったもの

出来上がったものの挙動はこの通りです。

gifmovie2.gif

以下、制作過程になります

ひな形

今回作成するもののひな形のHTMLとCSSです。


See the Pen
JjKgLgY
by Kabooley (@kabooley)
on CodePen.


クリックイベントに一度だけ発火するリスナ

pic1.PNG
ここの"+ Add new card..."のボックスをクリックしたら、
フォームが登場するようにし、
フォームが登場している間は、
アプリケーション全体にclickイベントを検知できるようにしていきます。

1. フォームにclickイベントリスナを設置する

まずフォームをクリックされたら検知できるようにします。
このクリック検知は一回限りにしたいので、
その場で解除します。

/****************************************************
 * @param: 
 *      formElement はフォーム要素、div.form-partのこと
 *   
 *  div.form-partがクリックされたらコンソールに'click!'と出力し
 *  以降は発火しません。
 * 
 * */
var addEventListenerToForm = function(formElement) {

    formElement.addEventListener('click', (function(){
        return function callback(event) {
            console.log('click!');

            // 例)
            // 引数、Eventオブジェクト両方とることができる
            someFunctionRequiresArgument(event, target);

            formElement.removeEventListener('click', callback, false);
        }
    })(), false);
}

出力結果
gifmovie3.gif

即時関数をaddEventListenerのコールバック関数として渡し、
即時関数に内部関数をこさえてクロージャとします。

クロージャ内部で呼び出す関数はaddEventListener
コールバック関数としての責務を負わないので
その引数を自由にセットできます。

ここではformElement.addEventListener()のコールバック関数は、
callbackになるので、
その解除にはcallbackが必須になります。

内部関数内でremoveEventListenerを実行することで、
一度きりの発火を実現できます。

2. HTML全体でclickイベント検知できるようにする

次に、フォームが登場している限り、
HTML全体でclickイベントを検知できるようにします。
先ほどと同様に即時関数とクロージャの組み合わせを利用します。

/*********************************************
 * @param: 
 *      target is 'div.form-part' element.
 * 
 * */
var addEventListenerToDocument = function(target) {
    document.addEventListener('click', (function(){
        return function callback(event) {
            console.log('click-event has detected!');
            console.log(event);
            document.removeEventListener('click', callback, false);
        }
    })(), false);
}


// div.form-partにイベントリスナをセットする関数
var addEventListenerToForm = function(formElement) {
    console.log('f set addEventListener to forms');

    formElement.addEventListener('click', (function(){
        return function callback(event) {
            // 
            // addEventListenerをセットする関数を呼び出す
            // 
            addEventListenerToDocument(formElement);

            formElement.removeEventListener('click', callback, false);
        }
    })(), false);
}

これでどこをクリックしてもこのイベントリスナが発火します。

click-event has detected!
MouseEvent{}

フォームが現れている間は、
フォーム内部をクリックしている限りはフォームはそのままで、
フォーム以外のどれをクリックしてもフォームを閉じるようにしたいので
フォームのどこをクリックされたのか把握できるようにします。

これにはMouseEventオブジェクト内部にpathというプロパティがあり、
そこにはclickされた要素からhtmlの間に存在する
直系の親要素たちが含まれます
(すごく雑な説明)

となるとフォーム要素であるdiv.form-partが含まれている限り、
そのクリックは必ずフォーム内部をクリックしたと判定できます。

var addEventListenerToDocument = function(target) {
    document.addEventListener('click', (function(){
        return function callback(event) {

            // 内か外か判定する
            if(event.path.includes(target)) {
                console.log('inside');
                return;
            }
            else {
                console.log('outside');
                document.removeEventListener('click', callback, false);
            }
        }
    })(), false);
}

出力結果
gifmovie4.gif
これで内側か外側か判断できるようになりました。
あとは入力フォームの処理を追加していくだけです。
(この記事では省きます)

スコープ外でイベントリスナを解除する

スコープ外の何が問題なのか

さて、クリックされたら現れるフォームへ、実際に入力して
Today's Taskに新たなカードを追加できるようになったとします。
gifmovie5.gif
たとえばこのフォームだと、
外側をクリックするイベントと閉じるボタンをクリックするイベントが発火したら、
どちらも「フォームを閉じる」という共通の処理を行います。

共通の処理は次の通りとなります
- フォームを閉じる
- documentのclickイベントリスナを解除する
- もう一度フォームにclickイベントリスナをセットする

外側をクリックされたときに実行される関数と、
閉じるボタンをクリックされたときに実行される関数は
お互いスコープ外です
(内部で定義されたものに関して、という意味です)

なので、
document.removeEventListenerで解除しようにも、
解除に必須なcallbackにアクセスできないため解除できない問題が生じます。

/********************************************
 * @param: 
 *      parent is 'div.form-part' element. 
 * 
 * 
 * */
var closeBtnClickHandler = function(parent) {
    var close = document.querySelector('button.close');
    close.addEventListener('click', function(event){

        // フォームを閉じるための共通の処理を実行
        // - フォームを閉じる
        // - documentのclickイベントリスナを解除する
        // - もう一度フォームにclickイベントリスナをセットする
        // 
        // 問題:callbackがスコープ外だから解除できない!
        document.removeEventListener('click', callback, false);     // callbackが不明なので無視される
    }, false);
}


var addEventListenerToDocument = function(target) {
    document.addEventListener('click', (function(){
        return function callback(event) {

            if(event.path.includes(target)) {
                console.log('inside');
                return;
            }
            else {
                console.log('outside');
                // 
                // 閉じるボタン処理関数の呼出し
                // 
                closeBtnClickHandler(target);
                // フォームを閉じるための共通の処理を実行
                // - フォームを閉じる
                // - documentのclickイベントリスナを解除する
                // - もう一度フォームにclickイベントリスナをセットする
            }
        }
    })(), false);
}

引数としてコールバック関数を外部へ渡す

解決方法はとっても簡単で、
callbackcloseBtnClickHandlerへ引数として渡せばいいだけでした

/********************************************
 * @param: 
 *      parent is 'div.form-part' element. 
 * 
 * 
 * */
var closeBtnClickHandler = function(parent, callback) {
    var close = document.querySelector('button.close');
    close.addEventListener('click', function(event){

        // フォームを閉じるための共通の処理を実行可能
        // 解除できる!
        document.removeEventListener('click', callback, false);
    }, false);
}


var addEventListenerToDocument = function(target) {
    document.addEventListener('click', (function(){
        return function callback(event) {

            if(event.path.includes(target)) {
                console.log('inside');
                return;
            }
            else {
                console.log('outside');
                // 
                // 閉じるボタン処理関数の呼出し
                // 
                closeBtnClickHandler(target, callback);
                // フォームを閉じるための共通の処理を実行可能
            }
        }
    })(), false);
}

今回は短い簡単なプログラムなのでこれでも問題ないですが、
ネストが深いところの関数にcallbackを送らなくてはならない場合、
ただそれを実現するために間の関数でバケツリレーをしなくてはならなくなり、
非常に無駄が多いです。

なのでこれを回避するために工夫を施します。

バケツリレー回避案

ということで筆者の少ない頭を絞って出てきたのは
両関数の共通のスコープに
document.removeEventListenerが実現可能なクロージャを用意する案を
思いつきました。

var canceler;

/************************************************
 * @param:
 *      documentELCallback is callback function for document.addEventListener('click')
 * 
 *      closingFormListElement is div.form-part
 * 
*/
var closeFormHandler = function(documentELCallback) {
    return function closeForm (closingFormListElement) {

        // フォームを閉じる関数
        toggleForm(false, closingFormListElement);
        // documentクリックイベント検知の解除
        document.removeEventListener('click', documentELCallback, false);
        // フォームのクリックイベントリスナの再設置
        addEventListenerToForm(closingFormListElement);
    }
}



var closeBtnClickHandler = function(parent) {
    var close = document.querySelector('button.close');
    close.addEventListener('click', function(event){

        // フォームを閉じる処理実行
        canceler(parent);
    }, false);
}


var addEventListenerToDocument = function(target) {
    document.addEventListener('click', (function(){
        return function callback(event) {

            // クロージャのインスタンスをここで作成
            canceler = closeFormHandler(callback);

            if(event.path.includes(target)) {
                console.log('inside');
                return;
            }
            else {
                console.log('outside');
                closeBtnClickHandler(target, callback);
                canceler(target);
            }
        }
    })(), false);
}

完成品

See the Pen JjKgZPO by Kabooley (@kabooley) on CodePen.

さいごに

以上で、引数を渡すことが可能・あとから解除可能・どこでも解除可能を(なんとか)実現したプログラムができました。

目からうろこな解決策は思いつきませんでしたが、
個人的には引数でコールバック関数を渡せばいいという
(至極当たり前な)解決策を身に着けることができてよかったとは思っています。

こちらの記事がご覧になってくださった方へ、少しでも何かの役に立っていただけたら幸いです。
というのは定型句ですが、
そこ間違っているよ、もっといい方法があるよなどの
先達の皆様からのご指摘をどしどしいただきたいので、
ぜひコメントなり何なりでご指摘くださいましたら本当に助かります。
よろしくお願いします。

ご覧いただきありがとうございました。

補足

  • こちらの記事はJavaScript初心者がその学習過程をアウトプットした記事となります。
  • innerHTML使っているけどここでは正規表現などを用いた入力内容の検査はしてません。
  • 関数名のセンスのなさ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ドラッグで移動できるdialogタグをざっくりと作る

<dialog>タグとは

<dialog>: ダイアログ要素 - MDN web docs
https://developer.mozilla.org/ja/docs/Web/HTML/Element/dialog

まともに動くのはChromeとChromeベースのEdgeだけですね。
使いどころが難しいですが、社内システムとかで使う分には良いかなーって感じですね。

まずはダイアログを表示するコード

<dialog id="dialog">
  <h1>hello</h1>
</dialog>
<button id="show">show</button>

<script>
const showBtn = document.querySelector('#show');
const dlg = document.querySelector('#dialog');

showBtn.addEventListener('click', () => {
  dlg.showModal()
});
</script>

dialog要素はデフォルト非表示になります。
画面にはshowボタンしか表示されてませんね。

FireShot Capture 020 - Document - wsl.main.com.png

FireShot Capture 023 - Document - wsl.main.com.png

dialog要素はjsと共に使用され、dialog要素.showModal()で表示できます。

モーダルの枠線とかはデフォルトです。
モーダルの背景が若干灰色になってますね。
次はスタイルをいじってみましょう。

スタイルをいじる

<style>
/* モーダル */
dialog {
  border: none;
  border-radius: 10px;
}

/* モーダルの背景 */
dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
}
</style>

FireShot Capture 026 - Document - wsl.main.com.png

枠線を消して、角丸を入れてみました。
dialog::backdropはモーダルの背景の疑似要素です。半透明の黒を設定してみました。

ドラッグできるようにする

<style>
/* モーダル */
dialog {
  border: none;
  border-radius: 10px;
}

/* モーダルの背景 */
dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
}

/* モーダル内のテキストとかを選択不可にする */
dialog * {
  user-select: none;
}
</style>
<dialog id="dialog" draggable="true">
  <h1>hello</h1>
</dialog>
<button id="show">show</button>
<script>
const showBtn = document.querySelector('#show');
const dlg = document.querySelector('#dialog');

showBtn.addEventListener('click', () => {
  dlg.showModal()
});

/**
 * モーダルのどこをつかんで移動を開始したか保存する用
 */
let mouse = {
  x: 0,
  y: 0,
};

/**
 * モーダルのどこをつかんで移動を開始したか保存する
 * ドラッグ時についてくる半透明の要素を空のdivにして消す
 */
dlg.addEventListener('dragstart', evt => {
  mouse.y = dlg.offsetTop-evt.pageY;
  mouse.x = dlg.offsetLeft-evt.pageX;
  evt.dataTransfer.setDragImage(document.createElement('div'), 0, 0);
});

/**
 * ドラッグ時の座標を
 */
dlg.addEventListener('drag', evt => {
  if (evt.x === 0 && evt.y === 0) return;
  const top = evt.pageY + mouse.y;
  const left = evt.pageX + mouse.x;
  const right = window.outerWidth - evt.pageX;
  dlg.style.top = top + 'px';
  dlg.style.left = left + 'px';
  dlg.style.right = right + 'px';
});

/**
 * モーダルのどこをつかんで移動を開始したかをリセットする
 */
dlg.addEventListener('dragend', evt => {
  mouse = {
    x: 0,
    y: 0,
  };
});
</script>

FireShot Capture 029 - Document - wsl.main.com.png

こんな感じで移動できます。
ドラッグ開始したマウスの位置を設定しないと、マウスの位置がモーダルの左上となります。

また、dialog要素はデフォルトで

dialog {
  left: 0;
  right: 0;
  position: absolute;
  ...他
}

が入ってるのでleftとrightの両方を設定します。

evt.dataTransfer.setDragImage(document.createElement('div'), 0, 0);は無くても良いですが、半透明の要素がくっついてきます。

おしまい

ライブラリも何もなしにモーダル表示できるのは楽ですね。
ちなみにdialog要素form要素を内包して使うことが多いです。

<dialog>
  <form method="dialog">
    <button value="ok">OK</button>
  </form>
</dialog>
<script>
document.querySelector('dialog').addEventListener('close', () => {
  console.log(
    document.querySelector('dialog').returnValue
  );
});
</script>

method="dialog"を設定してsubmitするとモーダルが非表示になります。
OKのボタンが押されるとokという文字列をjsで受け取ることもできます。

皆さんも使ってみてはいかがでしょうか?

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

UserScriptでの@includeで詰まったので

atcoder関連のUserScriptを書いていて、

https:// atcoder.jp/users のみでスクリプトを実行したいが
https:// atcoder.jp/users/kemkemG0?graph=rank や、
https:// atcoder.jp/users/kemkemG0/history 等では実行してほしくない ということがあった。

シンプルに

// @include      *://atcoder.jp/users*
// @exclude      *://atcoder.jp/users/*?graph=rank
// @exclude      *://atcoder.jp/users/*/history*

でよかった。

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