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

[JavaScript]入力フォーム漏れがあった時のアラート表示方法

はじめに

RailsでECサイトを作成しています。
カートに商品を追加する時に、数量選択がなかった場合に以下のようなポップアップを表示させたいと思いました。今回はその備忘録です。
スクリーンショット 2020-05-29 23.12.14.png

補足情報

  • Ruby 2.5.1
  • Ruby on Rails 5.2.4.2

フォームの内容

上記写真のページは以下のようなViewファイルになっています。

.showMain
  .showMain__Boxes
    .showMain__Boxes__leftBox
      = image_tag @product.image, width: 450, height: 450, class: "showMain__Boxes__leftBox__image"
    .showMain__Boxes__rightBox
      = form_with url: add_item_carts_path, local: true, name: "formForCart" do |f|
        .showMain__Boxes__rightBox__name
          = @product.name
        .showMain__Boxes__rightBox__namejap
          = @product.namejap
        .showMain__Boxes__rightBox__description
          = @product.description
        .showMain__Boxes__rightBox__orderQuantity
          = f.label :quantity, "数量", class: "showMain__Boxes__rightBox__orderQuantity__title"
          = f.select :quantity, stock_array_maker(@stock), {include_blank: "選択してください"}, {class: "showMain__Boxes__rightBox__orderQuantity__form"}
        .showMain__Boxes__rightBox__line
          .showMain__Boxes__rightBox__line__price
            = "本体価格 : ¥#{convertToJPY(@product.price)}"
          .showMain__Boxes__rightBox__line__fav
            - if user_signed_in? && current_user
              - if @product.bookmark_by?(current_user)
                = link_to product_bookmark_path(@product.id), class: "showMain__Boxes__rightBox__line__fav__btn.fav", method: :delete, remote: true do
                  %i.fas.fa-star.star
              - else
                = link_to product_bookmarks_path(@product.id), class: "showMain__Boxes__rightBox__line__fav__btn.fav", method: :post, remote: true do
                  %i.far.fa-star.star-o (お気に入りに追加)
        .showMain__Boxes__rightBox__line
        .showMain__Boxes__rightBox__addCart
          = f.hidden_field :product_id, value: @product.id
          = f.hidden_field :cart_id, value: @cart.id
          = f.submit "Add to Cart", {class: "showMain__Boxes__rightBox__addCart__btn", id: "addToCart"}

JSの記載方法

submitタグ、quantityのselectタグの2つのノードを取得しています。

$(function(){
  $("#addToCart").on('click', function(e){
    if(document.getElementById('quantity').value === ""){
      alert("数量を選択してください。");
      return false;
    }else{
      return true;
    }
  })
});

フォームのsubmitボタンが押された時に発火するように設定をして、= f.select :quantity ~ のidのvalueがない場合にfalseを返し、アラートで警告します。
返り値がtrueならサブミットし、falseならサブミットしません。
因みに、"return false"がないと、ポップアップが出ますが、次のページに進んでしまいます。

上記JSは以下の方法でもOK

$(function(){
  $("#addToCart").on('click', function(e){
    if(!(document.getElementById('quantity').value)){
      alert("数量を選択してください。");
      return false;
    }else{
      return true;
    }
  })
});

参考サイト

JavaScriptでフォームの入力チェックをする方法
【JavaScript】 空文字のチェック方法【勉強中】

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

[JavaScript]JavaScriptでFizzBuzz!(復習編)

目次

  • はじめに
  • サンプルコード
  • サンプル画像と使用例
  • おわりに
  • 参考にしたサイト

はじめに

拙著の記事にも書いたが、FizzBuzzをできないようでは情報技術者失格らしい。この頃、それを実感する出来事があった。そのため、自分が経験してきた言語でFizzBuzzを復習していく。
今回は、JavaScriptでFizzBuzzを解き、HTMLとCSSを用いて画面上にその結果を表示する。

サンプルコード

〇HTML + JavaScript

<html>

<head>
    <title>Let's FizzBuzz</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.4/css/all.css">
    <link rel="stylesheet" type="text/css" href="FizzBuzz_var1.01.css">
    <script>
        function btnFizzBuzz()
        {
            var num1 = document.forms.FizzBuzz.numFizzBuzz1.value
            var num2 = document.forms.FizzBuzz.numFizzBuzz2.value
            var arrFizzBuzz = [];
            // 空白チェック
            if (num1 === "" || num2 === "")
            {
                alert('数値①と数値②には数値を入力してください。');
            } else
            {
                for (var i = num1;i <= num2;i++)
                {
                    if ((i % 3 === 0) && (i % 5 === 0))
                    { //3かつ5の倍数
                        arrFizzBuzz.push('FizzBuzz');

                    } else if (i % 3 === 0)
                    { //それ以外で3の倍数
                        arrFizzBuzz.push('Fizz');

                    } else if (i % 5 === 0)
                    { //それ以外で5の倍数
                        arrFizzBuzz.push('Buzz');

                    } else
                    {
                        arrFizzBuzz.push(i);
                    }
                }
                alert(arrFizzBuzz);
            }
        }
    </script>
</head>

<body>
    <h1>Let's FizzBuzz</h1>
    <table>
        <tr>
            <th>
                <div class="boxTrivia1">
                    <label for="btnTrivia1">そもそもFizzBuzzとは?</label>
                    <input type="checkbox" id="btnTrivia1">
                    <ul class="text">
                        <li>英語圏で長距離ドライブ中や飲み会の時に行われる言葉遊び…らしい</li>
                        <li>そして、このゲームを画面に表示させるプログラムとして作成させることで、<br>
                            コードが書けないプログラマ志願者を見分ける手法を、<br>
                            Jeff AtwoodがFizzBuzz問題 (FizzBuzzQuestion)として提唱した。<br>
                            <s>余計なことをしやがって</s>
                        </li>
                    </ul>
                </div>
            </th>
            <th>
                <div class="boxTrivia2">
                    <label for="btnTrivia2">FizzBuzzが出来ないと…?</label>
                    <input type="checkbox" id="btnTrivia2">
                    <ul class="text">
                        <li>FizzBuzzが出来ない = 情報技術者(要はプログラマ)失格レベルらしい</li>
                        <li>企業に入社した際の新人研修でこのFizzBuzz問題を課題として出すことも多い</li>
                        <li>また、転職する際の技術試験で出されることもある</li>
                        <li>なので、せめて自分が得意とする言語ではFizzBuzzを空で<br>
                            コーディングできるように、なろう!
                        </li>
                    </ul>
                </div>
            </th>
        </tr>
    </table>
    <h2>数値を入力してね!</h2>
    <form id="FizzBuzz">
        <p>
            数値①(最低値)<input type="number" id="numFizzBuzz1" size="10" maxlength="20">
        </p>
        <p>
            数値②(最高値)<input type="number" id="numFizzBuzz2" size="10" maxlength="20">
        </p>
        <input type="button" class="btnFB" value="FizzBuzz出力" onClick="btnFizzBuzz()">
    </form>
</body>

</html>

〇CSS

body {   
    height:100%;
}
body:after {
    position: fixed; /* fixed:ページがスクロールされても、いつでも同じ場所に配置される */
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    content: "";
    z-index: -1;
    background: linear-gradient(-45deg, rgba(246, 255, 0, 0.678), rgba(212, 26, 144, 0.8)),
    url("お好みの画像を入れてください");
    background-size: contain; /* 縦横比は保持して、背景領域に収まる最大サイズになるように背景画像を拡大縮小する */
}
h1{
    width: 400pt;
    margin: auto;
    border: 1pt solid rgb(0, 0, 0);
    border-radius: 8pt;
    background: linear-gradient(45deg, #f5962a 25%, transparent 25%, transparent 75%, #f5962a 75%),
                linear-gradient(45deg, #a9a9a9 25%, transparent 25%, transparent 75%, #a9a9a9 75%);
    background-color: #f3c220;
    background-size: 40px 40px;
    background-position: 0 0, 20px 20px;
    box-shadow: 0 4pt 8pt 0 rgba(0, 0, 0, 0.2), 0 3pt 10pt 0 rgba(0, 0, 0, 0.19);
    color:rgb(0, 0, 0);
    text-align: center;
    font-family: "Monotype Corsiva"; 
    font-size: 8ex;
}
h2{
    color:rgb(0, 0, 0);
    text-align: center;
    font-family: "Monotype Corsiva"; 
    font-weight: bold;
}
p{
    color:rgb(0, 0, 0);
    text-align: center;
    font-family: "Meiryo UI"; 
    font-weight: bold;
}
/** トリビアボタン **/
.boxTrivia1  label {
    display: inline-block;
    margin: 10px;
    color: #332c10; /* ボタンの文字色 */
    background-color: #ffdb4f; /* ボタンの背景色 */
    font-weight: bold; /* 文字の太さ */
    padding: 0.5em 1em; /* ボタン内側の余白 */
    border-bottom: solid 4px #ccb03f; /* ボタンの影部分 */
    border-radius: 3px; /* 角丸 */
    cursor: pointer; /* ボタンにカーソルを合わせた時に指アイコンを表示 */
}
/** ボタンクリック時のボタンを押し込む動作 **/
.boxTrivia1 label:active {
    -webkit-transform: translateY(4px); /* Chrome、Safari用 */
    -moz-transform: translateY(4px); /* Firefox用 */
    -ms-transform: translateY(4px); /* IE用 */
    transform: translateY(4px);
    border-bottom: none;
}
/** チェックボックス **/
.boxTrivia1 input {
    display: none; /* 非表示 */
}
/** 表示・非表示を切り替えるテキスト **/
.boxTrivia1 .text {
    color: rgb(0, 0, 0); /* 文字色 */
    font-weight: bold; /* 文字の太さ */
    overflow: hidden;
    opacity: 0; /* 文字を非表示 */
    text-align: left;
}
/** チェックボックスにチェックが入った時の、テキストの処理 **/
.boxTrivia1 input:checked ~ .text {
    height: auto;
    opacity: 1; /* 文字を表示 */
}
.boxTrivia2 label {
    display: inline-block;
    margin: 10px;
    color: #332c10;                   
    background-color: #ffdb4f;      
    font-weight: bold;             
    padding: 0.5em 1em;             
    border-bottom: solid 4px #ccb03f; 
    border-radius: 3px;           
    cursor: pointer;               
}
.boxTrivia2 label:active {
    -webkit-transform: translateY(4px);
    -moz-transform: translateY(4px);    
    -ms-transform: translateY(4px); 
    transform: translateY(4px);
    border-bottom: none;
}
.boxTrivia2 input {
    display: none;
}
.boxTrivia2 .text {
    color: rgb(0, 0, 0);  
    font-weight: bold;
    overflow: hidden;
    opacity: 0; 
    text-align: left;
}
.boxTrivia2 input:checked ~ .text {
    height: auto;
    opacity: 1; 
}
.btnFB {
    display: block;
    text-align: center;
    margin: 10px auto;
    cursor: pointer; 
    background: linear-gradient(45deg, #f5962a 25%, transparent 25%, transparent 75%, #f5962a 75%),
                linear-gradient(45deg, #a9a9a9 25%, transparent 25%, transparent 75%, #a9a9a9 75%);
    background-color: #f3c220;
}

サンプル画像と使用例

実際の画面は以下の通りになる。ちょっとオシャンティ(死語)にレイアウトしてみた。
コメント 2020-05-29 221913.png
各ラベルをクリックしたら、以下の文章が表示される。若干見辛い…?
コメント 2020-05-29 222107.png
また、テキストボックスに数値を入力後、ボタンをクリックすると、FizzBuzzの結果がaleartで表示される。
コメント 2020-05-29 222928.png

おわりに

今回を振り返って、JavaScriptでシンプルにFizzBuzzを解いてみた。また、FizzBuzzの結果を画面上で見られるように、alertで表示するようにもこだわってみた。
意外と簡単にできるな、と思ったが、どうやらできるだけ短くコーディングするやり方があるらしい。(Code Golf という、可能な限りもっとも短いソースコードで記述する競技があるとか)調べてみると、以下のソースコードが最短っぽい。

for(i=0;++i<101;console.log(i%5?x||i:x+'Buzz'))x=i%3?'':'Fizz'

わけわかんないですね…。
FizzBuzz問題は奥が深い。だからこそ、プログラマの物差しになっているのだと思う。
次からは、できる限りシンプルにコーディングしてみようと思う。

参考にしたサイト

  • FizzBuzz、JavaScript系

FizzBuzz問題のJavaScript最短コードを解説します
HTMLクイックリファレンス
【JavaScript】 空文字のチェック方法【勉強中】

  • HTML、CSS系

HTMLとCSSだけ!要素の表示・非表示を切り替える方法
CSSのグラデーション(linear-gradient)の使い方を総まとめ!

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

JavaScriptHack Hackフォルダ Form編

JavaScript Hackとは(進捗率 40%) Form編

JavaScriptはDomとFormを押さえれば、実務ではかなりいけるはずという見込みのもと記事を書いていく。
その中でもFormを取り上げる。

きちんと学ぶ事も大事だが、まごまごしている間にいわゆる英語のように単語だけを並べ立てて強引にお客さんに乗り切られ、残念な結果は避けたい。

コンテンツ

・分類する
・少しずつ例を持ってくる。
分類したものは全てカバーするとする。

分類する

・ノンバリデーション
・バリデーション
・記法の違い
・部品による違い

参考アドレスを多少変えて自分でFormを作成する。

・バリデーション

      // Form validation code will come here.
      function validate() {

         if( document.myForm.Name.value == "" ) {
            alert( "Please provide your name!" );
            document.myForm.Name.focus() ;
            return false;
         }
         if( document.myForm.EMail.value == "" ) {
            alert( "Please provide your Email!" );
            document.myForm.EMail.focus() ;
            return false;
         }
         if( document.myForm.Zip.value == "" || isNaN( document.myForm.Zip.value ) ||
            document.myForm.Zip.value.length != 5 ) {

            alert( "Please provide a zip in the format #####." );
            document.myForm.Zip.focus() ;
            return false;
         }
         if( document.myForm.Country.value == "-1" ) {
            alert( "Please provide your country!" );
            return false;
         }
         return( true );
      }
      </script>      
   </head>

   <body>
      <H1>AWS友の会会員登録</H1>
      <form action = "/cgi-bin/test.cgi" name = "myForm" onsubmit = "return(validate());">
         <table cellspacing = "2" cellpadding = "2" border = "1">

            <tr>
               <td align = "right">お名前</td>
               <td><input type = "text" name = "Name" /></td>
            </tr>

            <tr>
               <td align = "right">メールアドレス</td>
               <td><input type = "text" name = "EMail" /></td>
            </tr>

            <tr>
               <td align = "right">コード</td>
               <td><input type = "text" name = "Zip" /></td>
            </tr>

            <tr>
               <td align = "right">出身国</td>
               <td>
                  <select name = "Country">
                     <option value = "-1" selected>[choose yours]</option>
                     <option value = "1">USA</option>
                     <option value = "2">UK</option>
                     <option value = "3">INDIA</option>
                  </select>
               </td>
            </tr>

            <tr>
               <td align = "right"></td>
               <td><input type = "submit" value = "Submit" /></td>
            </tr>

         </table>
      </form>      

参考アドレス
https://www.tutorialspoint.com/javascript/javascript_form_validations.htm

テキストなどから例を持ってくる。

シラバスを作成する。

参考アドレス

参考資料
現代の JavaScript チュートリアル
https://ja.javascript.info/
JavaScriptのsubmitイベントで、フォーム送信をコントロールしよう
https://www.sejuku.net/blog/28720

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

(JavaScript)即時関数の形にモヤモヤしているあなたに

仲間内で行った説明が好評だったので、Qiita用に纏めてみました。

即時関数って?

通常の関数は、あらかじめ定義しておいて後で実行する形をとりますね。対して即時関数は、その名の通り関数定義後にそれを即実行するものです。無名関数でこのメソッドを使用することによって、名前空間の汚染を気にせずに変数を使えるといったメリットがあります。以下のような書き方が主流だと思います。

(function() {
   ...
})();

(function() {
   ...
}());

しかし、あくまで主流です。即時関数の仕組みさえ理解していれば、もっと違う書き方も可能なのです!

まずは、前提となる無名関数の特徴についてから説明します。

無名関数について知っておくべき前提

無名関数は、それ単体だけ書くとエラーを吐いてしまいます。

function() {
    ...
}
//Uncaught SyntaxError: Function statements require a function name

しかし、functionが行頭に無いならばエラーは吐きません(勿論そのままでは「ただそこにあるだけ」で、実行はされません)。

!function(n) {
    ...
}
[function(n) {
    ...
}][0]
0 == function(n) {
    ...
}
(function(n) {
    ...
})

本題:括弧の正体

まずは関数を変数として宣言してみましょう!

var a = function(n) {
    ...
}

次に、宣言した関数を使ってみましょう!

a("引数");

関数を使用する際、皆さんは関数名(引数)という形で使用しますよね。この最後の括弧は、functionというオブジェクトに対しての「実行せよ!あっ、引数も受け取っておいてね!」という命令なんですね。事実、括弧無しで関数名だけポンと書いても、関数は実行されません。

a;
//何も起こらない

じゃあこの理屈をそのままに、変数宣言無しに関数を定義し、即!実行してみましょう。

//これが
var a = function(n) {
    ...
}
a("引数");

//こうなる
function(n) {
    ...
}("引数");

しかし、この記事の冒頭に書いてあった内容を思い出してください。「無名関数の場合、行頭にあるとエラーを吐く」という前提があったはずです。

function(n) {
    ...
}("引数");
//Uncaught SyntaxError: Function statements require a function name

というわけで、この無名関数をエラーの出ない形、つまりfunctionが行頭に無い形に書き換えてしまいましょう。

!function(n) {
    ...
}("引数")
[function(n) {
    ...
}][0]("引数");
0 == function(n) {
    ...
}("引数");
(function(n) {
    ...
})("引数");
(function(n) {
    ...
}("引数"));

見覚えのある形・無い形もあるかもしれませんね。しかしこれらは全て即時関数であり、キチンと機能します。もしここにある例よりも奇想天外かつコンパクトな即時関数をご存知の方は、是非!ご教授ください。

ご清覧ありがとうございました!


おまけ

無名関数でなくとも、関数の定義後即実行は可能です。

function a(n) {
    ...
}("引数");

a("引数2");

無名関数を使用するのは、大抵は名前空間を汚したくないといった理由かと思います。単に「名前空間なんざ知らない」「関数定義した後にひとまず即実行したい」「一度実行した後も別の場所でこの関数を再利用したい」といった方には、このような書き方もおすすめです。

おまけ - 2

関数を変数として宣言する際、名前付き関数を入れても問題はありません(ただしその名前はその関数内でしか使えません)。

var a = function b(n) {
    if(n < 10) {
        return b(n+1);
    }
    return n;
}(0);
//10

b(0);
//Uncaught ReferenceError: b is not defined
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BootstrapVueのダサいイメージコンポーネントをクールなデザインにカスタマイズしてみた

スクリーンショット 2020-05-29 16.25.36.png

Vueバージョン確認

npm list vue

まずは上記コマンドでバージョンの確認

twinzlabo@0.1.0 /Users/twinzlabo

── vue@2.6.11

BootstrapVueの導入

BootstrapVueの導入がまだの方のために念のため導入方法書いときますね

とりあえずコピペして環境を整えてください

main.js
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue)
npm install vue bootstrap-vue bootstrap

以上でBootstrapVueの導入は完了です

BootstrapVueのダサいイメージをクールなデザインにカスタマイズ

すでに上の方で確認してもらったかと思いますが、

BootstrapVueの非常にダサいImageコンポーネントをスタイル修正を行うことでクールなデザインに編集していきましょう
スクリーンショット 2020-05-29 16.13.19.png

デフォルトの上の画像をhoverしたら下の画像のように色がつくようにカスタマイズしていきます
スクリーンショット 2020-05-29 16.13.11.png

この感じなかなかクールですよね

では早速コードをコピペしていきましょう

<template>
  <div>
    <b-img src="https://picsum.photos/300/150/?image=41" fluid alt="Fluid image"></b-img>
  </div>
</template>
<style>
img {
  margin-right: 10px;
  margin-bottom: 30px;
  display: inline-block;
  width: 500px;
  height: 400px;
  background-size: contain;
  background-repeat: no-repeat;
  cursor: pointer;
  transition: all 200ms ease-in;
  filter: grayscale(1) opacity(.8);
}

img:hover {
  filter: grayscale(0) opacity(1);
}
</style>

これだけです

いかがでしたでしょうか?ちゃんと同じようなデザインになりましたか?

今回も超簡単にカスタマイズできましたね

以上です

参考記事(より応用的な実装が希望の方)

【Vueデザイン/コピペだけ】白黒画像一覧でhoverすると色がつくCSSアニメーション実装を徹底解説
←トップ画像のような実装ができます
【Vue/BootstrapVueコピペのみ】Bootstarap導入からシンプルな画像一覧画面の実装方法までを徹底解説

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

【Rails】セレクトボックスを活用した多階層構造データの表示について

経緯

とあるプログラミングスクール受講生です。
存知の方も多いでしょう「フリマクローンサイト」作成にあたり、出品カテゴリーをajaxで実装しましたので、自分の頭の整理を兼ねてまとめていきたいと思います。

なお、カテゴリーの設定は「ancestry」というGemを使用しております。
今回の記事につきましては、カテゴリー設定後を想定しております。

※説明が必要ない方はコードのみ追ってもらえれば実装できると思います。

完成イメージ

category.gif

コード

ビュー(items.new.html.haml)
.form-title
  =f.label "カテゴリ"
  .form-title__required
    %label 必須
.form-input-select
  = f.select :category, @category_parent_array, {}, {class: 'listing-select-wrapper__box--select', id: 'parent_category'}
.listing-product-detail__category
  • 今回、重要なのは下3行のみです。他はご自由に。
  • = f.select :category, @category_parent_array, {}, {class: 'listing-select-wrapper__box--select', id: 'parent_category'} の {} は超重要ですので消さないでください。
    → 軽く触れておきます。{}の部分が2つありますね。一つ目が「オプション」の記載、二つ目(今回記載がある方)が「HTMLオプション」となります。
    「オプション」を設定しない場合は空欄でも{}の記載しておかないと、「HTMLオプション」が反映されません。
コントローラー(items_controller.rb)
  def new
    @category_parent_array = ["---"]
    Category.where(ancestry: nil).each do |parent|
      @category_parent_array << parent.name
    end
  end

  def get_category_children
    @category_children = Category.find_by(name: "#{params[:parent_name]}", ancestry: nil).children
  end

  def get_category_grandchildren
    @category_grandchildren = Category.find("#{params[:child_id]}").children
  end

デモを見てもらうと、カテゴリーが三段階で表示されているのが確認できると思います。
・ 一段目の表示がnew
・ 二段目の表示がget_category_children
・ 三段目の表示がget_category_grandchildren
となっております。

複雑な部分はnewのコードでしょうか。
@category_parent_array に "---" しか入っていない配列を代入していますね。
次の行のeach文で先ほど設定した@category_parent_array配列にカテゴリを1件ずつ代入しています。
(ancestry: nil)につきましては、「ancestry」でカテゴリーを設定した後に保存データを確認してみてください。意味がわかると思います。

二段落目、三段落目の記載はajaxでの処理となりますので、アクションを分けています。
params[:parent_name]、params[:child_id]については、ajaxでJavaScriptから飛んでくる値です。
あとで設定しますので、覚えておきましょう。

routes.rb
 resources :items do
    collection do
      get 'get_category_children', defaults: { format: 'json' }
      get 'get_category_grandchildren', defaults: { format: 'json' }
    end
  end

先ほどコントローラーで出てきた2つのajax用アクションを設定しています。
itemsにネストしております。
軽くcollectionについて簡単すると、routingにidが付くのがmember付かないのがcollectionです。
今回は、「個(id)」を特定する必要がないので、collectionですね。

コントローラーは基本的に処理をビューに返すのですが、
defaults: { format: 'json' }の設定をしておくと、デフォルトでjsonファイルに処理を返すようになります。
(コントローラーでrespond_toを使用してjsonファイルに振り分ける必要が無くなります。)

get_category_children.json.jbuilder
json.array! @category_children do |child|
  json.id child.id
  json.name child.name
end
get_category_grandchildren.json.jbuilder
json.array! @category_grandchildren do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end

ファイルの場所、間違えないでくださいね!!ビューと同じ場所に格納します。

コントローラーで二段落目、三段落目のアクション処理を行うと、このjsonファイルに飛びます。
(routes.rbで先ほど設定しましたね。)
コントローラーで設定した変数をここでajax用データに変換している訳ですね。

ちなみに、json.array!は配列形式のデータをコントローラーから受け取る時に設定します。

ajax処理の流れは、
ビュー(カテゴリー選択)→JavaScript(発火)→コントローラー(処理)→json.jbuilder(データ変換)→JavaScript(処理)→ビュー
の繰り返し(と認識しています)。

さて、最後にJavaScriptのお出ましです。

JS(items.js)
$(function)(){

  //子カテゴリー、孫カテゴリーのセレクトボックスの選択肢
  function appendOption(category){
    var html = `<option value="${category.name}" datacategory="${category.id}">${category.name}</option>`;
    return html;
  }

  //子カテゴリーのビュー作成
  function appendChildrenBox(insertHTML){
    var childSelectHtml = '';
    childSelectHtml = `<div class='listing-select-wrapper__added' id= 'children_wrapper'>
                        <div class='listing-select-wrapper__box'>
                          <select class="listing-select-wrapper__box--select" id="child_category" name="category_id">
                            <option value="---" data-category="---">---</option>
                            ${insertHTML}
                          <select>
                        </div>
                      </div>`;
    $('.listing-product-detail__category').append(childSelectHtml);
  }

 //孫カテゴリーのビュー作成
  function appendGrandchildrenBox(insertHTML){
    var grandchildSelectHtml = '';
    grandchildSelectHtml = `<div class='listing-select-wrapper__added' id= 'grandchildren_wrapper'>
                              <div class='listing-select-wrapper__box'>
                                <select class="listing-select-wrapper__box--select" id="grandchild_category" name="category_id">
                                  <option value="---" data-category="---">---</option>
                                  ${insertHTML}
                                </select>
                              </div>
                            </div>`;
    $('.listing-product-detail__category').append(grandchildSelectHtml);
  }

  //親カテゴリーが選択された時の処理(子カテゴリーの表示)
  $("#parent_category").on('change', function(){
    //選択された親カテゴリーの値を取得
    var parentCategory = document.getElementById('parent_category').value;
    //選択された親カテゴリーが"---"(初期設定)のままだとfalse、変わっているとtrue
    if (parentCategory != "---"){
      $.ajax({
        url: 'get_category_children',
        type: 'GET',
        //コントローラーに飛ばす値です。
        data: { parent_name: parentCategory },
        dataType: 'json'
      })
      .done(function(children){
        //まず、既に表示されている子、孫カテゴリーを削除
        $('#children_wrapper').remove();
        $('#grandchildren_wrapper').remove();
        //insertHTMLという変数にカテゴリーのセレクトボックスの選択肢を入れる。(一番最初の段落で設けた変数)
        var insertHTML = '';
        children.forEach(function(child){
          insertHTML += appendOption(child);
        });
        //2段落目で設定した子カテゴリーのビューの呼び出し
        appendChildrenBox(insertHTML);
      })
      .fail(function(){
        alert('カテゴリー取得に失敗しました');
      })
    }else{
      $('#children_wrapper').remove();
      $('#grandchildren_wrapper').remove();
    }
  });

  //子カテゴリーが選択された時の処理(孫カテゴリーの表示)
  $('.listing-product-detail__category').on('change', '#child_category', function(){
    var childId = $('#child_category option:selected').data('category');
    if (childId != "---"){
      $.ajax({
        url: 'get_category_grandchildren',
        type: 'GET',
        data: { child_id: childId },
        dataType: 'json'
      })
      .done(function(grandchildren){
        if(grandchildren.length != 0) {
          $('#grandchildren_wrapper').remove();
          $('#size_wrapper').remove();
          $('#brand_wrapper').remove();
          var insertHTML = '';
          grandchildren.forEach(function(grandchild){
            insertHTML += appendOption(grandchild);
          });
          appendGrandchildrenBox(insertHTML);
        }
      })
      .fail(function(){
        alert('カテゴリー取得に失敗しました');
      })
    }else{
      $('#grandchildren_wrapper').remove();
      $('#size_wrapper').remove();
      $('#brand_wrapper').remove();
    }
  });
});

はい。言いたいことはわかります。私、エスパーですから。
そんな皆さんに私から頑張れという便利な言葉を送ります。

ここにつきましてはあまりに長いので、コードにコメントアウトで処理の説明をしています
イマイチわかりにくかったら各自調べてもらえればと思います。

だらだらと長い記事を最後まで読んでいただき、ありがとうございました。
○○キャンプの受講生はLGTM必須で。

参考とさせていただいたサイト

https://qiita.com/ATORA1992/items/bd824f5097caeee09678
@ATORA1992様(とてもわかりやすい記事でした。ありがとうございました!!)

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

jQueryのfadeOutとかの実行を一定時間遅らせたい

はじめに

ある要素が出現した後1~2秒程表示して、その後フェードアウトさせたいみたいなことがありました。

例えば1秒かけてフェードアウトするだと以下ですね。
(1000ms=1秒なので、fadeOutメソッドの引数に1000が入ります。)

sample.js
$(".header_notice").fadeOut(1000);

しかしこれだと表示された瞬間いきなりフェードアウトが始まるので、ユーザーは要素に書いてある文章などが読みづらいですね。

一定時間実行を遅らせるにはどうするか

delayメソッドを挟みます。

sample.js
$(".header_notice").delay(3000).fadeOut(1000);

これで表示された後3秒待って、その後1秒かけてフェードアウトします。

ちなみにfadeOutメソッド以外も同じようにdelayを使って遅らせることができます。

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

SlackBOT Node.js axios await asyncを使ったPOSTリクエスト

はじめに

最近のJavaScriptでGET,POSTリクエストするにはaxiosを使うのがイケてるらしいが、非同期の処理になるためコールバック地獄が起こる。それを解決するためにasync awaitを使ったPOSTリクエストのサンプルコード(個人的なメモ)

SlackBOTに指定のチャンネルにテキストを投稿させるためのコードです。
カスタムインテグレーションではなく、「App」の方です
基本的なPOSTリクエストなので応用は効くと思います

コード

node.js
const axios = require('axios');//npm install axios してね

//Slackにメッセージを送る
//引数1(文字列) : チャンネル名 (例: #勤怠履歴)
//引数2(文字列) : 送りたいメッセージ
const postSlack = async (ch,msg) =>{
    console.log('postSlack...')
    const req_url = 'https://slack.com/api/chat.postMessage'
    console.log('req_url:' + req_url);

    //これを使わずにオブジェクトで送るとJSONの形式ガーーーー!!みたいなErrorがでます
    let params = new URLSearchParams();

    params.append('token','アクセストークン') //正式なものをいれてください
    params.append('channel',ch)
    params.append('text',msg)

    const res = await axios.post(req_url, params)

    return res
}

呼び出し方

index.js
const test = async () => {
 const result = await postSlack('#general','こんにちはせかい')
 console.log('result: ' + JSON.stringify(result.data))
}

結果

スクリーンショット 2020-05-29 午後6.38.38.png

うまく動かない場合は
SlackAppの管理画面から「OAuth & Permissions」→から以下の権限を与えてください(不要な権限があるとは思いますが、個人的な設定です

channels:manage
channels:read
chat:write
chat:write.customize
chat:write.public

遭遇したエラー

new URLSearchParams()を使わないときに遭遇したエラー

(node:73150) UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'ClientRequest'
    |     property 'socket' -> object with constructor 'TLSSocket'
    --- property '_httpMessage' closes the circle
    at JSON.stringify (<anonymous>)
    at test (/Users/merarli/WebstormProjects/hogehoge/index.js:1143:32)
    at processTicksAndRejections (internal/process/task_queues.js:85:5)
(node:73150) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:73150) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

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

JavaScript でのオブジェクト作り方

JavaScriptのオブジェクトは、二種類の方法で作成することが出来る

オブジェクトリテラルでの生成

var obj1 = {  
   val: 'hoge',  
   func: function(){  
     console.log('fuga');  
   } 
};

new演算子での生成

var obj2 = new Object(); 
obj2.val = 'hoge'; 
obj2.func = function() {  
   console.log('fuga'); 
};

一般的にはオブジェクトリテラルを使う方法が好れる。 変数名に変更があった場合に一箇所だけ直せば済むことや、プロパティやメソッドの宣言が固まっていて見やすかったりするためです。

ドット演算子によるプロパティアクセス

var obj = {  name : 'hoge' }; 


console.log(obj.name); // hoge

ドット演算子でのプロパティアクセスでは、プロパティ名が変数の命名規則に則っている必要があるため、数字から始まっていたり使用できない記号が含まれている名称を使用した場合はエラーになります。

コンストラクタとnew演算子

オブジェクトのインスタンスを生成するには、コンストラクタをnew演算子で呼び出します。

function hoge(name) {  
  this.name = name;  
  this.walk = function() {  
   console.log(this.name + 'が歩きます');  
  }; 
}  

var taro = new hoge('太郎'); 
taro.walk(); // 太郎が歩きます

注意として、newを付け忘れると、呼び出そうとした際に「未定義エラー」が発生するため要確認です。

独自オブジェクトの生成

Objectは、JavaScriptの全てのオブジェクトにとって基底となる存在です。 ユーザ独自のオブジェクトを用意する場合は、Objectをインスタンス化してプロパティやメソッドを追加します。

// Objectをインスタンス化 
var myhoge = new Object();  

// プロパティを追加 
myhoge.name = 'myObj';

// メソッドを追加 
myhoge.getName = function() {  
  return this.name; 
}  
// 独自オブジェクトのメソッドを呼び出す 
 console.log(myhoge.getName()); // myObj

Objectをインスタンス化したオブジェクトが利用できるメソッドとして、一般的に以下のようなものがあります。

toString

オブジェクトの値を表す文字列を返す

valueOf

指定されたオブジェクトのプリミティブな値を返す

hasOwnProperty

指定したプロパティがオブジェクト自身が保持しているかどうかを返す

まとめ

参考

Object について
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object

すぐに実践できるサイト

https://codepen.io/

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

React始めました。(環境構築編)

はじめに

おはようございます。こんにちは。こんばんは。
ワタタクです。
今回から数回にかけてReact.jsを勉強していこうと思います。

※この記事は初学者用かつ自分の勉強用で書いていきます。
※Macを使っての解説です。

ゴール

Reactを使ってアプリケーションを作れるようになる。
最終的にFirebaseを用いてのチャットボット的な物を作成する。

React.jsとは

公式サイト

  • SPA(Single-Page Application) を実現する JavaScript フレームワークの一つです。
  • Facebook 社によって開発され、Facebook の Web サイトでも利用されています。
  • 2020年4月現在の最新バージョンは 16.13.1 です。
  • JavaScript の中に直接 HTML/XML を記述する JSX という技術を利用しています。
  • JavaScript は ES6 の文法である import やアロー関数を取り入れています。

create-react-appとは

reactの環境開発を簡単に行なってくれるもの

環境構築

何か新しい言語を始めようとするには、最初に環境構築を行うことがほとんどの場合必要ですのでやっちゃいましょう。
自分の場合はReact.jsを触る前にVue.jsを触っているので、nodeやnpmといったものがもうすでに入っていますが、初学者向けに0からやっていきます。
まずはnode.js
1__bash.png

あります。(バージョン情報が出ればインストールされています。)
インストールがまだの方はこちらのインストーラーを使うことをお勧めします。

インストールが完了するとnpmも同時に使えるようになります。

npm(Node Package Manager)とは?

  • Node.jsのモジュール管理ツールです。
  • フロントエンドで使用するJavascriptのパッケージ(例えば、vueといった有名なフレームワーク、gulpなどのビルドシステム等)のインストールとバージョン管理に使います。
  • 公式サイト

1__bash_と_初めてのFirebaseを触ってみる_Authentication_.png

それでは、いよいよReactアプリを作成します。
以下のコマンドを入力して下さい。

$ npx create-react-app hello

※helloはプロジェクト名です。

実行

$ cd hello
$ npm start

React_App.png

以上。お疲れ様でした。

サーバーアップ

$ npm run build

できたbuildフォルダの中身を本番サーバに置く。

最後に

もし、間違い等、アドバイス、ご指摘等有れば教えていただけたら幸いです
次回は基本的な書き方とかやっていきます。

最後まで読んでいただきありがとうございました。
Twitterやってます。良ければチェックして見てください。:point_up::point_up::point_up:

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

[Jest] DIしたオブジェクトのメソッド呼び出しをテストする

こんにちは、スープです。
医療テックのスタートアップのお手伝いをしています。

DIしたオブジェクトのメソッド呼び出し方法を調べました。
わかりやすくするために、シンプルな例を示します。

export class CarFactory {
  constructor(private logger: ILogger) {}

  public create(name: string): void {
    this.logger.log(`creating ${name} car...`)
  }
}

このとき、コンストラクターインジェクションされた loggerlog が呼ばれていることをテストしてみます。

describe('CarFactory', () => {
  test('create', () => {
    const logger = new Logger();
    const carFactory= new CarFactory(logger);

    const spy = jest.spyOn(logger, 'log');

    // オプショナルで、Logger.log の挙動を指定したい場合は mockImplementation を呼ぶ
    spy.mockImplementation(() => console.log('log is being called'));

    const carName = 'Toyota'
    carFactory.create(carName);

    // 呼び出されていることをチェック
    expect(logger.log).toHaveBeenCalled();
    // 想定通りの引数で呼び出されていることをチェック
    expect(logger.log).toHaveBeenCalledWith(`creating ${carName} car...`);

    // これでリセットできる
    spy.mockRestore();
  });
});

参考

https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname

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

【rails】javascriptのファイル名がコントローラと同じ場合にイベント発火しなかった時の記録

javascriptのイベントが発火しない。

app/assets/javascripts/users.js
$(function(){
  $("#user-search-field").on("keyup", function() {
    console.log("OK");
  });
});

jsのファイル名を変えてみた。

app/assets/javascripts/test.js
$(function(){
  $("#user-search-field").on("keyup", function() {
    console.log("OK");
  });
});

イベント発火した。

いろいろなファイル名を試した。

○ test.js
○ ttt.js
○ user.js
× users.js
× groups.js
○ group.js

コントローラと同じファイル名だけダメ。

よくみたら次のようなファイルがあることに気付いた。

app/assets/javascripts/...
groups.coffee
users.coffee

coffee scriptの影響を疑い、coffeeファイルを削除すると、イベント発火するようになった。

もう一回、railsを再起動するとエラー。

LoadError: cannot load such file -- coffee_script 

cofee-railsをuninstall

Gemfile
# gem 'coffee-rails', '~> 4.2'
terminal
bundle install
terminal
rails tmp:cache:clear

解決!

参考

https://qiita.com/yakimeron/items/7945a1350bd4b8c2438b

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

ESLintのルールを一部警告対象外にする


パターン1:「この行だけ!このルールだけ!特別扱いして無視したい!」

記法

ある特定のルールを 特定の箇所のみで許容したいときは
許容したい行の直前にこのフォーマットでコメントを書くと、[rule-name]に記載したルールだけが無視されます。

  // eslint-disable-next-line [rule-name]

例えば

ESLintには==での比較を禁止し、===を使うよう促すルール「eqeqeq」というものがあります。
eqeqeqルールを有効にしている場合、以下のコードでは2箇所で警告が出ます。

javascript.js
  const target = 1;
  if ('2' == target) { //だめ
    return;
  } else if ('3' == target) { //だめ
    return;
  }

ここにeslint-disable-next-lineのコメントをつけると

javascript.js
  // eslint-disable-next-line eqeqeq
  ...
  const target = 1;
  if ('2' == target) {
    return;
  // eslint-disable-next-line eqeqeq
  } else if ('3' == target) {
    return;
  }

7行目は無視されます。

パターン2:「このファイル全体的に、このルール無視したい!」

記法

このフォーマットでコメントを書くと、[rule-name]に記載したルールだけが無視されます。
0はoffを意味します。
ファイル内のどこに書いても、全行に適用されます。

/* eslint [rule-name]: 0 */

例えば

こうすると2箇所とも無視されます

javascript.js
  /* eslint eqeqeq: 0 */
  ...
  ...
  ...
  if (1 == 2) {
    return;
  } else if (2 == 3) {
    return;
  }

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

JavaScript(ES6)のexportとimportについて調べてみた

ES6で導入されたexportとimportについて調べた

注!完全に自分用の忘備録です。

<script type="module">が必須

import / export を使うにはscriptのtype属性に module の指定が必須です。
指定しないと以下のようなエラーになります。

Uncaught SyntaxError: Cannot use import statement outside a module

<script type="module">の実行タイミング

type="module"のスクリプトは最後に実行されます。
またDOM構築後に実行されるので、今までのようにheadタグに読み込むかbodyの閉じタグの上で読み込ませるか考える必要がありません。

実装例(その1)

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>JSサンプル</title>
</head>
<body>
<script type="module" src="import.js"></script>
</body>
</html>
import.js
// 'module.js'でexportされたclassをimportする。
import { Point, Rectangle } from './module.js';

// importしたクラスを使用する。
const p = new Point(1, 2);
const r = new Rectangle(0, 0, 5, 5);
console.log(p.toString());
console.log(r.toString());
module.js
// classをexportしている。
export class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `{x:${this.x}, y:${this.y}}`;
    }
}
export class Rectangle {
    constructor(x, y, width, height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
    toString() {
        return `{x:${this.x}, y:${this.y}, width:${this.width}, height:${this.height}}`;
    }
}

export defaultが推奨されている

特に理由がなければexportはdefaultが推奨されています。
参考:Default exports are favored

一旦まとめ

export、importの構文に振れる機会がそんなに多くなくてまだ完全に理解していないので、この記事は自分の理解度と共にアップデートしていく予定です。

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

【JavaScript】コールバック関数に匿名関数をいれてみる

匿名関数について

匿名関数がない場合とある場合で分けてます。
書き方が違います。
現場でよく使われるらしいです。
機能は一緒です。

//子機能2(匿名関数なし)
function followCancel(){
  console.log("本当にフォローを外しますか?");
}

//子機能2(匿名関数)
const followCancel = function(){
  console.log("本当にフォローを外しますか?");
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】firebase基礎編(Auth版):Googleログインをできるようにしよう

前置き

Frame 6.png
メールアドレスログインと
要領は同じです!
コードもこちらに付け足します✍️
https://note.com/aliz/n/n7f4ae08ba828

ということで
firebaseAuthを使います?

使わなくてもできますが
その場合はやることが
3倍には増えます…笑
Google Cloud Platformから
OAuthクラウドIDを発行したり
tokenの照会をするコードを書いたり??

firebaseAuthを使うと
すっっごく!!!
簡単に!!!
実装できます?

Step1

Googleサインインについてはこちら
firebase Google Sign-In

まずはAuthentication > Sign-in method
Googleを選択

スクリーンショット 2020-05-18 18.34.31.png

メールアドレスを選択し保存するだけ!

スクリーンショット 2020-05-18 18.36.46.png

【解説/store/index.js】
準備は整ったので
公式Contens2つめに参りましょう♪
Handle the sign-in flow with the Firebase SDK

必須項目:1, 5
Optional:2, 3, 4

ということで必須項目だけ
Vuexにコピペしていきます。

・actions loginGoogleを作成
 ログインできたら
 ログイン状態をtrueにしたいので
 それを行うcheckLoginをdispatchで呼ぶ
・不要な物を削除
 コメント
 セミコロン(;)
 var token
 var user

store/index.js
import firebase from '~/plugins/firebase'

export const state = () => ({
 user: {
   uid: '',
   email: '',
   login: false,
 },
})

export const getters = {
 user: state => {
   return state.user
 }
}

export const actions = {
 login({ dispatch }, payload) {
   firebase.auth().signInWithEmailAndPassword(payload.email, payload.password)
     .then(user => {
         console.log('成功!')
         dispatch('checkLogin')
       }).catch((error) => {
         alert(error)
       })
 },
 loginGoogle ({ dispatch }) {
   var provider = new firebase.auth.GoogleAuthProvider()
   firebase.auth().signInWithPopup(provider).then(function (result) {
     dispatch('checkLogin')
   }).catch(function (error) {
     console.log(error)
   })
 },
 checkLogin ({ commit }) {
   firebase.auth().onAuthStateChanged(function (user) {
     if (user) {
       commit('getData', { uid: user.uid, email: user.email })
       commit('switchLogin')
     }
   })
 },
}

export const mutations = {
 getData (state, payload) {
   state.user.uid = payload.uid
   state.user.email = payload.email
 },
 switchLogin (state) {
   state.user.login = true
 },
}
Login.vue
<template>
<div class="login">
  <p
    v-if="user.login"
    class="text"
  >
    {{ user }}
  </p>
  <form
    v-else
    class="form"
    @submit.prevent
  >
    <label class="label">
      <span class="label">
        email
      </span>
      <input
        class="input"
        type="text"
        v-model="email"
      >
    </label>
    <label class="label">
      <span class="label">
        password
      </span>
      <input
        class="input"
        type="password"
        v-model="password"
      >
    </label>
    <button
      class="button"
      type="submit"
      @click="login"
    >
      Login
    </button>
    <button
       class="button"
       type="submit"
       @click="loginGoogle"
    >
       googleでログイン
    </button>
  </form>
</div>
</template>

<script>
export default {
 computed: {
  user () {
    return this.$store.getters['user']
  },
 },
 data () {
  return {
    email: '',
    password: '',
  }
 },
 methods : {
  login (email, password) {
    this.$store.dispatch('login', {email: this.email, password: this.password})
  },
  loginGoogle () {
    this.$store.dispatch('loginGoogle')
  },
 }
}
</script>
index.vue
<template>
<div class="page">
  <Login />
  <p
    v-if="user.login"
    class="text"
  >
    ログインに成功!
  </p>
</div>
</template>

<script>
import Login from '~/components/Login.vue'

export default {
components: {
  Login: Login,
},
computed: {
  user () {
    return this.$store.getters['user']
  },
},
}
</script>

ログインはこれだけです?
アカウント作成や、
ログアウトの仕方はまた別記事にて♪

次回予告

【Nuxt.js】Nuxt文法編:v-for
6/2(火)公開予定です!
お楽しみに♪

記事が公開したときにわかる様、
フォローをお願いします??

https://twitter.com/aLizlab

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

【JavaScript】高階関数について

高階関数について

関数の中に関数を入れたい(機能の中に機能を入れたい)場合に用いる。

高階関数の構文

function 高階関数(コールバック関数){
  //処理
 コールバック関数()
}

実際の文

//子機能1(コールバック関数で呼ばれる関数)
function tweetCancel(){
  console.log("ツイートキャンセルしていいんですか?");
}

//子機能2(コールバック関数で呼ばれる関数)
function followCancel(){
  console.log("本当にフォローを外しますか?");
}

//親機能1(高階関数)
function confirmed(fn){
  if(window.confirm("実行しますか?")){
    fn();
  }
}

//親機能1と子機能2を実行
//実行順序は親機能の処理に準ずる
confirmed(followCancel);

■処理を実行する時、 fn = 子機能の関数名 が入る。

■子機能はコールバック関数として呼ばれる。

※実際にこのコードをChromeDevで実行してみると、わかります。

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

node-addon-apiについて

Node-addon-api について

node.jsから、CやC++のAPIを直接実行するためには、
N-API を使用することになるが、非常に複雑で、コーディングが面倒になるので、
C++でN-APIをラップしたnode-addon-apiを使用したほうが良い。

ただ、Javascriptの知識と、C++の知識(メモリ管理等)が必要となりますが、
基本的に細かい制御はクラスライブラリがやってくれるので、
実装に集中できます。

ビルド方法

package.jsonの準備

まずは、package.json を準備する。
bindingsとnode-addon-apiをインストールします。

npm init # 初期設定
npm install bindings node-addon-api

binding.gypファイル

binding.gypファイルを作成して、C++のビルト環境を作成します。

{
"targets": [ 
     { 
       "target_name": "hello", 
       "cflags!": [ "-fno-exceptions" ], 
       "cflags_cc!": [ "-fno-exceptions" ], 
       "sources": [ "hello.cpp" ], 
       "include_dirs": [ 
         "<!@(node -p \"require('node-addon-api').include\")" 
       ], 
       'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], 
     } 
   ] 
 } 

hellow.cpp

cpp ファイルを作成します。

#include <napi.h>

Napi::String Method(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  return Napi::String::New(env, "world");
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "hello"),
              Napi::Function::New(env, Method));
  return exports;
}

NODE_API_MODULE(hello, Init)

C++の説明

NODE_API_MODULE(hello, Init)

javascriptとの接続するためのマクロ
Init 関数内でエクスポートするファンクションを定義する。
exports.SetでhelloというjavascriptのメソッドをMethodというC++関数に割り当てている。

Method関数内で、worldという文字列を返している。

hello.js

var addon = require('bindings')('hello');

console.log(addon.hello()); // 'world'

bindingsモジュールをロードして,
helloを呼び出します。

ビルド

npm install

を実行すると、C++のコンパイラが実行され、ビルドされます。

実行

node hello.js

を実行するとworldという文字列が表示されると思います。

終わりに

以下の例は、ほぼ node-addon-apiのサンプルを元にしました。

https://github.com/fore-vision/node-addon-api 
上にサンプルとドキュメントがあるので、色々できると思います。

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

[Vue.js]Object.assignの配列コピーで地味にハマったメモ

リアクティブに配列をコピーしたくて使用。
「空の配列」ひとつに対し、値を格納した配列を複数用意。
タブ切り替え時、コピーする配列を変更。

test.vue
let a = [1, 2, 3, 4, 5];
let b = [6, 7, 8];
let c = [9, 10];

タブ切り替え時、コピーする配列(target部分)を変える。
Object.assign(this.array, target);

その配列の要素数をviewで表示するというような実装を行ったところ、
正しい要素数が表示されずに困った。

表示結果
<p>{{array.length}}</P>
    ↓ ↓ ↓
一番要素数が多いaの配列を格納すると、
それ以降はどのタブに切り替えても、aの要素数から変更されない

原因

参考資料にもある通り、
Object.assign()メソッドはコピーを行うので、
今ある要素に上書きされる形になる。
そりゃ、要素数に変更がないわけだ・・・

test.vue
let a = [1, 2, 3, 4, 5];
let b = [6, 7, 8];
let c = [9, 10];

①aをarryにコピーする
Object.assign(this.array, a);
  → this.arry[ 1, 2, 3, 4, 5];

②次はbをコピーする
Object.assign(this.array, b);
  → this.arry[ 6, 7, 8, 4, 5]; //上書きされているため、要素数は変わらない

対策

配列に格納する前に、初期化処理を入れるようにした。
これで正しい要素数を表示できるようになった。

test.vue
// 配列初期化
this.array.splice(-this.arry.length);
//コピー
Object.assign(this.array, target);

最後に

実際は格納した配列を子に渡すなど、処理が複雑になっているため、
こんな簡単なことに気付くのが遅れてしまいました・・・
勉強あるのみですね!!

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

[Vue.js]Object.assignの配列コピーで地味にハマったメモ[追記]

リアクティブに配列をコピーしたくて使用。
「空の配列」ひとつに対し、値を格納した配列を複数用意。
タブ切り替え時、コピーする配列を変更。

test.vue
let a = [1, 2, 3, 4, 5];
let b = [6, 7, 8];
let c = [9, 10];

タブ切り替え時、コピーする配列(target部分)を変える。
Object.assign(this.array, target);

その配列の要素数をviewで表示するというような実装を行ったところ、
正しい要素数が表示されずに困った。

表示結果
<p>{{array.length}}</P>
    ↓ ↓ ↓
一番要素数が多いaの配列を格納すると、
それ以降はどのタブに切り替えても、aの要素数から変更されない

原因

参考資料にもある通り、
Object.assign()メソッドはコピーを行うので、
今ある要素に上書きされる形になる。
そりゃ、要素数に変更がないわけだ・・・

test.vue
let a = [1, 2, 3, 4, 5];
let b = [6, 7, 8];
let c = [9, 10];

①aをarryにコピーする
Object.assign(this.array, a);
  → this.arry[ 1, 2, 3, 4, 5];

②次はbをコピーする
Object.assign(this.array, b);
  → this.arry[ 6, 7, 8, 4, 5]; //上書きされているため、要素数は変わらない

対策

配列に格納する前に、初期化処理を入れるようにした。
これで正しい要素数を表示できるようになった。

test.vue
// 配列初期化
this.array.splice(-this.arry.length);
//コピー
Object.assign(this.array, target);

最後に

実際は格納した配列を子に渡すなど、処理が複雑になっているため、
こんな簡単なことに気付くのが遅れてしまいました・・・
勉強あるのみですね!!

追記

コメントよりご指摘いただきました下記の方法でなら、
初期化処理不要で、配列のコピーができました!!
こちらの方がコードがスッキリしていいですね:relaxed:
slice()の参考資料

追記
let a = [1, 2, 3, 4, 5];
let b = [6, 7, 8];
let c = [9, 10];

①配列aのコピーをする
this.array = a.slice();
→this.array[1, 2, 3, 4, 5]

②①の後、this.arrayに配列bを再度格納する
this.array = b.slice();
→this.array[6, 7, 8]  
//上書きではなく、コピー元の配列全体を切り出して新しく配列を生成してくれた!

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

【JavaScript】forループ内でawaitする方法

経緯

forループ内で同期処理を行いたかったので調べてみたら for await...of というものがあることを知りました。

結論

このように async のなかに for await...of を書くことで forループ内で await を宣言できます。

index.js
// 対象の反復オブジェクト
const targetArr = [1, 2, 3];

// 実行する関数
const sampleFunc = (value) => {
// asyncの効果は各functionブロックで切れるので逐一指定が必要
    return new Promise(resolve => { 
        // 2秒待ってから計算結果をresolveする
        setTimeout(() => {
            console.log('Calculating...');
            resolve(value * 2);
        }, 2000);
    })
}

// for await...of文は必ずasyncの中で
(async () => {
  for await (num of targetArr) {
    // 関数の実行結果を格納して表示
    const result = await sampleFunc(num);
    console.log(result);
  }
})();

for await...of とは

for await...of 文は非同期(と同期)の反復オブジェクトを繰り返して処理するループを作ります。対象の反復オブジェクトは、ビルトインの String、Array、配列様オブジェクト( arguments、NodeList 等)、TypedArray、Map、Set、さらに、ユーザーが定義した非同期・同期の反復オブジェクトが含まれます。オブジェクトの各プロパティの値に対して実行されるステートメントを使用してカスタム反復フックを呼び出します。

簡単に言うと
反復オブジェクト(ArrayやObjectなど)の中で同期処理を行う事ができる文です。

ESLintでは非推奨

便利な構文ですが、ESLintでは設計思想的な意味で推奨されていません。

https://eslint.org/docs/rules/no-await-in-loop

Performing an operation on each element of an iterable is a common task. However, performing an await as part of each operation is an indication that the program is not taking full advantage of the parallelization benefits of async/await.
Usually, the code should be refactored to create all the promises at once, then get access to the results using Promise.all(). Otherwise, each successive operation will not start until the previous one has completed.
Concretely, the following function should be refactored as shown:

反復の各要素に対して操作を行うことは一般的な作業です。
しかしながら、各段階の操作で await を実行すると async/await による並列化の利点を十分に活用できません。
一般にこのようなコードは、全てのプロミスを一度に作成し Promise.all() を用いて結果を得るようにするべきです。

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

文字色をランダムに設定したいが同じ人の発言は常に同じ色にしたかったので、文字列から色を生成する

どう実現するのか

発言者毎に不変な ID を種に HSL の H を決める関数を用意する。S と L は固定値を使うことでチャットに適した色を確実に出させる

つまりこんな感じ。

const generateColor = (seed)=>{
  return `hsl(${Number(seed) % 360}, 100%, 40%`;
};

発言者ごとに色を分けたい

著者はしばしば Discord のテキストチャネル上で TRPG をやる。
テキストチャットでやると後から実施したログを簡単に共有できるのは便利だ、と思ったのだが Discord のログを後から読み返すために目標地点までログをさかのぼるのは少々面倒だ。

「ログを HTML 形式でダウンロードして適当な場所にアップロードすればいいじゃないか」と思いそういうツールを用意して実行、無事チャットログを取得できた。だが、取得してみて欲が出た。発言者毎に色を分けたい

発言者毎に色を分けることそのものについては賛否あると思うが、私のプレイ環境では発言者毎に色を分けるのが一般的だったのである。

当たった課題

どのチャンネル(チャットルーム)をどのタイミングでダウンロードしても同じ人は同じ色であってほしい

テキストチャットのログを処理する度に発言者 ID 毎に色を設定し、生成される HTML すべてに色を割り当てていく、という方法を原則としては採る。

しかし、ダウンロードするごとに色をランダムで生成しているとダウンロードするごとに発言者毎の色が変わる。全12回のゲームで毎回発言者毎に色が違う、とかそういった事態になると読者は混乱するだろう。

そのため、特定の文字列を入力すると常に特定の色を返す関数を用意することにした。これを用いれば同じ発言者による発言は常に同じ色を割り当てることができる。先の関数に発言者の ID を入力して、出力された色を用いればよい。

背景色とのコントラストを維持したい

先の関数で色を生成する際に素直に色を生成すると次のような方法になるかと思う。

const color = `#${(Number(id) % (255*255*255)).toString(16)}`;

しかし、これで出すと #fff4f8 といった値が出力されることもある。残念ながら大変に読みにくい。白い背景であれば暗めの色を常に出してほしい。常に暗めの色を出力させるためには HSL の形式で色を決めると簡単。すなわち hsl(90, 100%, 40%) のような形式である。

const color = `hsl(${Number(id) % 360}, 100%, 40%`;

HSL の形式で色を表現する場合は1つめの値に色相を入力する。これが具体的な色を定義する要素であるため、この値が ID によって決まるようにする。
2つめの値には彩度が入る。色の鮮やかさや濃さを定める値となる。数字が小さければ色はくすむ。この値は文脈にもよるものの今回の目的では 100% でよいだろう、と考えた。
3つ目の値には輝度が入る。 50% が標準の値であり、値が増えると明るい色になり値が減ると暗い色になる。白背景には暗い色の方が読みやすいだろうということで今回は 50% より低い 40% を与えている。背景が暗い色なのであれば大きな値を入れる必要がある。

と書いても分かりにくいので彩度、輝度については以下にサンプルを書いてみた。

彩度\輝度 0% 20% 40% 50% 60% 80% 100%
0% hsl(90, 0%, 0%) hsl(90, 0%, 20%) hsl(90, 0%, 40%) hsl(90, 0%, 50%) hsl(90, 0%, 60%) hsl(90, 0%, 80%) hsl(90, 0%, 100%)
20% hsl(90, 20%, 0%) hsl(90, 20%, 20%) hsl(90, 20%, 40%) hsl(90, 20%, 50%) hsl(90, 20%, 60%) hsl(90, 20%, 80%) hsl(90, 20%, 100%)
40% hsl(90, 40%, 0%) hsl(90, 40%, 20%) hsl(90, 40%, 40%) hsl(90, 40%, 50%) hsl(90, 40%, 60%) hsl(90, 40%, 80%) hsl(90, 40%, 100%)
50% hsl(90, 50%, 0%) hsl(90, 50%, 20%) hsl(90, 50%, 40%) hsl(90, 50%, 50%) hsl(90, 50%, 60%) hsl(90, 50%, 80%) hsl(90, 50%, 100%)
60% hsl(90, 60%, 0%) hsl(90, 60%, 20%) hsl(90, 60%, 40%) hsl(90, 60%, 50%) hsl(90, 60%, 60%) hsl(90, 60%, 80%) hsl(90, 60%, 100%)
80% hsl(90, 80%, 0%) hsl(90, 80%, 20%) hsl(90, 80%, 40%) hsl(90, 80%, 50%) hsl(90, 80%, 60%) hsl(90, 80%, 80%) hsl(90, 80%, 100%)
100% hsl(90, 100%, 0%) hsl(90, 100%, 20%) hsl(90, 100%, 40%) hsl(90, 100%, 50%) hsl(90, 100%, 60%) hsl(90, 100%, 80%) hsl(90, 100%, 100%)

発言者の ID じゃなくて発言者の名前で色を決めたい

以下のようにして文字を数字に見立てることで実現できる。

let id = 0;
for(var i = 0; i < name.length; i++) {
  id += name.charCodeAt(i);
}
const color = `hsl(${Number(id) % 360}, 100%, 40%`;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】世界一使いやすいストップウォッチ

世界一使いやすいストップウォッチ

長押しして、離したらスタート

ストップウォッチ

特徴

  • 画面全体がボタンになっているため、扱いやすい
  • 押す状態によって色が変化するため、直感的に使える
  • スマホとPCそれぞれのイベントに対応(Chromeは確認済)
  • iPhoneやAndroidのデフォルトのストップウォッチの問題点を改善

使い方

  1. 500ミリ秒長押しして、離すとスタート
  2. クリック(スマホはホバーまたはクリック)するとストップ
script.js
const swCheck = document.getElementById('sw-check')
const count = document.getElementById('count');
const btn = document.getElementById('btn');

let elapsedTime1 = 0; // 経過時間(単位:ミリ秒)
let elapsedTime2 = 0;
let startTime1; // スタート時の時刻
let startTime2;
let timerId1; // タイマーのID
let timerId2;

// 10ミリ秒ごと計算
function preCountUp() {
    timerId1 = setTimeout(() => {
        elapsedTime1 = Date.now() - startTime1;
        preCountUp();
    }, 10);
    if(elapsedTime1 > 500){
        clearTimeout(timerId1);
        //経過時間
        elapsedTime1 = 0;
        swCheck.checked = true;
        count.textContent = 'OK';
    }
}

// 10ミリ秒ごと計算&更新
function countUp() {
    timerId2 = setInterval(() => {
        elapsedTime2 = Date.now() - startTime2;
        let m = Math.floor(elapsedTime2 / (1000 * 60));
        let s = Math.floor((elapsedTime2 % (1000 * 60)) / 1000);
        let ms = elapsedTime2 % 1000;
        m = `0${m}`.slice(-2);
        s = `0${s}`.slice(-2);
        ms = `00${ms}`.slice(-3);
        count.textContent = `${m}:${s}.${ms}`;
    }, 10);
}

// 事前カウント開始
function preStart() {
    btn.classList.add('start');
    btn.classList.remove('stop');
    if(swCheck.checked === false) {
        startTime1 = Date.now();
        preCountUp(); //カウントを開始
    }
}
// 事前カウント終了
function preStop() {
    btn.classList.remove('start');
    btn.classList.add('stop');
    if(swCheck.checked === false) {
        clearTimeout(timerId1);
    }
}

function start() {
    if(swCheck.checked === true) {
        btn.classList.add('start');
        btn.classList.remove('stop');
        startTime2 = Date.now();
        clearTimeout(timerId1);
        countUp(); //カウントを開始
    }
}
function stop() {
    if(swCheck.checked === true) {
        swCheck.checked = false;
        btn.classList.remove('start');
        btn.classList.add('stop');
        clearTimeout(timerId2);
        // 最終的な記録
        let m = Math.floor(elapsedTime2 / (1000 * 60));
        let s = Math.floor((elapsedTime2 % (1000 * 60)) / 1000);
        let ms = elapsedTime2 % 1000;
        m = `0${m}`.slice(-2);
        s = `0${s}`.slice(-2);
        ms = `00${ms}`.slice(-3);
        count.textContent = `${m}:${s}.${ms}`;
    }
}

const ua = navigator.userAgent;
if (ua.indexOf('iPhone') > -1 
    || (ua.indexOf('Android') > -1 && ua.indexOf('Mobile') > -1) 
    ||  (ua.indexOf('iPad') > -1 || ua.indexOf('Android') > -1)) {
    // スマートフォン or タブレット
    //touchstartはSafariとIE以外対応
    btn.addEventListener("touchstart", ()=> {
        preStart();
        stop();
    });
    btn.addEventListener("touchend", ()=> {
        preStop();
        start();
    });
} else {
    // PC
    btn.addEventListener("pointerdown", ()=> {
        preStart();
        stop();
    });
    btn.addEventListener("pointerleave", ()=> {
        stop();
    });
    btn.addEventListener("pointerup", ()=> {
        preStop();
        start();
    });
}

仕組み

  • 長押し状態で、500ms経過するとチェックボックスにフラグを立てる
  • フラグが立っている場合のみ、クリックを離したらスタート可にする
  • クリックされると、フラグを消す

最後に

これがJavaScriptを学習されている皆様にお役に立てれば幸いです。誤り等ございましたら、ご気軽にコメントをいただけたらと思います。

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

【セレクトボックス連動】WebAPI を叩く際に Enum を JSON で渡す

Kotlin API を叩く際に Enum を JSON から変換する

  • SpringBoot2
  • Thymeleaf
  • Jackson (Jsonのシリアライズ・デシリアライズで利用)
  • パラメータで渡すのが一番簡単ですが、折角(?)なので JSON で渡してみる
  • Jsonで渡すと書いてありますが実際は、Json -> Enum 変換
  • Kotlin

実施環境

開発PC: Windows 10
Java: 8
Kotlin: 0.8.19
Eclipse: 2019-06 (4.12.0)

想定される場面

  • 連動するセレクトボックスなど
    • 1つ目のセレクトボックスの選択によって2つ目のセレクトボックスの内容が変わる場合
    • Enum の入れ子でやってみる
    • Enum は継承出来ないのが今ひとつ

1つ目のセレクトボックス用 Enum

  • JsonSerialize, JsonDeserialize アノテーションでシリアライズ・デシリアライズのクラスを指定
@JsonSerialize(using = EnumSerializer::class)
@JsonDeserialize(using = EnumDeserializer::class)
enum class Select1Enum(val text:String, val arr:Array<*>) {

    SELECT1("セレクト2-1", Select21Enum.values()),
    SELECT2("セレクト2-2", Select22Enum.values()),
    SELECT3("セレクト2-3", Select23Enum.values()),
    ;
        // 名称により Enum 取得
        fun getByText(text: String):Select1Enum? {

            var result: Select1Enum? = null
            enumValues<Select1Enum>().forEach {
                if (it.text == text) {
                    result = it
                }
            }
            return result
        }

    }

    override fun toString():String {
        return ReflectionToStringBuilder.toString(this)
   }
}

2つめのセレクトボックス用 Enum

  • 同様に複数用意
enum class Select21Enum(val text:String) {

    SELECT1("セレクト2-1ー1"),
    SELECT2("セレクト2-1-2"),
    SELECT3("セレクト2-1-3"),
    ;
        // 名称により Enum 取得
        fun getByText(text: String):Select21Enum? {

            var result: Select21Enum? = null
            enumValues<Select21Enum>().forEach {
                if (it.text == text) {
                    result = it
                }
            }
            return result
        }

    }
}

シリアライズ・デシリアライズ用のクラス

  • 簡単に Enum の text で enum の項目が指定できるように
class EnumDeserializer:JsonDeserializer<Any>() {

    @Throws(IOException::class)
    override fun deserialize(jp:JsonParser, ctxt:DeserializationContext):Select1Enum? {

        val jsonNode:JsonNode = jp.getCodec().readTree(jp)

        val jsonValue = jsonNode.get("text").asText()
        for (enumValue in Select1Enum.values()) {
            if (enumValue.text.equals(jsonValue)) {
                return enumValue
            }
        }
        return null
    }
}

class EnumSerializer:JsonSerializer<Select1Enum>() {

    @Throws(IOException::class)
    override fun serialize(value:Select1Enum, jgen:JsonGenerator, provider:SerializerProvider) {
        jgen.writeStartObject()
        jgen.writeFieldName("text")
        jgen.writeString(value.text)
        jgen.writeEndObject()
    }
}

APIで出力するテンプレートを用意

  • fragment_select2.html(thymeleaf)
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>Insert title here</title>
</head>
<body id="fragmentBody" th:fragment="fragmentBody()">
            <select name="select2" id="select2" th:fragment="fragmentSelect()">
              <option value="">選択してください。</option>
              <option th:if="${select2List != null}" th:each="i : ${select2List}" th:value="${i.text}" th:text="${i.text}" th:selected="${i.text} == *{select2}"> --- </option>
            </select>
            <span th:if="${#fields.hasErrors('select2')}" th:errors="*{select2}" id="select2.errors" class="help-block error" th:style="|display: block;|">正しく入力してください。</span>
</body>
</html>

API出力用コントローラ

  • /api/select/getSelect2
  • @RequestBody アノテーションをつける
  • 後述 javascript で呼び出される
  • Json 形式で受け取ったパラメータを Enum へデシリアライズ
@Controller
@RequestMapping("api/select")
open class SelectApiController {

    @PostMapping(path=["/getSelect2"],
        consumes=[MediaType.APPLICATION_JSON_VALUE])
    open fun getSelect2(@RequestBody(required=false) select1:Select1Enum?, model:Model): String {

        if (select1 != null) {
            model.addAttribute("select2List", select1.arr)
        }

        return "fragment_select2::fragmentSelect()"
    }
}

入力用フォームのHTML(抜き出し)

            <div>
              <select name="select1" id="select1" th:field="*{select1}">
                <option value="">選択してください。</option>
                <option th:each="i : ${selectList}" th:value="${i.text}" th:text="${i.text}">選択1</option>
              </select>
              <span th:if="${#fields.hasErrors('select1')}" th:errors="*{select1}" id="select1.errors" class="help-block error" th:style="|display: block;|">セレクト1を正しく入力してください。</span>
            </div>
            <div id="fragment_select2" th:include="select/fragment_select2::fragmentBody()">

セレクトボックス連動用 javascript

  • jquery 利用
  • Json 形式で渡す
  • 結果は #fragment_select2 に注入
// ページロード後に呼び出す
function initializeSelect1Events() {

    // 業種セレクト連動
    $("#select1").on('change', function(e) {
        select1OnChange();

    });
}

function select1OnChange() {
    var url = contextPath + '/api/select/getSelect2';
    var value = $("#select option:selected").val();
    var data = JSON.stringify({'text': value});

    $.ajax({
        headers: {
            'Content-Type': 'application/json'
        },
        url: url,
        type: 'POST',
        cache: false,
        dataType: 'html',
        data: data,
        traditional: true,
        beforeSend: function(xhr, settings) {}
    })
    .done(function(data, textStatus, xhr) {
        if (data.error !== undefined) {
            alert(data.error);
        }
        else if (data.length == 0) {
            alert("セレクト2が取得できませんでした");
        }
        else {
            $("#fragment_select2").html(data);
        }
    })
    .fail(function(xhr, textStatus, errorThrown){
        alert("セレクト2が取得できませんでした");
    })
    .always(function(xhr, msg){
    });
}

以上、お疲れさまでした!

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

Snowpack v2 リリース - 標準 ES モジュールで戦う未来

Snowpack v2 がリリースされました1。そもそも Snowpack とは何か、v1 からの変更点は何かを見ていきます。
(LGTM と感じたら Snowpack を使ってあげてください。たくさん使われるほど育つので。)

cf. v1 のときの記事 普通じゃ満足できない脱 webpack マニアのあなたに贈る Snowpack - Qiita

後半では、Create React App で生成したトップページと同じものを作ってみます。

Create React App で生成したトップページ

Snowpack is 何

npm パッケージを、ブラウザーから読み込めるよう ES modules 形式2に変換してくれるバンドラーです。

たとえば React は、次のように import して使います。この import 文は、webpack などのバンドラーが解釈し変換することで初めて動きます。

App.tsx

import React from 'react'

export function App() {
  return <h1>Hello</h1>
}

一方、ブラウザーはもう標準で import が使えるようになっています3。これが ES modules ですが、それでも React の import に webpack を使うのは、残念ながら React パッケージ側が ES modules に対応していないからです。ES modules は新しめですし、React に関しては実質トランスパイルが必須なので ES modules だけでは使えないというのが影響しているのでしょう。

Snowpack はその点を補ってくれます。React や、ES modules に対応していない npm パッケージを変換し、ES modules で読み込めるようにしてくれます。

一度 npm パッケージを変換したらそれ以降、新規に npm パッケージを追加しない限り、Snowpack によるバンドルは発生しません。アプリケーションコードは ES modules で書けばよく、変更のたびにバンドルする必要がないのです。

次のように、node_modules 内のパッケージを web_modules というフォルダーに ES modules 形式で出力して、アプリケーションから使うイメージです。

node_modules/  ->  web_modules/
  react              react.js
  react-dom          react-dom.js

App.tsx

-import React from 'react'
+import React from '/web_modules/react.js'

 export function App() {
   return <h1>Hello</h1>
 }

v1 との違い

v1 時点では前述の機能しか持ちませんでした。徹底してアプリケーションコードはノータッチ。拡張したければ Rollup プラグインや Babel プラグインを使う必要がありました。

開発サーバーも存在せず、Servør など別のパッケージを使う必要がありました。

v2 からは、ES modules で高速に開発する という思想はそのままに、よく使う機能が最初から組み込まれています。ある程度アプリケーションコードも触ってきます。

開発サーバーが組み込みに - dev コマンド

snowpack dev コマンドで開発サーバーを起動できます。次に説明する、import パスの変換もやってくれます。

API サーバーへのプロキシーも設定可能です。

image.png

import パスを 自然に 書けるように

import React from '/web_modules/react.js' は見慣れませんね。tsconfig.json に設定も追加せねばなりません。import React from 'react' と書きたくなります。とはいえブラウザーは前者の書き方でないとファイルを見つけられません。

v1 では、import パスを変換したければ Babel を設定する必要がありました。v2 からは、Snowpack がよしなに変換してくれます。

App.tsx

-import React from '/web_modules/react.js'
+import React from 'react' // v2 からこれで OK

 export function App() {
   return <h1>Hello</h1>
 }

内部では Go 製の esbuild を使っているようです。JSX もデフォルトで変換してくれます。

CSS や画像の import が可能に

v1 では、ブラウザーが標準で import できないものはやはり import できませんでした。v2 から、次の資材は import できるようになりました。

  • CSS
  • CSS modules(import styles from './style.module.css' のように拡張子を .module.css にすれば OK)
  • JSON
  • PNG, SVG

本番資材のビルドも可能に - build コマンド

本番資材は snowpack build でビルドできます。dev コマンドと同様 import パスの変換をしたうえで、index.html や favicon.ico などの静的資材もまとめてくれます。

また、オプションで Parcel による最終バンドルもしてくれるようです。開発中は ES modules のスピードを享受しつつ、ES modules が使えないブラウザーや多数の通信がデメリットになる HTTP/1 環境に向けてもリリースが可能です。

node_modules 配下にないパッケージもバンドル可能に - install コマンドと webDependencies

v1 では、node_modules 配下にあるパッケージソースをバンドルして web_modules に起き直す動きしかありませんでした。v2 でも引き続き可能ですが、node_modules になくてもいいパッケージは snowpack install コマンドによって直接 web_modules フォルダーに配置できます。

$ npx snowpack install react

そのように追加したパッケージは、package.json の webDependencies フィールドで管理されます。

package.json

 {
   "name": "snowpack-example",
+  "webDependencies": {
+    "react": "^16.13.1"
+  },
   "devDependencies": {
     "snowpack": "^2.0.2",
     "typescript": "^3.9.3"
   }
 }

まだまだうまくいかないパッケージは多い感触ですが、react のように型パッケージが別になっているパッケージの型もうまいことインストールしてくれるのは良い点です。

ちなみに、install によって取得するパッケージは Pika CDN で管理されています。Snowpack は Pika の製品群の一つです。

型検査や lint を実行可能に

v1 では、tsc や eslint コマンドを watch モードで別途起動する必要がありました。v2 からは、dev コマンドと同時にこれらのチェックを走らせることができます。詳しくは具体例の項で示します。

Snowpack プラグインの利用が可能に

v1 時点でも、Snowpack の内部は Rollup なので Rollup プラグインとしてプラグインを使うことはできました。v2 からは、Snowpack 専用のプラグイン API が提供されます。

ボイラープレートの自動生成が可能に - Create Snowpack App (CSA)

次のコマンドでボイラープレート(ブランクプロジェクト)の作成が可能になりました。使えるテンプレート一覧はこちら https://github.com/pikapkg/create-snowpack-app/blob/v1.0.4/README.md

$ npx create-snowpack-app new-dir --template @snowpack/app-template-blank

Yarn の場合。

$ yarn create snowpack-app new-dir --template @snowpack/app-template-blank --use-yarn

Create React App で生成したトップページと同じものを作る

この画面を目指します。

Create React App で生成したトップページ

  • React, ReactDOM
  • TypeScript 3.9
  • Babel なし(JSX も TS でトランスパイル可能なため)
  • PWA 関係の設定はなし
  • テストなし

ファイル構成

全量はこのようになります。

.
├── package-lock.json
├── package.json
├── plugin/
│   └── typescript.js
├── public/
│   ├── favicon.ico
│   └── index.html
├── snowpack.config.json
├── snowpack.lock.json
├── src/
│   ├── App.css
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   └── logo.svg
├── tsconfig.json
├── typings/
│   └── static.d.ts
└── web_modules/
    ├── .types/
    │   ├── react/
    │   │   ├── experimental.d.ts
    │   │   ├── global.d.ts
    │   │   └── index.d.ts
    │   └── react-dom/
    │       ├── experimental.d.ts
    │       └── index.d.ts
    ├── common/
    │   └── react-8a00ae8c.js
    ├── import-map.json
    ├── react-dom.js
    └── react.js

量が多いですが、種類別にすると次のようになります。

# npm 関係
├── package-lock.json
├── package.json

# Snowpack 関係
├── plugin/
│   └── typescript.js
├── snowpack.config.json
├── snowpack.lock.json

## Snowpack による自動生成
└── web_modules/
    ├── .types/
    │   ├── react/
    │   │   ├── experimental.d.ts
    │   │   ├── global.d.ts
    │   │   └── index.d.ts
    │   └── react-dom/
    │       ├── experimental.d.ts
    │       └── index.d.ts
    ├── common/
    │   └── react-8a00ae8c.js
    ├── import-map.json
    ├── react-dom.js
    └── react.js

# ソースコード
├── public/
│   ├── favicon.ico
│   └── index.html
├── src/
│   ├── App.css
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   └── logo.svg

## TS のオプションと追加の型定義
├── tsconfig.json
├── typings/
│   └── static.d.ts

ファイルの中身

npm 関係

package.json

{
  "name": "snowpack-example",
  "version": "0.0.1",
  "license": "MIT",
  "scripts": {
    "prepare": "snowpack",
    "start": "snowpack dev",
    "build": "snowpack build"
  },
  "webDependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "snowpack": "^2.0.2",
    "typescript": "^3.9.3"
  }
}

Snowpack 関係

snowpack.config.json に Snowpack の設定を書きます。

snowpack.config.json

{
  "plugins": ["./plugin/typescript"],
  "scripts": {
    "mount:public": "mount public --to /",
    "mount:web_modules": "mount web_modules",
    "mount:src": "mount src --to /_dist_",
    "run:ts,tsx": "tsc --noEmit",
    "run:ts,tsx::watch": "$1 --watch"
  },
  "installOptions": {
    "installTypes": true
  },
  "devOptions": {
    "bundle": false
  }
}
  • plugins に npm パッケージや自作のスクリプトを指定して、プラグインとして使うことができます。
  • scripts には dev/build コマンド時に snowpack にやってほしいことを指示します。
    • run:* で型検査や lint を実施します。run:ts,tsx なので、対象の拡張子を .ts, .tsx に限定しています。
    • run:*::watchdev コマンドのときに実行するものです。$1run:* の内容、つまりここでは tsc --noEmit に置き換わ流ので、run:ts,tsx::watchtsc --noEmit --watch を実行することになります。
    • mount:* はファイルコピーの処理です。上記の mount をした結果は次のようになります(出力先を build フォルダーとすると)。public 配下は build 直下にそのままコピー、src 配下は変換後に _dist_ へ、web_modules 配下は同名のフォルダーへコピーされています。
build
├── _dist_/
│   ├── App.css
│   ├── App.css.proxy.js
│   ├── App.js
│   ├── index.css
│   ├── index.css.proxy.js
│   ├── index.js
│   ├── logo.svg
│   └── logo.svg.proxy.js
├── favicon.ico
├── index.html
└── web_modules/
    ├── common/
    │   └── react-8a00ae8c.js
    ├── import-map.json
    ├── react-dom.js
    └── react.js
  • installOptions.installTypes は、webDependencies の取得時に型定義も取得するかどうかを設定します。
  • devOptions.bundle で、Parcel による最終バンドルをオフにしています。

ここで使っているプラグインの中身は次のとおり。TypeScript Compiler API を使って、型検査はスキップしてソースをトランスパイルだけしています。

plugin/typescript.js

// @ts-check
const ts = require('typescript')
const fs = require('fs').promises

module.exports = function plugin() {
  /**
   * The content of tsconfig.json relative to the CWD.
   *
   * @typedef {import('typescript').CompilerOptions} CompilerOptions
   * @type {Promise<{ compilerOptions: CompilerOptions }>}
   */
  const tsconfig = fs
    .readFile('tsconfig.json', { encoding: 'utf-8' })
    .then(
      source => ts.parseConfigFileTextToJson('tsconfig.json', source).config,
    )

  return {
    defaultBuildScript: 'build:ts,tsx',

    /**
     * Transpile TS/TSX source into JavaScript source.
     *
     * @param {object}  _
     * @param {string}  _.filePath
     * @param {string}  _.contents
     * @param {boolean} _.isDev
     * @returns {Promise<{ result: string; resources?: { css?: string } }>} contains JS source
     */
    async build({ filePath, contents }) {
      // d.ts files in web_modules will reach here, so I have to ignore them.
      if (filePath.endsWith('.d.ts')) return

      const { compilerOptions } = await tsconfig

      const result = ts.transpile(contents, compilerOptions, filePath)
      return { result }
    },
  }
}

defaultBuildScript: 'build:ts,tsx' があることで、このプラグインを読み込んだ snowpack.config.json の scriptsbuild:ts,tsx を追加したのと同じ効果が得られます。

build:* スクリプトは、dev/build コマンド両方で呼ばれる、該当した拡張子のファイルを変換する処理です。これによって TS ファイルを、ブラウザーで実行可能な JS ファイルに変換しているのです。

公式ドキュメントでは "build:ts,tsx": "babel --filename $FILE" が例として紹介されています。上記では、プラグインを利用して "build:ts,tsx": "tsc $FILE" のようなことを実現しています4

snowpack.lock.json は、webDependencies のバージョンを固定するファイルで、Snowpack が自動生成します。package-lock.json 相当です。

snowpack.lock.json

{
  "imports": {
    "react": "https://cdn.pika.dev/pin/react@v16.13.1-ByypZEPVPs6cpkpGdpQK/react.js",
    "react-dom": "https://cdn.pika.dev/pin/react-dom@v16.13.1-qv1YB4ZAVXJG84jEgino/react-dom.js"
  }
}

Snowpack による自動生成

web_modules 配下は、Snowpack が npm パッケージを ES modules 対応に変換した結果を配置する場所です。これは package.json の scripts.prepare の実行時、つまり npm install の直後に生成されます5

package.json

  "scripts": {
    "prepare": "snowpack",

ソースコード

アプリケーションのソースコードは index.html 以外、Create React App が生成するのとほぼ同じものなので、解説はスキップします。

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Snowpack Example</title>

    <script type="module" src="/_dist_/index.js"></script>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>

index.html は そのままコピーされる ことを前提に作る必要があります。つまり次の点に気をつけます。

  • %PUBLIC_URL% のような環境変数は使えない(置換されない)
  • script タグは自動で挿入されない

ただし、たとえば "build:html": "envsubst < $FILE" とすれば同等のことが可能ですし、プラグインを書くこともできます。

src/App.css

.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

src/App.tsx

import React from 'react'
import logo from './logo.svg'
import './App.css'

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  )
}

export default App

src/index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

src/index.tsx

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

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root'),
)

src/logo.svg

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
    <g fill="#61DAFB">
        <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
        <circle cx="420.9" cy="296.5" r="45.7"/>
        <path d="M520.5 78.1z"/>
    </g>
</svg>

TS のオプションと追加の型定義

Snowpack は TS のコンパイル過程には口を出してこないので、tsc コマンドがうまくいくように設定できていれば OK です。

注意点は、node_modules, node_modules/@types 配下だけでなく web_modules, web_modules/.types からもパッケージを探す必要があるので、baseUrlpaths を忘れず設定する点です。

tsconfig.json

{
  "compilerOptions": {
    // Target latest browsers
    "target": "ES2019",
    "lib": ["ES2019", "DOM", "DOM.Iterable"],

    // Required: Use module="ESNext" so that TS won't compile/disallow any ESM syntax.
    "module": "ESNext",

    // Required for some packages.
    "moduleResolution": "Node",

    // `import React` instead of `import * as React`
    "allowSyntheticDefaultImports": true,

    // <div /> => React.createElement("div")
    "jsx": "react",

    // Required: Map "/web_modules/*" imports back to their node_modules/ TS definition files.
    "baseUrl": "./",
    "paths": {
      "*": [
        "node_modules/@types/*",
        "web_modules/.types/*",
        "node_modules/*",
        "web_modules/*.js"
      ]
    },

    // Skip npm module type check.
    "skipLibCheck": true,

    // Only for type checks.
    "noEmit": true,

    // Useful type checks.
    "strictNullChecks": true
  },

  "include": ["./typings/**/*.d.ts", "./src/**/*"]
}

typings/static.d.ts

declare module '*.css'

declare module '*.svg' {
  const ref: string
  export default ref
}

ここまで作りきって、プロジェクト直下(package.json と同じ階層)で次のコマンドを実行します。

$ npm install
$ npm start

開発サーバーが起動して、http://localhost:8080 に画面が表示されれば成功です。

Snowpack を使った感想

依存が少なくて手軽

package.json の devDependencies はたったこれだけです。

  "devDependencies": {
    "snowpack": "^2.0.2",
    "typescript": "^3.9.3"
  }

開発を進めるとこれよりも増えていくのでしょうが、最小構成ですら以下のようになる webpack と比べると、だいぶ心理的負担が少なくなります。

  "devDependencies": {
    "webpack": "",
    "webpack-dev-server": "",
    "webpack-html-plugin": "",
    "ts-loader": "",
    "css-loader": "",
    "file-loader": "",
    "typescript": "^3.9.3"
  }

設定ファイルは書かないといけませんが、それでも肥大化+ロジックの紛れ込みがちな webpack.config.js よりはすっきりと抑えられると思います。

とはいえ、これらのメリットは webpack の自由度の高さとトレードオフなので、がっつりとチューニングをしたい(そして継続メンテする体力がある)ときは webpack が良いでしょう。

チャンク分割が簡単

最大のメリットはこれだと思います。アプリケーションコードをばらばらのままデプロイし、依存をブラウザーに解消してもらうので、ビルド時にあれこれ考えなくて良くなっています。

読み込みの必要なスクリプトの総量が減るわけではないので、dynamic import やそもそものダイエットなど基本的な施策は必要ですが、それは webpack も同じです。

バンドラーの移行先としても現実味を帯びてきた

既存の webpack プロジェクトも、ローダーとプラグインへの依存次第ではほとんどそのまま移せると思います。CSS modules もありますし、大体要件は満たせそう。

逆に、移行できないような webpack プロジェクトは特殊なローダーを使っている可能性が高いので、webpack.config を継続メンテする覚悟が必要かも。


以上です。LGTM と感じたら Snowpack を使ってあげてください。

参考


  1. https://www.snowpack.dev/posts/2020-05-26-snowpack-2-0-release/ 

  2. Webpack を使わずに import 文を使う - Qiita 

  3. https://caniuse.com/#feat=es6-module 

  4. 厳密には tsc $FILE では代用できません。build:* の実行結果は標準出力に変換結果を出力する必要があるものの、tsc コマンドは標準出力ではなく直接ファイルを生成してしまうためです。babel コマンドは標準出力に結果を出力するので babel --filename $FILE が可能になっています。 

  5. postinstall スクリプトの代わりに prepare が推奨 

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

fetchのmodeについて

仕様

https://fetch.spec.whatwg.org/#concept-request-mode

ここに書いてあることの和訳と実際使う時の補足。

fetchのmode

リクエストのモードを決めるオプション。

fetch(url, { mode: "cors" })

no-cors

CORS-safelisted methodsCORS-safelisted request-headersだけを使ったリクエストを送る。
成功するとopaque filtered responseを返す。

no-corsという文字通り、実質別オリジンへのリクエストとしては機能しなくなる。

CORS-safelisted methodsCORS-safelisted request-headers

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
ここの「単純リクエスト」の項目を参照。

opaque filtered response

no-corsの場合、CORSであってもエラーが出ず、下記のような虚無なレスポンスが返る。

  • type: "opaque"
  • URL list: 空
  • status: 0
  • status message: 空文字列
  • header list: 空
  • body: null

スクリーンショット 2020-05-29 13.35.06.png

cors

CORSなリクエストを送る。
CORSのプロトコルに沿わない場合(必要なヘッダが無いなど)にはエラーとなる。

CORSなリクエストをしたいのであればこれ。

same-origin

Used to ensure requests are made to same-origin URLs. Fetch will return a network error if the request is not made to a same-origin URL.

別オリジンへのリクエストを送れないようにする。
リクエスト先が別オリジンだった場合即エラー。

navigate

ページ遷移の時に使う特別なモード。
全くわからん、ページ遷移の時にjsでfetchを発火させるってどういうユースケース?

https://stackoverflow.com/questions/55106292/how-to-use-the-navigate-request-mode

websocket

websocketの接続を確立させるときに使う特別なモード。
仕様以外で言及されているのを見たことがないので無視して良いんじゃないかな()

modeのデフォルト値

fetchの仕様では「デフォルト値はno-corsだけど、新しい機能にno-corsを使うのは安全じゃないから推奨しないよ」と書いてあり、ChromeでもFirefoxでもSafariでもデフォルト値はcorsになります。
つまり、純粋にCORSリクエストを送る場合は何も指定しなくてOK。

結局どれ使えば良いの?

corsにしておけば良い気がしました(概ね最近のブラウザはデフォルト値がcorsなのでつまるところ指定が不要、そういう暗黙的な決定が気になる場合は明示的に指定しよう)。
同一オリジンの場合はもちろん問題なく通るし、別オリジンの場合も適切でなければエラーになります。

逆に同一オリジン以外への意図せぬアクセスをしないようにするためにsame-originとかを使う場合もあるのかな?

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

オブジェクトについて

オブジェクトとは

複数の値をまとめて管理するのに用いられる。
また、その管理においては、それぞれの値に対しプロパティと呼ばれる名前を付ける
{プロパティ1:値1, プロパティ2:値2}

定数への代入

const menu = {name:"ラーメン", price:500};
console.log(menu)
console.log(menu.name)
表示例

{name:"ラーメン",price:500}
ラーメン

オブジェクトを要素に持つ配列

配列

要素がオブジェクトの時

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

JS nullチェック

概要

JavaScriptで変数のnullチェックをする方法メモ

hoge.js
var element = document.getElementById('hoge');
// 改善前
if (element != null) {
  // 処理
}

// 改善後
if (!element) {
  // 処理
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新型コロナウイルス(COVID-19)の Lightning Web Component を作ってみた感触

属性

背景

最近Lightning Web Component(LWC)を触ってみて、実践的な開発経験をしてみたいと思ってました。そして現在新型コロナウイルスが広がり、何か自分でも社会貢献にできることを考えました。

アイデア

Salesforceオブジェクトのレコードページ内にレコードの国名に関連する感染者情報を表示させたい。更に、感染者情報のトレンドも一目でわかるようにグラフで表現させたい。

やり方

国名データ抽出

そもそも国名の情報を持ってるSalesforceオブジェクトは取引先と取引先責任者のみなので、これらのオブジェクト限定で対応する形になります。デベロッパーエディションなどで、サンプルデータをみてみたら取引先オブジェクトの中に請求先の国と市区郡(BillingCountryと BillingCity)が一番記入されましたし、そして取引責任者は郵送先の国と市区郡(MailingCountryとMailingCity)が一番多かったと見てましたので、「BillingCountry、BillingCity、MailingCountryとMailingCity」を元に国名のデータを抽出します。

COVID-19データソース

色々調査し、比較した結果今回こちらのデーターソースを利用してます。毎日3回アップデートされるので、最新データを取得できるかと思います。

グラフ描画

ChartJSを利用します。こちらの公式記事にも書いてありますので、LWCでグラフを描画させるのはChartJSが一番最適です。

ロジックと流れ

こんな感じでロジックと流れを考えました。

属性

アーキテクチャー

データソースはAPIではなくJSONファイルのため、LWCに渡る前に事前処理をしないといけません。そのために、中間APIが必要です。中間APIは事前処理のためだけではなく、キャッシュをさせるためもできます。それを実現したら、こんな感じになります。
architechture.png

直面した課題

オブジェクトによりレコードを取得

LWCでレコードを取得するのは@wireで取得する必要があります。しかし@wireでのオブジェクトカラムの定義、普通は静的でないといけないので、今回のように取引先オブジェクトかどうかを検出し定義するカラムを決める、という動的な定義はできませんでした。

import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';

export default class Record extends LightningElement {
    @api recordId;
    // 静的でアブジェクトカラムを定義
    @wire(getRecord, { recordId: '$recordId', fields: [ACCOUNT_NAME_FIELD] })
    record;
}

解決方法 - $

$プリフィックスはよく@wireの中にレコードIDなどに取得するために使用すると思いますが、実はこの記号を付けると変数をリアクティブかつ動的にすることができたと以下の記事に書いております
wireサービスの公式記事
つまり、変数をfieldsに入れて値が変わる毎にwireを動的に実行するという実装ができました。こんな感じです。

import { LightningElement, wire, api } from 'lwc';

export default class Covid19 extends LightningElement {
  @api recordId;
  @api objectApiName;
  fields;

  @wire(getRecord, { recordId: '$recordId', fields: '$fields' })
  load(result) {
    if (result.data) {
      this.record = result.data.fields;
      this.fetchApi()
    }
  }
  async connectedCallback() {
    if (this.objectApiName === 'Account')
      this.fields = [ACCOUNT_BILLING_CITY, ACCOUNT_BILLING_COUNTRY];
    else
      this.fields = [CONTACT_MAILING_CITY, CONTACT_MAILING_COUNTRY];
  }
}

後、ご存知ない方も居るかもしれないので、htmlに使用する変数は@track必要ありません。こちらのプルリクエストを参考して下さい。

レコードに国名が含まれない時

必ずやレコードの中に国名が含まれるわけではないですが、JSONでは国名がキーになってます。

解決方法

all-the-citiesというnpmパッケージを中間APIに導入し、フィルタリングします。コードはこんな感じです。

import Cities from 'all-the-cities'

export const searchCountry = (city, flags) => {
  const dataList = Cities.find((cityName) => {
    return cityName.name.toLowerCase().match(city.toLowerCase())
  })
  const countryCode = dataList.country
  const flagsKeys = Object.keys(flags)
  const flagsValues = Object.values(flags)
  const idx = flagsValues.findIndex((country) => {
    return country.code.match(countryCode)
  })
  return flagsKeys[idx]
}

国名の類義語 (サーバー側課題)

今回中間APIを使って、データソースのJSONリストから該当の国のデータのにを抽出します。しかし、世界には国の名前の呼び方がいくつかあるという国が存在してます。日本ではJapanということで完結できましたが、例えば米国とかだと色々な国名があります。USUSAUnited StatesUnited States of Americaなどでも同じ国を指してます。JSON側は一つしか存在してないので、問題が起こりました。実施にこちらのissueにこのような問題が上がりました。

解決方法 - 類義語システム

上記の問題にならないために、中間サーバーに類義語システムを実装しました。例:先ほどの米国とかはUSUSAUnited StatesUnited States of AmericaなどをUSに変換し、JSONからデータを抽出されます。

ソースコード

完成したソースコードはこちらGithubにありますので、どうぞ自由に使ってください。

フィードバックやプルリクエスト

何かありましたらこちらのGithub IssueGithub PRを活用してください。

いやあ、久々に書きました。そのためまだ書き慣れてないので、何か訂正があればコメントをお願いします。

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

JavaScript HSV RGB 変換

この手の変換処理はすでに出尽くしている感もありますが…
十数年前に作成したスクリプトを見つけたので、引数受け取り周辺を中心にちょっと手直しして。

hsv2rgb

HSVからRGBに変換します。

引数は h, s, v の順で渡します。
h = 0以上360未満の浮動小数点数
s = 0~1の浮動小数点数
v = 0~1の浮動小数点数

戻り値はRGBに変換した各値をオブジェクトで返します。
hex : #RRGGBB 形式の文字列
rgb : R, G, B 各値(0~255)の配列
r: Rの値(0~255の整数)
g: Gの値(0~255の整数)
b: Bの値(0~255の整数)

rgb2hsv

RGBからHSVに変換します。

引数は r, g, b の順、または色コードで渡します。
r = 0~255の整数
g = 0~255の整数
b = 0~255の整数

色コードの場合は16進数6桁、または3桁で渡せます。
例えば3桁で18fと渡した場合は1188ffとして扱います。
11ffや221188ffなど6桁でも3桁でもない場合は、0011ffや1188ff等として扱います。
先頭に'#'は付けても付けなくても構いません。

戻り値はHSVに変換した各値をオブジェクトで返します。
h: Hの値(0以上360未満の浮動小数点数)
s: Sの値(0~1の浮動小数点数)
v: Vの値(0~1の浮動小数点数)

hsv2rgb.js
/**
 *  hsv2rgb (h, s, v)
 */
function hsv2rgb(h, s, v) {
    // 引数処理
    h = (h < 0 ? 360 + h % 360 : h % 360) / 60;
    s = s < 0 ? 0 : s > 1 ? 1 : s;
    v = v < 0 ? 0 : v > 1 ? 1 : v;

    // HSV to RGB 変換
    let c = []
      , a = Math.floor(h)
      , f = h - a;
    c[Math.floor((a + 1) / 2) % 3] = v;
    c[Math.floor((a + 4) / 2) % 3] = v * (1 - s);
    c[(7 - a) % 3] = v * (1 - s * (a % 2 ? f : 1 - f));
    for (let i in c)
        c[i] = Math.round(c[i] * 255);

    // 戻り値
    return {
        hex: '#' + (('00000' + (c[0] << 16 | c[1] << 8 | c[2]).toString(16)).slice(-6)),
        rgb: c,
        r: c[0],
        g: c[1],
        b: c[2],
    };
}
rgb2hsv.js
/**
 *  rgb2hsv (r, g, b)
 *  rgb2hsv (colorcode)
 */
function rgb2hsv(r, g, b) {
    // 引数処理
    let tmp = [r, g, b];
    if (r !== undefined && g === undefined) {
        let cc = parseInt(r.toString().replace(/[^\da-f]/ig, '').replace(/^(.)(.)(.)$/, "$1$1$2$2$3$3"), 16);
        tmp = [(cc & 0xff0000) >> 16, (cc & 0xff00) >> 8, cc & 0xff];
    } else {
        for (let i in tmp)
            tmp[i] = tmp[i] > 255 ? 255 : tmp[i] < 0 ? 0 : Math.floor(tmp[i]);
    }
    r = tmp[0];
    g = tmp[1];
    b = tmp[2];

    // RGB to HSV 変換
    let h = 0
      , s
      , v = Math.max(r, g, b)
      , d = v - Math.min(r, g, b);
    if (s = v ? d / v : 0)
        h = (v === r ? (g - b) / d + (g < b ? 6 : 0) : v === g ? 2 + (b - r) / d : 4 + (r - g) / d) * 60;

    // 戻り値
    return {
        h: h,
        s: s,
        v: v / 255,
    };
}

実行例

// example

let hsv = rgb2hsv('#4f8eac'); // 16進数で指定
//let hsv = rgb2hsv(79, 142, 172); // RGB各値で指定
console.log('rgb2hsv');
console.log(hsv);
console.log(`h = ${hsv.h}`);
console.log(`s = ${hsv.s}`);
console.log(`v = ${hsv.v}`);

let rgb = hsv2rgb(hsv.h, hsv.s, hsv.v);
console.log('hsv2rgb');
console.log(rgb);
console.log(rgb.rgb);
console.log(`hex = ${rgb.hex}`);
console.log(`r = ${rgb.r}`);
console.log(`g = ${rgb.g}`);
console.log(`b = ${rgb.b}`);

結果

result.jpg

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