20191026のJavaScriptに関する記事は25件です。

年末まで毎日webサイトを作り続ける大学生 〜8日目 数字当てゲームを作る〜

はじめに

初めまして。
年末まで毎日webサイトを作っている者です。
今日はMDNのサイトにあった数字当てゲームを作りました。
載ってあるコードは一切見ずに、完成しているものを触っていきなり作り出したのでかなりカオスな状態になっております。。。
ちなみに+@で正解したかどうか画像で判断できるようにしました。
扱う技術レベルは低いですが、同じように悩んでる初心者の方を勇気付けられれば幸いです。
今日は8日目。(2019/10/26)
よろしくお願いします。

サイトURL

やったこと

MDNのサイトに載ってある数字当てゲームをコードを見ずに作りました。
+@で画像で正解したかどうかわかるようにしてあります↓
test.gif

では、かなり汚いコードを載せますのでご注意ください・・・↓

    <script>
        var cNumber = Math.floor(Math.random() * 101);
        var eNumbers = [10];
        var count = 0;
        var yosou = document.getElementById('yosou');
        var button = document.getElementById('button');
        var houkoku = document.getElementById('houkoku');
        var daisyo = document.getElementById('daisyo');
        var textBox1 = document.getElementById('textBox1');
        var flag2;
        var flagImg = document.getElementById('flagImg');

        function imgChange() {
            var mySrc = flagImg.getAttribute('src');
            if (flag2 === true) {
                flagImg.setAttribute('src', 'maru.png');
            } else {
                flagImg.setAttribute('src', 'batu.png');
            };
        };

        function onButtonClick() {
            if (count < 10) {
                count += 1;
                var iNumber = document.forms.form1.textBox1.value;
                eNumbers[count - 1] = iNumber;
                var miNumber = Number(iNumber);
                yosou.textContent += miNumber + " ";
                textBox1.value = "";
                if (count == 10) {
                    button.disabled = true;
                };
                if (miNumber === cNumber) {
                    houkoku.textContent = "おめでとう!正解です!";
                    flag2 = true;
                    daisyo.textContent = "";
                    button.disabled = true;
                } else {
                    houkoku.textContent = "間違いです!";
                    flag2 = false;
                    if (cNumber > miNumber) {
                        daisyo.textContent = "今の予想は小さすぎです!もっと大きな数字です。";
                    } else {
                        daisyo.textContent = "今の予想は大きすぎです!もっと小さな数字です。";
                    };
                };
            };
        };

        function allReset() {
            eNumbers.length = 0;
            count = 0;
            button.disabled = false;
            yosou.textContent = "";
            houkoku.textContent = "";
            daisyo.textContent = "";
        };
    </script>

imgChange関数は正解かどうかで画像を切り替えています。
昨日作ったものと同じ構成です。
変数flag2で正解したかどうか判断していますが、この変数はonButtonClickの中で切り替えています。

感想

動くものを触って中身を判断するのは思ったより大変でした!
最初は「おっ、簡単そうじゃん!」なんて思っていたのですが、実際に作るとなるとやっぱり違いますね。

ちなみにMDNはとてもいいサイトだということが分かったので今後もMDNを中心に勉強していこうと思います。よし、明日も頑張るぞ。
最後までお読みいただきありがとうございます。そして今までグッドを押してくれた皆様、本当に励みになっております。ありがとうございます!

参考

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

補間関数マシンとしてのAI実験  AI Test as an Interpolation Function Machine

補間関数マシンとしてのAI実験  AI Test as an Interpolation Function Machine

ニューラルネットは補間関数だと前に述べた(https://randomwalkjapan.blogspot.com/2019/05/ai-reality-of-booming-ai.html)が、下の自作ツールで試してみよう!Ealier I mentioned that neural net is an interpolation function(https://randomwalkjapan.blogspot.com/2019/05/ai-reality-of-booming-ai.html) so let's try it out in below tool I made!

10個のデータポイントを色々変えると、関数が難しくなるにつれて、ニューロン数、繰り返し回数、中間層の数を増やさないと追従できなくなることが確認できる。If you change 10 data points, as function become complex, you can confirm that interpolation becomes difficult unless you increase number of neurons, epochs, layers.

https://randomwalkjapan.blogspot.com/2019/10/aiai-as-interpolation-machine.html

Program is at
https://github.com/tanakayutaka/Tensorflow.js-tools-/upload/master .

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

React Firebase

忘備録として、、、、

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

Google Apps Scriptでスプレッドシート上の特定条件に合致する行を削除するループ処理

GASを用いてスプレッドシートにあるデータの内、特定の条件がある行のみを削除するループ処理の関数を作成した時のメモです。
カウントアップすると削除時に行ズレが発生するので、カウントダウンさせています。

例:2列目に登録されている時刻データを現在時刻と比較して、古い場合は行を削除します。

ClearRowOnSpreadSheet
function ClearRowOnSpreadSheet(){
  const Now = new Date(); //現在時刻を取得
  const Sheet = SpreadsheetApp.getActiveSheet(); //シートを取得
  const Time_Column = 2; //条件検索の列を指定
  const lastRow = sheet.getLastRow();

  for(var row = lastRow; row > 0; row--){
   var Time = sheet.getRange(row, Time_Column).getValue();
   var TimeDate = Date.parse(startTime);

     if(TimeDate < Now.getTime()){
      sheet.deleteRow(row);
      }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

n件ずつ表示する「もっと見る」ボタンのシンプル実装

私はなぜか「もっと見る」ボタンを実装することがやたらと多いので、いろいろなやり方を試す中で「これが一番シンプル!」と発見した方法を書いておきます。

要件

今回は以下のような要件の「もっと見る」ボタンを実装します。
more_btn.gif

  • 一覧があり、最初は6件表示されている
  • 「もっと見る」ボタンをクリックすると6件ずつ表示される
  • もっと見るものがなくなったら、「もっと見る」ボタンは消える
  • データのロードは最初に全件行う。「もっと見る」をクリックしたときにロードするわけではない
  • したがって、速度を速くする目的ではなく、一覧の量が多いので隠しておきたい時に使えるものを実装する

デモ

デモを作成しましたので以下のリンクから動作確認ができます。
もっと見るボタン - codepen

この「もっと見る」ボタンのおすすめポイント

  • jsがたったの8行!
  • htmlはn件ごとにdivで区切ったりする必要なし
  • 「もっと見る」をクリックして出現する時、ささやかなアニメーションつき

実装のポイント

詳しい実装はデモのcodepenを見てほしいのですが、jQueryでやっていることは下記のような内容です。

  1. 最初の6件以外を非表示にする
  2. 「もっと見る」をクリックしたら、非表示になっている項目の頭から6件だけ表示する
  3. 表示後、非表示の項目が0件の場合は、「もっと見る」ボタンを非表示にする

実装は以下のようになっています。

JavaScript
var moreNum = 6;
$('.list li:nth-child(n + ' + (moreNum + 1) + ')').addClass('is-hidden');
$('.more').on('click', function() {
  $('.list li.is-hidden').slice(0, moreNum).removeClass('is-hidden');
  if ($('.list li.is-hidden').length == 0) {
    $('.more').fadeOut();
  }
});

1. 最初の6件以外を非表示にする

「最初の6件以外」をどう取得するかがポイントですが、cssのnth-childという疑似クラスを使用しました。以下のコードの部分です。

$('.list li:nth-child(n + ' + (moreNum + 1) + ')').addClass('is-hidden');

.list li:nth-child(n + 7)とすることで、「listクラス内のliタグの7番目以降」が取得できます。nth-childは大変便利で、jsでロジックを書かなくてもcssのセレクタだけでn番目、n以前、n以降、最初、最後などが取得できます。

2. 「もっと見る」をクリックしたら、非表示になっている項目の頭から6件だけ表示する

こちらは以下のような実装をしました。

$('.list li.is-hidden').slice(0, moreNum).removeClass('is-hidden');

非表示の要素にはis-hiddenクラスが付与されるので、これをセレクタにします。
6件目までを取得するには配列のsliceメソッドを使用しています。sliceは、配列の0から6までの要素をコピーして新しい配列を返すメソッドです。

最初はnth-childでできないか試してみたのですが、この場合では難しかったです。
li.is-hidden:nth-child(-n + 6)とか
li.is-hidden:nth-of-type(-n + 6)とか
これらはうまくいきません。理由はnth-childnth-of-typeもタグに対して何番目かを選択できるもので、is-hiddenなどのクラス要素内で何番目かは選択できないのでした。

スライドアニメーションの実装方法

アニメーションはcssで実装しています。以下はアニメーション部分のcssの抜粋です。

css
.list li {
    opacity: 1;
    height: 32px;
    margin-top: 10px;
    transition: all 0.4s ease 0s;
}
.list li.is-hidden {
    opacity: 0;
    height: 0;
    margin: 0;
}

.list li {...}に書かれているのが表示状態のスタイルで、
.list li.is-hidden {...}に書かれているのが非表示状態です。
非表示→表示に変わる時の変化の指定をしているのが、transitionプロパティです。transitionはアニメーション関連の設定のショートハンドです。一言で解説すると「0.4秒で変化しろ!」という指定となっています!

「非表示のスタイルはdisplay:none;でいいんじゃないの?」と思う方もいるかもしれませんが、ダメなのです。
transitionできるプロパティとできないプロパティがあるためです。そのため、非表示→表示や表示→非表示にアニメーションを入れたい場合、opacity(透明度)をよく使っています。fadeIn・fadeOutの効果が出せておすすめです。
今回はheightにも変化をつけたため、「fadeInしながらスライド」みたいなアニメーションになりました。

この実装の問題点

初回表示時に全件表示されたあと7件目以降が隠れるところのが一瞬見えてしまいます。もっと良い実装をするなら、アニメーション時だけ特定のクラスを付与すると良いのですが…それはまたの機会に…。
気になる場合はアニメーションをやめると良いと思います。

まとめ

css疑似クラスのnth-childやjs配列のsliceメソッドを使って、n番目以降やn番目以前を取得する方法をご紹介しました。こういうテクを知っていればごりごりロジックを書く必要がなくなって便利ですね!

参考記事

何番目系の便利なCSSまとめ
【CSS】nth-childとnth-of-typeでもう混乱しない。
【CSS3】Transition(変化)関連のまとめ

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

たった3行のJavaScriptでRedmineの項目削除時の「よろしいですか?」問題を解決した件

初めに

Ankosoftの山崎です。
redmine.tokyo の第17回勉強会まで後1週間と迫っていますので、少しでもRedmineを盛り上げられればと思い、RedmineのUI改善のお話をしたいと思います。

Redmineで管理者権限があるユーザーが「管理」ページで「ユーザー」、「グループ」、「ロール」、「トラッカー」、「チケットのステータス」、「カスタムフィールド」、「文書カテゴリ」、「チケットの優先度」、「作業分類 (時間管理)」で項目を削除することができます。

しかし下図のように項目名と削除のボタンがかなり離れているため、削除したい項目の削除ボタンをクリックしたつもりが、他の項目の削除ボタンを誤ってクリックしてしまう可能性があります。(実際に何度か誤って削除してしまったことがあり、対応に苦労しました)

alt

誤って項目が削除されることを防止するユーザーの操作性の向上のためにアラートがポップアップする仕様になっていますが、「よろしいですか?」というメッセージでは「OK」をクリックした場合に何が削除されるのか分からないので、ユーザーとしては不安感が残るUIになっていました。

さすがにプロジェクトが誤って削除されたら、問題の影響が大きいので、「ユーザー」、「グループ」などとは異なり、削除をクリックすると、ページが遷移して、プロジェクト名が明記された状態でユーザーに最終意思確認をしています。

alt

改善した削除UI

下図のように「削除」をクリックすると、【「項目名」を削除してもよろしいですか?】というメッセージを表示します。
これにより、正しい項目を削除することができ、削除の最後の確認段階で自分が意図した項目であるかどうかを確認することができます。

alt

alt

作成したJavaScriptコード

htmlソースを確認すると、削除ボタンをクリックすると、「data-confirm」に記載されている「よろしいですか?」がポップアップアラートのメッセージとして表示されることが分かります。
alt

強制的にJavaScriptで「data-confirm」に「【項目名】+削除しても」を追加してポップアップアラートのメッセージに項目名を表示させます。

qiita.rb
$('tr').each(function(i, elem){
    $(elem).find("a:last").data('confirm',""+$(elem).find("a:first").text()+"」を削除しても"+$(elem).find("a:last").data('confirm'))
});

簡単にコードの説明をすると
$('tr').each(function(i, elem){の部分で全てのtrタグに対してループ処理をしています。

$(elem).find("a:last").data('confirm',"の部分で、trタグ配下の一番最後のaタグの「data-confirm」に下記の2つの値を設定しています。

"「"+$(elem).find("a:first").text()+"」を削除しても"の部分で、項目名の取得

+$(elem).find("a:last").data('confirm')の部分で元々の「data-confirm」の値、ここでは「よろしいですか?」を取得

これにより、「data-confirm」の値が「よろしいですか?」から「【項目名】+削除してもよろしいですか?」に変更されます。

これにより、削除ボタンをクリックすると「【項目名】+削除してもよろしいですか?」が表示されます。

上記のJavaScriptをview customize pluginに登録して使えばOKです。

ちなみに、このコードで「ユーザー」、「グループ」、「ロール」、「トラッカー」、「チケットのステータス」、「カスタムフィールド」、「文書カテゴリ」、「チケットの優先度」、「作業分類 (時間管理)」全ての削除ボタンで項目名を表示させることができます。

参考情報

ではでは、来週のredmine.tokyo の第17回勉強会で皆様とお会いできるのを楽しみにしております。

JavaScriptでUIを改善した記事です。
たった1行のJavaScriptコードでRedmine UI (カスタムフィールドのトラッカー名の表示)を改善する方法

RedmineのガントチャートUIを大幅に改善したANKOガントチャートの紹介ページ
ANKOガントチャート

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

VanillaJS - 初心者こそ、Docker を使おう! 

初心者こそ、Dockerを使おうの 素のjavascript 編です。

多分一番ボリュームがあります。
* 素のJSという割には、Parcel, Pug, TypeScript,等を使っていますのでここでは、Vue.js や React 等を使わないでという意味で解釈ください。

このTodoは、JavaScript Primer さんのTodo を参考にしています。
class を使って実践的なTodoが学べますので、ぜひ覗いてみてください.
* https://jsprimer.net/use-case/todoapp

  • fileの構成予定
├── docker-compose.yml
└── src
    ├── App.ts
    ├── EventEmitter.ts
    ├── TodoItemModel.ts
    ├── dist
    ├── html-util.ts
    ├── index.pug
    ├── index.ts
    ├── package.json
    ├── style.stylus
    └── yarn.lock
  • 同じディレクトリで作業をするなら、データのコピーはもう済んでいるので docker-compose.ymlworking_dir, command#を差し替えると、docker-compose up -d でバックグラウンドで Parcel がたちあがるはずです。 docker-compose ps で確認してみてください. バックグラウンドのコンテナに入るコマンドは docker-compose exec node bash です

00

docker-compose.yml

version: "3"
services:
  node:
    container_name: node
    image: atoris1192/node:0.1.5
    # build: .
    # volumes は上書きに注意
    volumes:
      - .:/app
    ports:
      - "1234:1234"
      - "1235:1235"
    # working_dir: /app
    working_dir: /app/src
    # command: cp -rp /tmp/src /app
    command: npx parcel --hmr-port 1235 --hmr-hostname localhost index.pug
    tty: true

src/index.pug

<!DOCTYPE html>
html(lang="en")
  head
    meta(charset="UTF-8")
    meta(name="viewport", content="width=device-width, initial-scale=1.0")
    meta(http-equiv="X-UA-Compatible", content="ie=edge")
    title Document
    link(rel="stylesheet", href="style.stylus")
  body
    .todoapp
      form#js-form(action="")
        input#js-form-input.new-todo(type="text" placeholder="new todo" autocomplete="off")

      #js-todo-list.todo-list

      footer.footer
        span#js-todo-count TodoItems: 0
    script(src="./index.ts")

index.ts

import { App } from './App';

const app = new App()
document.addEventListener('DOMContentLoaded', () => {
  app.main();
})

App.ts

export class App {
  constructor() {
    console.log("App init ...");
  }
  main() {
    console.log("App ...");
  }
}

style.stylus

html,
body {
    margin: 0;
    padding: 0;
}

button {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-size: 100%;
    vertical-align: baseline;
}

body {
    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    line-height: 1.4em;
    background: #f5f5f5;
    color: #4d4d4d;
    min-width: 230px;
    max-width: 550px;
    margin: 0 auto;
    font-weight: 300;
}

:focus {
    outline: 0;
}

.todoapp {
    background: #fff;
    margin: 130px 0 40px 0;
    position: relative;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
    0 25px 50px 0 rgba(0, 0, 0, 0.1);
    min-height: 150px;
}


.todoapp input::placeholder {
    font-style: italic;
    font-weight: 300;
}

.new-todo {
    position: relative;
    margin: 0;
    width: 100%;
    font-size: 24px;
    line-height: 1.4em;
    border: 0;
    color: inherit;
    box-sizing: border-box;
    padding: 16px 16px 16px 60px;
    border: none;
    background: rgba(0, 0, 0, 0.003);
    box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}

.todo-list ul {
    margin: 0;
    padding: 0;
    list-style: none;
}

.todo-list li {
    position: relative;
    font-size: 24px;
    border-bottom: 1px solid #ededed;
    padding: 16px;
}

.todo-list li:last-child {
    border-bottom: none;
}

.todo-list li input[type="checkbox"] {
    width: 40px;
    height: auto;
    margin: auto 0;
    border: none;
}

.todo-list li .delete {
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    width: 40px;
    height: 40px;
    color: #cc9a9a;
}

.footer {
    color: #777;
    height: 20px;
    padding: 10px 15px;
    border-top: 1px solid #e6e6e6;
}

Parcel 確認

  • docker-compose.yml を書き換えている人は、もうすでに Parel が立ち上がっているので以下は必要ありません。
docker-compose up
docker-comppse ps
docker-compose run --service-port node bash
uname
cd src
npx parcel --hmr-port 1235 --hmr-hostname localhost index.pug

描画処理 App.ts に書いていきます

App.ts

記述は、html 文字列で書いて、処理は、htmlエレメントがほしいので、とりあえず関数化htmlElement()しておきます。

処理は、 main 関数に書いていきます。 描画は render に分けておきます

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

export class App {
  constructor() {

  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');

    function htmlElement(todos) {
      const ul = document.createElement('ul');
      const template = document.createElement('template');

      todos.forEach( todo => {
        const li = `<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
        template.innerHTML = li;
        ul.appendChild(template.content.firstElementChild)
      })
      return ul;
    }
    // 配列 -> htmlエレメント
    const todosElement = htmlElement(todos);

    jsTodoList.textContent = '';
    jsTodoList.appendChild(todosElement);

  }
  main() {
    console.log("App ...");

    this.render();

  }
}

input 処理

App.ts

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

export class App {
  constructor() {
    console.log("App init ...");
  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');

    function htmlElement(todos) {
      const ul = document.createElement('ul');
      const template = document.createElement('template');

      todos.forEach( todo => {
        const li = `<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
        template.innerHTML = li;
        ul.appendChild(template.content.firstElementChild)
      })
      return ul;
    }
    // 配列 -> htmlエレメント
    const todosElement = htmlElement(todos);

    jsTodoList.textContent = '';
    jsTodoList.appendChild(todosElement);

  }
  main() {
    // input 処理
    const jsForm = document.querySelector('#js-form');

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');

      interface Item {
        id: number;
        title: string;
        isDone: boolean;
      }

      const item: Item = {
        id: new Date().getTime(),
        title: jsFormInput.value,
        isDone: false,
      }

      todos.push(item);
      jsFormInput.value = '';
      this.render()
    })


    this.render();

  }
}

HTMLエレメント処理を、外部ファイルにする

javascript の html 処理は、実に面倒くさいのですが、JavaScript Primerさんのhtml-utilをそのまま使わせていただきます。

タグ付きテンプレート文字列を使って、スマートに変換してくれます。
おまけにエスケープ処理までしてくれますので、これを使わない理由がありません。
詳しい使い方、JavaScript Primerさんをチェックしてください。
* element<li>${ todo.id }</li> この文字列タグをhtmlエレメントタグに変換してくれます。

これを使って書き換えます

html-util.ts

function escapeSpecialChars(str: string) {
  return str
    .replace(/&/g, "&apm:")
    .replace(/</g, "&lt;")
    .replace(/>/g, "%gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

// htmlString -> htmlElement DOM Node
function htmlToElement(html: string): any {
  const template = document.createElement('template') ;
  template.innerHTML = html;
  return template.content.firstElementChild;
}

// escape + DOM Node
export function element(strings, ...values) {
  const htmlString = strings.reduce((result, str, i) => {
    const value = values[i - 1];
    if (typeof value === "string") {
      return result + escapeSpecialChars(value) + str;
    } else {
      return result + String(value) + str;
    }
  })
  return htmlToElement(htmlString);
}

// Add child Element
export function render(bodyElement, containerElement) {
  containerElement.innerHTML = ''; // 一旦全削除
  containerElement.appendChild(bodyElement);
}

  • コード量は、今はあまり変わりませんが、エスケープ処理もしてくれていますし、htmlエレメントを意識しなくてよくなるので書きやすくなります。

App.ts

import { element, render } from './html-util';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

export class App {
  constructor() {
    console.log("App init ...");
  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`

    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })

    render(ul, jsTodoList);
    // jsTodoList.textContent = '';
    // jsTodoList.appendChild(ul);

  }
  main() {
    // input 処理
    const jsForm = document.querySelector('#js-form');

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');

      interface Item {
        id: number;
        title: string;
        isDone: boolean;
      }

      const item: Item = {
        id: new Date().getTime(),
        title: jsFormInput.value,
        isDone: false,
      }

      todos.push(item);
      jsFormInput.value = '';
      this.render()
    })


    this.render();

  }
}

data 処理用クラスを作る todoLiseModel

todo データを切ったり貼ったりするメソッドは、すべてtodoListModel に集約させたいと思います。

App.ts

import { element, render } from './html-util';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]
class TodoListModel {  // todo data 処理用
  private todos: any;
  constructor(todos = []) {
    this.todos = todos // 初期化
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCount() {
    return this.todos.length;
  }
  addTodo({ title }) {
    interface Item {
      id: number;
      title: string;
      isDone: boolean;
    }
    const item: Item = {
      id: new Date().getTime(),
      title: title,
      isDone: false,
    }
    this.todos.push(item);
  }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos) // インスタンス作成 仮データセット
  }

  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');

    // input 処理
    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo({
        title: jsFormInput.value,
      })
      jsFormInput.value = '';
      this.render()
    })

    this.render();
  }
}

データの定義クラスを作る todoItemModel

todoListModel で todoデータの処理を担当してもらいましたが、定義もそのなかに含まれています。
それを新たなクラスで管理したいと思います
これで、機能の分担化ができました。

App.ts

import { element, render } from './html-util';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

class TodoItemModel { // item 定義用
  private id: number;
  private title: string;
  private isDone: boolean;
  constructor({ title }) {
    this.id = new Date().getTime()
    this.title = title
    this.isDone = false
  }
}

class TodoListModel {  // todo data 処理用
  private todos: any;
  constructor(todos = []) {
    this.todos = todos // 初期化
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCoount() {
    this.todos.length;
  }
  addTodo( item ) {
    this.todos.push(item);
  }
  // addTodo({ title }) {
  //   interface Item {
  //     id: number;
  //     title: string;
  //     isDone: boolean;
  //   }
  //   const item: Item = {
  //     id: new Date().getTime(),
  //     title: title,
  //     isDone: false,
  //   }
  //   this.todos.push(item);
  // }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos) // インスタンス作成 仮データセット
    // this.todoListModel_2 = new TodoListModel() // インスタンスなのでいくつでも必要個数作成できる
  }

  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');

    // input 処理
    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo( new TodoItemModel({ // インスタンス作成 使い捨てです。
        title: jsFormInput.value,
      }))
      // this.todoListModel.addTodo({
      //   title: jsFormInput.value,
      // })
      jsFormInput.value = '';
      this.render()
    })

    this.render();
  }
}

独自イベント を作る

これは、予め用意されたイベント以外に、独自にイベントの発行とリスナーを作ります。
用意されたイベントというのは、ボタンをクリックしたらとか、チェックボックスの状態が変わったらなど条件検知で命令を実行するイベントリスナーを自分で作ってしまいます。
今回は、todo データの状態が変わったら、実行するというイベント用になります。
差し替えるメソッドは、this.render() と差し替える形をとりますが、このイベントリスナーは、今回でいうとそんなに恩恵があるわけではありません。 this.render() で特に不便はないのですがイベント検知の考え方は、より汎用性の高い使い方ができそうです。 イメージで言えば、ライトが着いたらとか、温度が規定温度になったら実行するというようなイベントを検知した時点で実行されるものなので、応用が効くと思われます。

これも、拝借してきたものをそのまま使います。
中身をみると、Map関数にイベント用のキーと関数を入れる器が用意されているだけです。
これを、TodoLiistModel の親要素のして継承させます。
そうすることによって、TodoListModel にはメソッドを書かなくても使えるようになります。

src/EventEmitter.ts

export class EventEmitter {
  private _listeners: any;
  constructor() {
    // イベント名、リスナー関数が入る
    this._listeners = new Map();
  }
  addEventListener(type:string, listener:any) {
    if (!this._listeners.has(type)) {
      this._listeners.set(type, new Set());
    }
    const listenerSet = this._listeners.get(type);
    listenerSet.add(listener);
  }
  emit(type:string) {
    const listenerSet = this._listeners.get(type)
    if (!listenerSet) {
      return;
    }
    listenerSet.forEach(listener => {
      listener.call(this);
    })
  }
  removeEventListener(type:string, listener:any) {
    const listenerSet = this._listeners.get(type);
    if (!listenerSet) {
      return;
    }
    listenerSet.forEach(ownListener => {
      if (ownListener === listener) {
        listenerSet.delete(listener);
      }
    })
  }
}

src/App.ts

import { element, render } from './html-util';
import { TodoItemModel } from './TodoItemModel';
import { EventEmitter } from './EventEmitter';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

// EventEmitter を 追加
class TodoListModel extends EventEmitter {  // todo data 処理用
  private todos: any[];
  constructor(todos = []) {
    super();
    this.todos = todos // 初期化
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCoount() {
    this.todos.length;
  }
  addTodo( item ) {
    this.todos.push(item);
  }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos) // インスタンス作成 仮データセット
  }

  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');

    // change イベントリスナー関数
    this.todoListModel.addEventListener('change', this.render)

    // input 処理
    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo( new TodoItemModel({ // インスタンス作成
        title: jsFormInput.value,
      }))
      jsFormInput.value = '';
      this.todoListModel.emit('change'); // changeイベントエミッター
      // this.render()
    })
    this.todoListModel.emit('change'); // changeイベントエミッター
    // this.render();
  }
}
  • これは、もう動きがないので別ファイルにして置きます。

src/TodoItemModel.ts

export class TodoItemModel { // item 定義用
  private id: number;
  private title: string;
  private isDone: boolean;
  constructor({ title }) {
    this.id = new Date().getTime()
    this.title = title
    this.isDone = false
  }
}

App.ts をリファクタリングしてmain関数を整理しておきます

import { element, render } from './html-util';
import { TodoItemModel } from './TodoItemModel';
import { EventEmitter } from './EventEmitter';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

class TodoListModel extends EventEmitter {  
  private todos: any[];
  constructor(todos = []) {
    super();
    this.todos = todos
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCount() {
    return this.todos.length;
  }
  addTodo( item ) {
    this.todos.push(item);
  }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos)
  }
  totalCount(totalCount) {
    const jsTodoCount = document.querySelector('#js-todo-count');
    jsTodoCount.textContent = `TodoItems: ${ totalCount }`
  }
  inputTodo() {
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo( new TodoItemModel({
        title: jsFormInput.value,
      }))
      jsFormInput.value = '';
      this.todoListModel.emit('change');
  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');

    this.todoListModel.addEventListener('change', this.render)  // リスナーは複数立てれる
    this.todoListModel.addEventListener('change', () => {
      const totalCount = this.todoListModel.getTotalCount();
      this.totalCount(totalCount)
    })

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      this.inputTodo();
    });
    this.todoListModel.emit('change');
  }
}

後は、削除や、チェックボックス, パージ処理を書いていきます

App.ts

import { element, render } from './html-util';
import { TodoItemModel } from './TodoItemModel';
import { EventEmitter } from './EventEmitter';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

class TodoListModel extends EventEmitter {  
  private todos: any[];
  constructor(todos = []) {
    super();
    this.todos = todos
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCount() {
    return this.todos.length;
  }
  addTodo( item ) {
    this.todos.push(item);
  }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos)
  }
  purge() { // 完了分全削除
    const todos = this.todoListModel.getTodos();
    const newTodos = todos.filter( todo => {
      return !todo.isDone
    });
    this.todoListModel.setTodos(newTodos);
    this.todoListModel.emit('change');
  }
  totalCount() {
    const jsTodoCount = document.querySelector('#js-todo-count');
    const totalCount = this.todoListModel.getTotalCount()
    jsTodoCount.textContent = `TodoItems: ${ totalCount }`
  }
  inputTodo() {
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo( new TodoItemModel({
        title: jsFormInput.value,
      }))
      jsFormInput.value = '';
      this.todoListModel.emit('change');
  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const todos = this.todoListModel.getTodos()

    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = todo.isDone
        ? element`<li><input type="checkbox" class="checkbox" checked/><del>${ todo.title } : ${ todo.isDone } : ${ todo.id}</del><button class="delete">[x]</button></li>`
        : element`<li><input type="checkbox" class="checkbox" />${ todo.title } : ${ todo.isDone } : ${ todo.id}<button class="delete">[x]</button></li>`

      // チェックボックス状態
      const checkboxState = li.querySelector('.checkbox');
      checkboxState.addEventListener('change', () => {
       const todos = this.todoListModel.getTodos();
       const item = todos.find( item => {
         return item.id === todo.id;
       })
       item.isDone = !item.isDone
       this.todoListModel.emit('change');
      })

      // 削除処理
      const deleteBtn = li.querySelector('.delete');
      deleteBtn.addEventListener('click', () => {
       const todos = this.todoListModel.getTodos();
       const pos = todos.map( todo => {
         return todo.id
       }).indexOf(todo.id);
       todos.splice(pos, 1)
       this.todoListModel.emit('change');
      })

      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');
    const purge = document.querySelector('#purge');

    purge.addEventListener('click', () => {
      this.purge();
    })
    // changeリスナー関数
    this.todoListModel.addEventListener('change', () => {
      this.render();
    })  
    this.todoListModel.addEventListener('change', () => { // 分ける必要は無いが、説明の為分けている
      this.totalCount()
    })

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      this.inputTodo();
    });
    this.todoListModel.emit('change');
  }
}

FireStore

このままでは、リロードでデータが消えてしまうので、FireStoreに接続させます

詳しくは、vue.js編のデータの永続化の投稿を参照ください。

firebase関係のエラーがでていたら、コンテナに入って


uname
cd src
yarn add Firebase
npx parcel --hmr-port 1235 --hmr-hostname localhost index.pug


ポートマッピングで二重起動していると失敗しますので、注意してください。

App.ts

import { element, render } from './html-util';
import { TodoItemModel } from './TodoItemModel';
import { EventEmitter } from './EventEmitter';
import * as firebase from 'firebase/app';
import 'firebase/firestore';
import { config } from '../firebase';

firebase.initializeApp(config);
const db = firebase.firestore()
const collection = db.collection('todos')


class TodoListModel extends EventEmitter {  
  private todos: any[];
  constructor(todos = []) {
    super();
    this.todos = todos
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCount() {
    return this.todos.length;
  }
  async addTodo({ title }) {
    interface Item  {
      id: number;
      titel: string;
      isDone: boolean;
      created_at: any;
    }
    const item = {
      id: new Date().getTime(),
      title: title,
      isDone: false,
      created_at: firebase.firestore.FieldValue.serverTimestamp(),
    }
    await collection.add(item)
      .then(result => {
        console.log(result);
      })
      .catch(err => console.log(err)
      )
  }
  // addTodo( item ) {
  //   this.todos.push(item);
  // }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel()
  }
  async purge() { // 完了分全削除

    // const todos = this.todoListModel.getTodos();
    // const newTodos = todos.filter( todo => {
    //   return !todo.isDone
    // });
    // this.todoListModel.setTodos(newTodos);
    await collection.where('isDone', '==', true)
      .get()
      .then(snapshot => {
        snapshot.forEach( doc => {
          collection.doc(doc.id).delete();
        })
      })
      .catch(err => console.log(err))
    this.todoListModel.emit('change');
  }
  totalCount() {
    const jsTodoCount = document.querySelector('#js-todo-count');
    const totalCount = this.todoListModel.getTotalCount()
    jsTodoCount.textContent = `TodoItems: ${ totalCount }`
  }
  inputTodo() {
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo({
        title: jsFormInput.value,
      });
      // this.todoListModel.addTodo( new TodoItemModel({
      //   title: jsFormInput.value,
      // }))
      jsFormInput.value = '';
      this.todoListModel.emit('change');
  }
  async render() {
    const fs_data = await collection.orderBy("created_at", "desc").get();
    const fs_dataItems = fs_data.docs.map( items => {
      return({
        dbId: items.id,
        id: items.data().id,
        title: items.data().title,
        isDone: items.data().isDone,
        created_at: items.data().created_at,
      })
    })
    this.todoListModel.setTodos(fs_dataItems);

    const jsTodoList = document.querySelector('#js-todo-list');
    const jsTodoCount = document.querySelector('#js-todo-count');
    const todos = this.todoListModel.getTodos()
    const totalCount = this.todoListModel.getTotalCount()
    jsTodoCount.textContent = `TodoItems: ${ totalCount }`

    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = todo.isDone
        ? element`<li><input type="checkbox" class="checkbox" checked/><del>${ todo.title } : ${ todo.isDone } : ${ todo.id}</del><button class="delete">[x]</button></li>`
        : element`<li><input type="checkbox" class="checkbox" />${ todo.title } : ${ todo.isDone } : ${ todo.id}<button class="delete">[x]</button></li>`

      // チェックボックス状態
      const checkboxState = li.querySelector('.checkbox');
      checkboxState.addEventListener('change', async() => {
      //  const todos = this.todoListModel.getTodos();
      //  const item = todos.find( item => {
      //    return item.id === todo.id;
      //  })
      //  item.isDone = !item.isDone
        await collection.doc(todo.dbId).update({
          isDone: !todo.isDone
        })
       this.todoListModel.emit('change');
      })

      // 削除処理
      const deleteBtn = li.querySelector('.delete');
      deleteBtn.addEventListener('click', async() => {

      //  const todos = this.todoListModel.getTodos();
      //  const pos = todos.map( todo => {
      //    return todo.id
      //  }).indexOf(todo.id);
      //  todos.splice(pos, 1)

       await collection.doc(todo.dbId).delete();
       this.todoListModel.emit('change');
      })

      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');
    const purge = document.querySelector('#purge');

    purge.addEventListener('click', () => {
      this.purge();
    })
    // changeリスナー関数
    this.todoListModel.addEventListener('change', () => {
      this.render();
    })  

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      this.inputTodo();
    });
    this.todoListModel.emit('change');
  }
}

Vue.js や React.js を使わないとどれだけ苦労するのかを、試してみましたが 確かに記述は増えてhtmlの処理は面倒くさくなるものの。
書いているのは、一番楽しいという結果になりました。(笑)
生JS が一番練習になると思いますので、ツールを使わないとどうなるかの検証もいいかなと思います。

見てくれた方ありがとうございました。m(_ _)m

以上、Docker + 終了です。

一応、git hub に UP しておきます。
https://github.com/atoris1192/docker-todos-vanilla

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

jQueryを使って遷移先リンクを別タブで開く

リンクをクリックした時、別タブで開く処理をjQueryを使って
書きたいことがあったので書き留めておきます。

script.js
$('.hogehoge').click(function(){
   $url = $(this).attr('href');
   window.open($url,'_blank');
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

100日チャレンジの137日目

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

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

137日目は、

とりあえず、Udemyでお試しで英語教材をやってみた感じ。
翻訳機能がないとなにいっているかわかりませんが、コードはなにやりたいか読めますので、特に不便はありませんでした。

javascriptでだいじょうぶなら、他の言語でも英語教材やってみるのもいいかもしれません。

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

【備忘録⑤】React & TypeScript & Webpack4 & Babel7 & dev-server の最小構成ボイラープレートの作成 -Jestの導入-

前回の記事 で作成したボイラープレートにJestを導入する。

各種設定

モジュールの追加

今回は Jest と Enzyme をインストールする。

$ npm install -D jest ts-jest enzyme enzyme-to-json enzyme-adapter-react-16


TypeScript を使用するので以下もインストールする。

$ npm install -D @types/jest @types/enzyme @types/enzyme-adapter-react-16

Jest設定ファイルの作成

Jestの設定ファイルを追加する。

// <プロジェクトルート>/jest.config.js

module.exports = {
  // 起点となるディレクトリを指定
  "roots": [
    "./src"
  ],
  // アセットの変換方法を指定
  "transform": {
    "^.+\\.tsx?$": "ts-jest"
  },
  // Jestで実行するテストコードの配置場所とテストコードの正規表現での指定
  // __tests__下の hoge.test.tsx または hoge.spec.tsx を対象とする
  "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
  // テスト対象となる拡張子を列挙
  "moduleFileExtensions": [
    "ts",
    "tsx",
    "js",
    "jsx",
    "json",
    "node"
  ],
  // Enzymeの設定
  snapshotSerializers: ['enzyme-to-json/serializer'],
  // Enzymeの設定ファイル(自分で作成する)
  setupFilesAfterEnv: ['./test/setupEnzyme.ts'],
};


上記で設定した__tests__ディレクトリを作成しておく。

$ mkdir ./src/__tests__

Enzyme設定ファイルの作成

今回は、Enzymeを使用するので、こちらも設定ファイルを用意する。

// <プロジェクトルート>/test/setupEnzyme.ts

import { configure } from 'enzyme';
import * as EnzymeAdapter from 'enzyme-adapter-react-16';
configure({ adapter: new EnzymeAdapter() });

tsconfig.jsonを作成

TypeScriptの設定ファイルを用意する。

{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "module": "es2015",   common.jsも設定できるが、静的解析の精度が落ちるのでes2015おすすめらしい
    "target": "es5",
    "jsx": "react",
    "esModuleInterop": true
  },
  "include": [
    "./src/**/*"
  ]
}

package.jsonにJest実行用設定を追加

package.jsonにJest実行用設定を追加する。

  "scripts": {
    "start": "webpack-dev-server --open",
    "build": "webpack",
    "build-prod": "webpack --mode=production",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook",
    "test": "jest"  ←ここ追加
  },

以上で、実行環境の構築は完了。

動作確認

確認用コンポーネント作成

前回作成したコンポーネントを使用する。

// <プロジェクトルート>/src/components/Button/index.tsx

import * as React from 'react';

export interface IButtonProps {
  text: string;
  flag?: boolean;
  action(): void;
}

const Button = (props: IButtonProps) => {
  const { text, flag, action } = props;
  return (
    <React.Fragment>
      { flag && <p>{text}</p> }
      <button onClick={ action }>Button</button>
    </React.Fragment>
  );
};

export default Button;

テストファイル作成

__tests__内にコンポーネントと同じディレクトリ構造を作成する。

$ mkdir ./src/__tests__/components/Button

テストコードを作成する。

// <プロジェクトルート>/src/__tests__/components/Button/index.test.tsx

import * as React from 'react';
import {shallow} from 'enzyme';
import Button from '../../../components/Button';

test('小コンポーネントが存在すること', () => {
  const wrapper = shallow(<Button text="ボタンです" flag={true} action={() => console.log("test")} />);

  expect(wrapper.find("button").length).toBe(1);
  expect(wrapper.find("p").length).toBe(1);

  expect(wrapper.find("p").text()).toEqual("ボタンです");

  expect(wrapper).toMatchSnapshot();
});

test('pコンポーネントが表示されないこと', () => {
  const wrapper = shallow(<Button text="ボタンです" flag={false} action={() => console.log("test")} />);

  expect(wrapper.find("button").length).toBe(1);
  expect(wrapper.find("p").length).toBe(0);

  expect(wrapper).toMatchSnapshot();
});

test('イベント発火時にコールバック関数が呼び出されること', () => {
  const Spy = jest.fn();
  const wrapper= shallow(
    <Button text="ボタンです" flag={true} action={Spy} />
  );

  wrapper.find('button').simulate('click');
  expect(Spy).toHaveBeenCalledWith();

  expect(wrapper).toMatchSnapshot();
});


実行

コンソールからテストコマンドを実行する。
実行すると、以下のようにテスト結果が出力される。

$ npm run test

> react-ts-webpack@1.0.0 test /Users/kento/Programing/VScodeProjects/ts-react-sass-simple-boiler-v4
> jest

 PASS  src/__tests__/components/Button/index.test.tsx
  ✓ 小コンポーネントが存在すること (33ms)
  ✓ pコンポーネントが表示されないこと (3ms)
  ✓ イベント発火時にコールバック関数が呼び出されること (6ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   3 passed, 3 total
Time:        2.182s, estimated 5s
Ran all test suites.


テスト実行後、__tests__ディレクトリ内に、__snapshots__ディレクトリが作成され、中にスナップショットファイルが作成される。

$ tree ./src/__tests__
./src/__tests__
└── components
    └── Button
        ├── __snapshots__
        │   └── index.test.tsx.snap
        └── index.test.tsx



index.test.tsx.snapの中身は以下の通り。

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`pコンポーネントが表示されないこと 1`] = `
<Fragment>
  <button
    onClick={[Function]}
  >
    Button
  </button>
</Fragment>
`;

exports[`イベント発火時にコールバック関数が呼び出されること 1`] = `
<Fragment>
  <p>
    ボタンです
  </p>
  <button
    onClick={
      [MockFunction] {
        "calls": Array [
          Array [],
        ],
        "results": Array [
          Object {
            "type": "return",
            "value": undefined,
          },
        ],
      }
    }
  >
    Button
  </button>
</Fragment>
`;

exports[`小コンポーネントが存在すること 1`] = `
<Fragment>
  <p>
    ボタンです
  </p>
  <button
    onClick={[Function]}
  >
    Button
  </button>
</Fragment>
`;



以上で、Jestの導入は完了。
作成したボイラープレートは こちら

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

customizable-gitmoji-cliで、gitのコミットメッセージに絵文字を付ける

はじめに

gitmoji文化というものがあります。

以下は私のプロジェクトにおけるコミット履歴です。

image.png

参照:https://github.com/SnO2WMaN/customizable-gitmoji-cli/commits/master

なぜ?

いろいろあると思いますが、私は以下の理由です。

コミットの粒度を上げる

特技はgit commit -a -m いろいろ修正です!
参照:横着で神経質な私とあなたに贈るgit add -p

私は放っておくとこれになっちゃうので、なんとなくカテゴライズしておくことで目標立てて開発をするようにしました。 1

Conventional Commitsの環境整えるの面倒になってきた

Conventional Commitsというものがあります。(こっちのほうがスタンダードだとは思います、少なくともJavascript / Node.js / フロントエンドサイドのプロジェクトではそうだと思います)

大体次のようなコミットメッセージです。

fix: Fix Something
chore(deps): Update dependencies.

これは自動でバージョン更新したりするときなどに有用なんですが、いかんせんCLIの整備やいろいろ付け加えた結果設定ファイルがモリモリ増えちゃって何が何なのかわからなくなっちゃいました。

gitmojiでは、同作者によるCLI(gitmoji-cli)が用意されていて、導入も簡単だったので良かったです。

参考

一方

gitmoji-cliはたしかに便利だったんですが、本家のgitmojiのみしか使えなかったので、プロジェクト下において明らかに不必要な絵文字があったり、逆に痒い所の為の絵文字が無かったりしてそこはそこで不便でした。

そこで今回、カスタマイズ可能なcustomizable-gitmoji-cliを作成しました。

customizable-gitmoji-cli

オプションの詳細などはREADME-jp.mdを読んでもらえると助かります。

このCLIでは、.gitmojircなどの設定ファイル2によって自由に絵文字を追加することが出来ます。また、ESLint/Prettier/Stylelint/etc...の共有設定ファイルのようにプレセットを読み込む機能もあります。

.gitmojirc
{
  "emojiFormat": "emoji",
  "presets": ["@sno2wman/gitmoji-preset"],
  "rules": [
    {
      "emoji": "?",
      "description": "Everything must go!!",
      "name": "clown_face"
    }
  ],
}

この設定ファイルをプロジェクトに含んでおけば、CLIからコミットをする限り、全員が同じコミット規則に従ってコミットが行われるはずです。

emojiFormat

コミット時に添付される絵文字のフォーマットを選択できます。
emoji, codeのどちらかを設定することが出来ます。

  • emoji
    • ?, ?, ?
    • ? Kill process
  • code
    • :ok_hand:, :dog:, :knife:
    • :knife: Kill process (Github上などでは絵文字が表示されます)

私はemojiに設定しています。(たまに各種サービスやCI上では:knife:と表示されて見栄えが悪いので…)

rules

以下の形式でgitmojiを定義してください。

{
  "emoji": "?",
  "description": "Feel good.",
  "name": "gorilla"
}

ikatyang/emoji-cheat-sheetが役に立つはずです。

presets

まだ本家のgitmojiに対応するプレセットのgitmoji-preset-baseと、私個人の@sno2wman/gitmoji-presetしかありません…
名前解決の方法は以下のとおりです。

  • @username/gitmoji-preset
    • そのまま読み込みます
  • gitmoji-preset-foo
    • そのまま読み込みます
  • bar
    • gitmoji-preset-barの形にして読み込みます。

gitmoji-preset-baseを除く全てのプレセットは、プロジェクト直下にnpm i/yarn addしてインストールされている必要があります。

展望

  • #3
    • プレセット側で設定することで、コミット履歴より自動でバージョン更新の判断などが出来るようにしたい。

おわりに 3

gitmojiが様々な場面で使われることを祈っています。

よかったらプロジェクトにスターをしてください。励みになります。

貢献をしてくれるともっと喜びます。


  1. 個人の感想です。 

  2. 設定ファイルはcosmiconfigに対応しています。つまり.gitmojirc, .gitmojirc.jsonなどの形式が対応しています。 

  3. 記事内で絵文字を使わない所が良くないと思う。 

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

JSON形式のAPIレスポンス構造をNode.jsのREPLで理解する

この記事の対象者

JSON形式のレスポンスをJavaSciptで処理する際に、

  • どういう書き方で書けば欲しい情報が取り出せるのか知りたい
  • JSONレスポンスが大きすぎて構造がよくわからない
  • APIリクエスト制限が厳しくて何度もリクエストを投げられない
  • 参考記事には結果のコードしか載ってなくて初見のAPIをどうやればいいのか

ということがある場合。JSON形式のAPIの扱いに慣れていない人向けです。

どうやって理解するのか

一旦APIレスポンスをJSONファイルとして出力した後にNode.jsのREPLで読み込んで、対話形式で解析します。
一度JSONファイルとして書き込むことで何度もAPIリクエストを投げる必要がないのでAPIリクエスト制限に引っかかる心配がありません。

やり方

例としてGitHub APIにて react で検索した時の結果をAPIで取得します。
https://developer.github.com/v3/search/#search-repositories

ターミナルで下記のコマンドを入力します。
JSON形式のデータ整形に jq というツールを使っているので無い場合はコマンド brew install jq で入れてください。

$ curl https://api.github.com/search/repositories?q=react | jq .

ターミナルを埋め尽くすほどのAPIレスポンスが帰ってきました。
この状態だと何が何だか分からないので一回JSONファイルに出力します。

$ curl https://api.github.com/search/repositories?q=react | jq . > result.json
result.json
{
  "total_count": 1054700,
  "incomplete_results": false,
  "items": [
    {
      "id": 10270250,
      "node_id": "MDEwOlJlcG9zaXRvcnkxMDI3MDI1MA==",
      "name": "react",
      "full_name": "facebook/react",
      "private": false,
      "owner": {
        "login": "facebook",
        "id": 69631,
        ...
      }
      ...
    }
  ]
}

ざっくり構造が見えてきました。まずトップ階層に検索件数が表示され、具体的な中身は items の中に Object[] として格納されているので、items[n] または map 関数で取り出せそうだなという目安がつきます。

これからNode.jsのREPLでデータを取り出してみます。

$ node
Welcome to Node.js v12.3.0.
Type ".help" for more information.
> const data = require("./result")
> data
... // Object形式で出力

JSONファイルは require で読み込むことでJavaScriptの Object として読み込まれます。

Object形式なので下記のように入力するとデータを取り出すことができます。

> data.total_count
1054700

ここで items の中から full_name のみ取り出してみます。

> data.items.map(item => item.full_name)
[
  'facebook/react',
  'duxianwei520/react',
  'reactphp/react',
  ...
  'reduxjs/react-redux'
]

条件を絞ってみます。 獲得スターが30k以上 のリポジトリの full_name のみ取り出してみます。

> data.items.filter(item => item.stargazers_count >= 30000).map(item => item.full_name)
[
  'facebook/react',
  'facebook/react-native',
  'ReactTraining/react-router',
  'zeit/next.js',
  'facebook/create-react-app',
  'enaqx/awesome-react'
]

data.items.filter(item => item.stargazers_count >= 30000) では条件に合致する Object[] が帰ってくるのでそれを map 関数で取り出しました。

これを実際のAPIレスポンスを扱うスクリプトに適応するとこんな感じです。

const axios = require("axios");

const http = axios.create({
  baseURL: "https://api.github.com"
});

async function main() {
  try {
    const response = await http.get("/search/repositories", {
      params: {
        q: "react"
      }
    });
    const data = await response.data
    const result = await data.items.filter(item => item.stargazers_count >= 30000)
    console.log(`30000 over stars Repository search by 'react'`);
    result.map(item => console.log(item.full_name))
  } catch (error) {
    console.log(error)
  }
}

main();

$ node index.js 
30000 over stars Repository search by 'react'
facebook/react
facebook/react-native
ReactTraining/react-router
zeit/next.js
facebook/create-react-app
enaqx/awesome-react

まとめ

これで初見のJSON形式のAPIでも対応できると思います。
REPLはレスポンス構造を解析するだけでなく構文のチェックも簡単にできるのでド忘れしやすい僕は多用してます。

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

Javascript基礎

javascript1
<!doctype html>
<html lang="ja">
<head>
 <meta charset="utf-8">
</head>

<div id="id1">新宿</div>
<div id="id2"></div>

<script>
// alert(1);

// 変数 可変の値の入れ物
const value = '東京';
console.log(value);

// 配列 変数に複数の値を設置できる。前から順に0、1、2・・・とインデックスがつけられる
const items = ['東京', '埼玉', '神奈川'];

// for文。配列をループし展開できる
for (let i = 0; i < items.length; i++) {
  console.log(i, items[i]);
}

// for文の別の書き方 値を取得
for (const item of items) {
  console.log(item);
}

// for文の別の書き方 インデックスを取得
for (const i in items) {
  console.log(i);
}

// if文 
for (const i in items) {
  if (i == 1) {
   console.log('if', items[i]);
  } else if (i == 2){
   console.log('else if', items[i]);
  } else {
   console.log('else', items[i]);
  }
}

// オブジェクト
const obj = {prefectureId: 1, city: [{1: '新宿'}, {2: '池袋'}]};
const prefecture = {1: '東京'};

console.log(obj);

for (const i in obj) {
  // 配列かどうか
  if (Array.isArray(obj[i])) {
    for (const city of obj[i]) {
      // オブジェクトのキーを取得
      const cityId = Object.keys(city);
      console.log(city[cityId]);
    }
  } else {
    console.log(prefecture[obj[i]]);
  }
}

const id1 = document.getElementById('id1').innerHTML;
document.getElementById('id2').innerHTML = id1 + '-池袋';

</script>

</body>
</html>

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

TypeScriptでタプル型をインターセクションに変換する

結論

// https://qiita.com/suin/items/93eb9c328ee404fdfabc
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

declare const boxedBrand: unique symbol;
type Boxed<T> = { [boxedBrand]: T };
type BoxedValueOf<T> = T extends Boxed<infer U> ? U : never;

type TupleToBoxedTuple<T> = { [K in keyof T]: Boxed<T[K]> };
type TupleToBoxedUnion<T> = TupleToBoxedTuple<T>[keyof T];
type TupleToIntersection<T> = BoxedValueOf<UnionToIntersection<TupleToBoxedUnion<T>>>;

type A = TupleToIntersection<['hoge', string]>;
// A = "hoge"

解説

型変換は、調べると「タプルからユニオン」とか「ユニオンからインターセクション」は出てくるのですが、「タプルからインターセクション」に変換する方法はなかなか見つかりません。
(ググラビリティの問題?)

いやでも待てよと、「タプルからユニオン」「ユニオンからインターセクション」に連続して変換すればタプルからインターセクションが出てくるじゃん!と思った私は騙されました。
次のような場合に想定しないことになってしまいます。

type A = UnionToIntersection<TupleToUnion<[string, 'hoge']>>;
// A = string
// 実際に得られる型

type B = string & 'hoge';
// B = "hoge"
// 本来ほしいのはこの型

なぜならば、一度'hoge' | stringというユニオンに変換された時点でstringという型になってしまうからです。
'hoge'0といったリテラル型は、string型やnumber型のようなより広い型に吸収されてしまうようです。

そこでなんとかリテラル型が吸収されないようにしながらユニオンに変換し、そのあとにインターセクションに変換するという手順を踏みます。

まずは、次のような型を用意します。

declare const boxedBrand: unique symbol;
type Boxed<T> = { [boxedBrand]: T };

これは型をラップするための型で、こいつを使うとユニオンを用いても型が吸収されるのを防げます。

type A = Boxed<'hoge'> | Boxed<string>;
// A = Boxed<"hoge"> | Boxed<string>

Boxed<T>型からT型を取り出すための型も用意しておきます。

type BoxedValueOf<T> = T extends Boxed<infer U> ? U : never;

type A = BoxedValueOf<Boxed<'hoge'>>;
// A = "hoge"

ポイントはこのBoxedValueOf<T>で、もはやネタバラシではありますが次のような型の取り出しが可能です。

type A = BoxedValueOf<Boxed<{ hoge: true }> & Boxed<{ fuga: true }>>
// A = {
//   hoge: true;
// } & {
//   fuga: true;
// }

Boxed<A> & Boxed<B>という型に対してBoxedValueOf<T>を適用すると、A & Bというインターセクションが得られます。

すなわち、

「タプルからユニオン」「ユニオンからインターセクション」

このフローではなく「タプルからBoxedユニオン」「BoxedユニオンからBoxedインターセクション」「Boxedインターセクションから生のインターセクション」という流れで型を変換することで、求めていた「タプルからインターセクション」の型変換を行うことができます。

まとめると、次のようなコードで「タプルからインターセクション」の型変換が可能です。

// https://qiita.com/suin/items/93eb9c328ee404fdfabc
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

declare const boxedBrand: unique symbol;
type Boxed<T> = { [boxedBrand]: T };
type BoxedValueOf<T> = T extends Boxed<infer U> ? U : never;

type TupleToBoxedTuple<T> = { [K in keyof T]: Boxed<T[K]> };
type TupleToBoxedUnion<T> = TupleToBoxedTuple<T>[keyof T];
type TupleToIntersection<T> = BoxedValueOf<UnionToIntersection<TupleToBoxedUnion<T>>>;

type A = TupleToIntersection<['hoge', string]>;
// A = "hoge"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】明示的に型変換をする!

一般的な型変換よりは冗長になるかもしれませんが、確実に型変換する方法を紹介します。

STEP1 : new 演算子を使って変換

JavaScriptでは new Numbernew String といった方法で文字列や数値を生成することができます。

構文

わざわざ載せる必要はないと思いますが一応

example.js
new String(10); //文字列の10が返る。厳密にはString {"10"}が返る。
new Number("10"); //数値の10が返る。厳密にはNumber {10}が返る。

/* その他の new Number の挙動 */
new Number({hoge:"piyo"}); //NaNが返る。
new Number("fuga"); //NaNが返る。
new Number(undefined); //NaNが返る。

new Number の注意点

new Number(null);

これは 0 が返ります。null 値を一緒に扱う場合はハマりそうなポイントでもあるので注意してください。

STEP2 : 生成したインスタンスを値に変換

new String とか new Number で生成されたインスタンス1厳密に言うとオブジェクトです。
(STEP1の例で「厳密にはString {"10"}が返る。」とか意味のわからないことを書いたのはそのためです)

そのため、 typeof などを使うと Object という結果が返ってきます。
やはり、それでは困ります。

じゃあ、どうするかというと・・・「valueOf」メソッドを利用して値に変換します。

valueOf はオブジェクトのプリミディブ値2を返すメソッドです。

new Number("10"); //Number {10} が返る。
new Number("10").valueOf(); //10が返る。

これを利用して、以下のようにするとインスタンスを数値や文字列に変換することができます。

let hoge = new Number("10"); //ここではまだNumber {10}
hoge = hoge.valueOf(); //ここで変換
console.log(hoge); //「数値」の10が出力される

valueOfを使わなくてもいい例

new Number に限りますが、new Number のインスタンスを演算子のオペランド3として扱う場合は自動的に valueOf の処理がされるので、絶対にSTEP2を踏まないといけないということはありません。

変換完了

この2ステップだけで簡単に型変換ができたと思います。
よく使う場合だと下のように関数化すると楽です。

function convertToString(value){
    return new String(value).valueOf();
}
function convertToNumber(value){
    return new Number(value).valueOf();
}

実行速度への影響

実行速度を簡易的に比較しました。
特に環境を整えたわけではないし、ソースコードも対照実験できるようなものではないので、参考程度にどうぞ。

実験方法

コード1
const hoge = "1";
new Number(hoge).valueOf();
コード2
const hoge = "1";
hoge - 0;
コード3
const hoge = "1";
function convertToNumber(value){
    return new Number(value).valueOf();
}
convertToNumber(hoge);

この3つのコードを console.time("hoge");console.timeEnd("hoge"); に挟んで処理時間を比較する。

実験環境

Microsoft Edge 79.0.313.0 (公式ビルド) canary (64 ビット) です。
ユーザーエージェント : Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.0 Safari/537.36 Edg/79.0.313.0

結果

コード1 コード2 コード3
1回目 0.012939453125ms 0.010986328125ms 0.13671875ms
2回目 0.014892578125ms 0.009033203125ms 0.070068359375ms
3回目 0.022216796875ms 0.01220703125ms 0.0869140625ms
4回目 0.015869140625ms 0.01806640625ms 0.095947265625ms
5回目 0.01708984375ms 0.009033203125ms 0.08935546875ms

となりました。

コード1 コード2 コード3
最早 0.012939453125ms 0.009033203125ms 0.070068359375ms
最遅 0.022216796875ms 0.01806640625ms 0.13671875ms

やはり、コード2が一番早いですね。
コード1はまだしもコード3は際立って遅いという結果になりました。

まとめ

よく使われる hoge - 0 見たいな方法よりは確実で、エラーも出にくい方法ですが、やはり遅いといえば遅いですね。
どちらを使うかはケースbyケースですね。大抵 hoge - 0 でいいよね


  1. インスタンス newで生成された実体のことです。 

  2. プリミディブ値 語弊はありますが、「値そのもの」などと解釈すればいいです。正しく知りたい人は個人で検索してください。 

  3. オペランド 式の演算子ではないほう。変数や数値のことです。 

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

HCJCODE - Webコーディングベース

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

JavaScript Primerを読んだ自分用ノート

この書籍について · JavaScript Primer #jsprimer を読んでJavaScriptを勉強したメモです。当然、原文を読んだ方が理解できます。この記事では自分が覚えたいところ、気を付けたいところだけメモしていきます。既に習得済みと私が認識している箇所は飛ばされる可能性があります。

私のスキルですが

  • powershellの扱いには手馴れている
    • WebAPI叩いてコントロール、データをクラウドDBに保管したり、は出来る
    • 変数、配列…などプログラミングの基本的な考えはOK
    • オブジェクト指向はうまく使えていない
  • JavaScriptは多少触っている
    • 入りがnode.js
    • コードをコピペして修正は出来る

です。

JavaScript実行環境

  • Webブラウザのコンソール画面で実行する
    • chromeならF12、firefoxならCtrl + Shift + K
  • index.htmlからindex.jsを呼び、実行する
  • consoleAPIを利用することで、Webブラウザのコンソールにprintデバッグできる
    • console.log("aaa")

基礎

  • 変数は大文字と小文字が区別される
    • 大文字と小文字をまぜて書くのはエラーのもと
  • 文の末尾はセミコロン(;)をつける
    • 最近はつけなくても動作するのでコーディングスタイル次第
  • 先頭に"use strict";をつけて、strictモードを有効にする

変数定義・型

  • 定数はconst、変数はletを使う
    • constで宣言した変数は文字列を大文字にしてletを視覚的に区別をするテクニックがある
    • カンマで変数をつなげて宣言する手にクックもある
      • count count01 = 1, count02 = 2 , count03 = 3;
      • let count11 = 1, count12 , count13;
  • 型を調べたい時はconsole.log(typeof 42);などtypeofを使う
  • 文字列はシングルクォート、ダブルクォートのどちらで括っても違いはない
  • バッククォートで区切ると改行を認識できる。また${str}で文字列中に変数を埋め込むことができる
  • オブジェクト:const obj = {};
  • 配列:const arr = [];
  • 正規表現:const regexp = //;

条件

  • 比較の時は===または!==を利用する。=== → 厳密等価演算子。
  • 三項演算子を使うこともできる:const valueA = true ? "A" : "B"; console.log(valueA);
  • switchはbreakが必要。ifの代わりに使うのは可読性の面で良くない。

ループ

  • ループ構文:while,do-while,for,for-of(powershellのforeachと同等)
  • ループ構文(メソッド):foreach,some,filter
  • 制御構文:break,continue,return

関数

難しかったので詳しくメモ

用語

// 関数宣言
function 関数名(仮引数1, 仮引数2) {
    // 関数を呼び出された時の処理
    // ...
    return 関数の返り値;
}
// 関数呼び出し
const 関数の結果 = 関数名(引数1, 引数2);
console.log(関数の結果); // => 関数の返り値
// 関数式
const 関数名 = function() {
    // 関数を呼び出した時の処理
    // ...
    return 関数の返り値;
};
  • 関数宣言
    • 関数を文として宣言。
    • const myfunc = fu;のような形で、文として宣言した関数を値にすることが出来る。関数式と同等。
  • 関数式
    • 関数を値として定義。const = 値と同じ形
  • 仮引数
    • カンマ区切りで複数定義できる
    • 呼び出しの時に指定がない場合、undefinedとなる
    • undefinedの時に設定する引数であるデフォルト引数を定義できる
    • 残余引数(...args)を指定すると、全ての引数を受け取れる
  • 関数の返り値
    • return文で定義。文が実行されると後続処理は実行されない。
    • return文はなくてもOK。

関数式と匿名関数とアロー関数

先ほどの説明では関数宣言と関数式を同等に覚える存在として扱ったが、近年のJavaScriptでは関数式を理解する重要度が増している。理由としては

  • 非同期処理を実現する時に頻繁に利用する
  • 関数宣言は多言語と同じ機能だが、関数式はJavaScript専用の理解が必要

だと思われる。

関数式と匿名関数

以下が標準的な関数式であるが、この関数式では関数名 = 変数名であり、本来関数名を定義する"この部分で関数名を宣言"は省略されている。

// 関数式
const 関数名 = function この部分で関数名を宣言() {
    // 関数を呼び出した時の処理
    // ...
    return 関数の返り値;
};

理由は、関数を利用したい場合は、関数名 = 変数名を利用すればよく、本来の関数名を宣言する必要がないため、省略可能である(※)。これを無名関数、匿名関数という。

※ 関数の中で関数を呼び出す場合は必要

アロー関数

関数式をさらに省略して記載できるのがアロー関数である。"アロー関数"と書かれているが記法だけの違いであり、機能としては関数式と同等だが、記載のしやすさ、見通しのよさより、関数式を利用する場合は、こちらの書き方が標準的になっている。

// 通常の関数式
const 関数名 = function() {
    // 関数を呼び出した時の処理
    // ...
    return 関数の返り値;
};

// Arrow Functionを使った関数定義
const 関数名 = () => {
    // 関数を呼び出した時の処理
    // ...
    return 関数の返す値;
};

以下の記法を利用し、さらにこれを省略して記載できる。

  • 引数記載の省略
    • 関数の仮引数が1つのときは()を省略できる
  • 関数記載の省略
    • 関数の処理が1つの式である場合に、ブロックとreturn文を省略できる(その式の評価結果をreturnの返り値とする)
// 引数記載の省略(以下の記載は同等の意味)
const fnA = (x) => {  };
const fnA = x   => {  };

// 関数記載の省略
const mulA = x => { return x * x; };
const mulB = x => x * x;

スコープ

  • ブロックスコープ
    • {}の間に定義した変数は、そのスコープか下位のスコープでしか使えない
    • 自身のスコープより上位で定義された変数がある場合、それを利用できる
    • ただし、自身のスコープで同名の変数を定義した場合、その値が優先される(スコープチェーンの考え方)
  • グローバルスコープ
    • スクリプトに素で定義した定数は、最上位のスコープで定義しているため、全てのスコープで利用できる
    • バグの混入を防ぐため、近年のプログラミングのセオリーでは最低限のスコープに対して変数を定義する事が推奨される
    • よって、特に理由がない限り、グローバルスコープに対して変数を定義すべきではない
  • クロージャー
    • スコープチェーンと静的スコープを利用して、関数に状態を持たせるテクニック

巻き上げ

どこで変数を宣言しても、スクリプトの0行目で宣言したのと同じことになる動作。varと関数宣言で動作する。便利なようで、varの場合は巻き上げによる動作が予測しづらいため、varを使うのが非推奨である理由の一つ。関数宣言はむしろ便利に使える。

thisについて

  • function関数で使われるthisは呼び出し方によって参照する値が違い、動的である。混乱の元。
  • アロー関数で使われるthisは常に1つ外側のオブジェクトを参照するため、静的である。直感的に使え、コードの見通しがよい。
  • よってthisはアロー関数でしか使わないか、全く使わない方がよい。

function関数とthisの必要可否についてはイマドキのJavaScriptの書き方2018 - Qiitaでも議論されているので、上級者視点でもメリットデメリットがありそう。

オブジェクト関係(オブジェクト、文字列、配列)

全てがオブジェクト、と考えればよい。文字列や配列も親となるクラスはオブジェクト。文字列や配列でできる事はpowershellと大差ない。

  • プロパティ名に変数を指定したい場合はプラケット記法([xxx])
  • 正規表現に変数を使用したい場合は、リテラル(/xxx/)ではなく、RegExpコンストラクタを使う
  • 存在しない属性はundefinedが返ってくるが文字列と区別がつかないため、inやhasOwnPropertyを使う

非同期処理とコールバック

一番ムズい所なのでここでは(この短文では)纏められない。いったん理解したことにして先に進む。

例外処理

  • try
  • catch
  • finally
  • throw
    • Errorオブジェクトで投げる
  • console.logとconsole.errorを使い分ける

Map/Set

  • Map:いわゆるハッシュ、連想配列
  • Set:重複した値を持たない配列。配列と違って順序の概念がないため、インデックスによるアクセスはできない。

JSON

  • Json.parse:文字列としてのJSONをJavaScriptのオブジェクトにする
  • Json.stringify:オブジェクトをJSON文字列に変換する

Date(日付時刻)

Dateが標準モジュールだが、時間を進める戻すなどできないので、他のライブラリも使って実現する事(moment.jsなど)

ECMAScriptモジュール

  • 他のJSから他のJSに変数や関数をエクスポートできる。
  • エクスポート:export [エクスポートする内容]
  • インポート:import [インポートする内容] from "ファイル名"

その他メモ

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

【ESLint】eslint-plugin-lodash-templateがJavaScriptテンプレートをサポートしたよ

かなり前に、【ESLint】eslint-plugin-lodash-template作ってみたよ という記事を書きましたが、今回、この eslint-plugin-lodash-template を拡張したのでその思い出を残します。

eslint-plugin-lodash-templateとは

Lodashの_.templateに渡して使えるテンプレートタグ内のJavaScriptに対してESLintで検証できるようにするESLintのプラグインです。

つまり、次のようなテンプレートでエラー検出ができるようになるESLintのプラグインです。

image.png

WEBで試す

JavaScriptテンプレートをサポートしました

(実験的機能という体で)JavaScriptを生成するテンプレートをサポートしました。
何を行っているかと言いますと、次のようなテンプレートでESLintかけられるようにしました。

const obj   = <%= JSON.stringify(options) %>

<% for (const key of Object.  keys(additionals)) { %>
    obj[ <%= key %>] =<%= additionals[key] %>
<% } %>

export default obj

image.png

具体的には、以前まではテンプレートタグ(<%...%>)の内側のJavaScriptだけ検証していましたが、今回のアップデートでテンプレートタグ(<%...%>)をひっぺがした外側のJavaScriptを検証できるようになりました :tada:

WEBで試す

使用方法

事前準備

ESLintを利用できる環境を整えてください。ここではESLintがすでに利用できる状態からの利用方法を書きます。

インストール

eslint-plugin-lodash-templateをインストールしましょう。

npm install --save-dev eslint-plugin-lodash-template

いないと思いますが、すでにインストールされている場合は最新版に更新してください。(ただし破壊的変更があるので注意してください)

設定

.eslintrc.*の設定ファイルに下記のいずれかを追記します。

  • 簡単設定版
    プラグインが持っているいくつかのルールも有効になります。
{
    // ...
    "overrides": [
        {
            "files": ["**/your/templates/*.js"],  // あなたのテンプレートファイルを指すように設定します
            "extends": [
                "plugin:lodash-template/recommended-with-script"  // 簡単設定
            ],
        }
    ]
    // ...
}
  • カスタマイズ設定版
    プラグインが持っているルールを有効にしたい場合は自分で"rules"に追加してください。
{
    // ...
    "overrides": [
        {
            "files": ["**/your/templates/*.js"],  // あなたのテンプレートファイルを指すように設定します
            "extends": [
                "plugin:lodash-template/base"  // eslint-plugin-lodash-templateが有効になります。
            ],
            "processor": "lodash-template/script", // JavaScriptテンプレートモードを有効にします。
            "rules": {
                // ... 好きなルールを追加してください。
            }
        }
    ]
    // ...
}

で動くと思います。

JavaScriptを生成するテンプレートの拡張子が.jsではない場合は、実行時にその拡張子を含めるようにしたり、エディタの設定が必要な場合があります。
また、以前からeslint-plugin-lodash-templateがサポートしている普通のHTMLテンプレートで使いたい場合は、別途、設定が必要です。

詳しくはドキュメントを参照してください。

JavaScriptテンプレートを検証する仕組み

僕がこの記事で一番書きたかった内容です。

複数のJavaScriptに分解して検証する

eslint-plugin-lodash-templateではテンプレートタグ(<%...%>)の内側のJavaScriptと外側のJavaScriptをESLintにかけるために、別々に複数のJavaScriptを作ってESLintで検証して、エラー結果をマージして出力するということをしています。

つまり次の例では、

const obj   = <%= JSON.stringify(options) %>

<% for (const key of Object.  keys(additionals)) { %>
    obj[ <%= key %>] =<%= additionals[key] %>
<% } %>

export default obj

次のようなJavaScriptを作り出します。

  • テンプレートタグ(<%...%>)の内側のJavaScript
                 JSON.stringify(options);  

   for (const key of Object.  keys(additionals)) {   
             key;         additionals[key];  
   }   


  • テンプレートタグ(<%...%>)の外側のJavaScript
const obj   = _hogehoge_hash1

/* コメントにする                              */
    obj[ _hogehoge_hash2] =_hogehoge_hash3
/* コメントにする */

export default obj

_hogehoge_hash*って書いた部分は実際はテンプレートタグのハッシュ値です。

と、このように複数のJavaScriptを作っています。

条件分岐で壊れるJavaScriptをサポートする

ところで、先ほどからずっと「複数」のJavaScriptと表現していますが、これまでの説明だとテンプレートタグの内側と外側のJavaScriptで2つだけですね。
なぜ「複数」と表現しているかといいますと、条件分岐で壊れるJavaScriptをサポートするために外側のJavaScriptはテンプレートタグ(<%...%>)の条件分によって複数作成します。

例えば、次のようなテンプレートがあった場合、

<% if (flag) { %>
    const obj = <%= JSON.stringify(optionsA) %>
<% } else { %>
    const obj = <%= JSON.stringify(optionsB) %>
<% } %>

export default obj

単純にテンプレートタグ(<%...%>)の外側のJavaScriptを作ると、

/* ... */
    const obj = _hash1
/* ... */
    const obj = _hash2
/* ... */

export default obj

となりますが、このJavaScriptはパースエラーです。const obj二つ目があるよと怒られるだけで終わってしまします。
なので、この問題を解決するために、テンプレートタグに条件分岐が含まれる場合は、全てのPathを通るようにJavaScriptを複数作成しています。

上の例だと、

/* ... */
    const obj = _hash1
/* ... */

/* ... */

export default obj

/* ... */

/* ... */
    const obj = _hash2
/* ... */

export default obj

の2つのJavaScriptを作ります。

ESLintプラグインでこの仕組みを作るには

ESLintにはProcessorっていうプラグインのための仕組みがありまして、例えば.html.mdで複数のJavaScriptを分解して、あとでエラーメッセージをマージするというのができるような仕組みです。バッチリ今回の用途にも合いそうな仕組みですね。
しかしこれも一筋縄ではいかず、ちょっと変わった使い方をしました。

分解したJavaScriptを渡して、エラー結果をマージするには、エラー結果の出現箇所を復元する必要があります。
しかし、これがなかなか難しく、例えば、行の最大文字数の検出は、ソースコード自体が違う場合にマージするなんてできませんね。

この問題を無理やり解決するために、Processorの前処理では、幾つのJavaScriptの処理が必要かどうかだけ計算して、同じ元のソースコードを複数個返し、Parserでパースするタイミングで、書き換えたJavaScriptでASTを構築して、エラー結果をもらうという対応をしました。

おかげでマージは楽になったので、Processorの後処理で、メッセージ重複を排除したり、適当に作ったハッシュが気に入らないと怒られる部分を除外したりするだけです。

残る不安

と、このような仕組みで作ってはみたもののいくつかの不安が残っています。

  1. テンプレートタグ(<%...%>)の外側JavaScript生成の時に、補間(<%=...%>)の部分にハッシュ値入れてるのですが、これで色々なテンプレートで問題なくパースできるJavaScriptが作れるのかどうかはかなり不安です。。
  2. 条件分岐があった場合に分岐をパス網羅するようにJavaScriptを作っているのですが、これで色々なテンプレートで問題なくパースできるJavaScriptが作れるのかどうかはかなり不安です。。

なので、実験的機能ということにしています。

あとがき

僕、実は、JavaScriptを生成するテンプレートってプロダクションで書いたことないんですけどね。
なんでこれ対応したかというとnuxt-communityで使いたいみたいなissueが来てちょっと面白そうだったからやってみたという感じです。実際、使えるかはまだわからないですけど。

もし、JavaScriptを生成するテンプレートを Micro-Template とか Lodash template とか Underscore template とか EJS とかで書いている方はぜひ使ってみていただけると嬉しいです。

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

GASのWEBアプリケーションでUAをスプレッドシートにロギングして簡易アクセスログ

三行で

  • UiApp.getUserAgent()(UiAPP)は廃止されHtmlService.getUserAgent()
  • IPはアクセス時点では取得できそうにない
  • スプレッドシートの年日でgroup byする方法が不明

アクセスみたい

ウェブアプリケーションとして公開したapps scriptのアクセスは、
doGetの自分以外による実行数を見ればぼんやりとわかります。

gas.JPG

でももうちょっとユニーク数を詳しく知りたいときが出たりするので、手軽な識別方法はないかなと考えました。

調べてみると、
GoogleAppsScriptでのあれやこれや - gstudio - blog
UserAgentを取得できるようなので、これでUAのユニーク数が最低限の利用者数だということができそうです。
短時間の連続実行も別々の人なのか同一の人のリロードなのかもなんとなくわかりますしね。

というわけでvar app = UiApp.createApplication();を追加しましたが動きません。

UiAppは廃止されたのでHtmlServiceを使うようです

Class HtmlService | Apps Script | Google Developers#getUserAgent()

というわけでこんな風に

function doGet() {
  const ua = HtmlService.getUserAgent();
  if (ua) {
    var as = getSheet(ACCESS_LOG_SHEET_NAME);
    as.appendRow([Utilities.formatDate(new Date(),  'Asia/Tokyo', 'yyyy年MM月dd日 HH:mm:ss'), ua]);
  }
  var htmlOutput = HtmlService.createTemplateFromFile("template").evaluate();
  htmlOutput.setTitle(WEB_TITLE);
  return htmlOutput;
}

レスポンスに悪影響がでる書き方ですが、スプレッドシートが短い現在は目立った処理速度の低下は見られませんでした。

ifにしてるのはエディターから直接実行した場合などを考えてますが、特に必要ではないはず。

問題点としては、ヘッダーとして公開しているウェブアプリか(/exec)、最新バージョンでの動作の確認か(/dev)見分けが付いてないということでしょうか。
解決策はあるかもしれませんが、些細な問題なので現状スルーで。

gas2.JPG

UAのユニーク数チェックには別シートに
=QUERY('アクセスログ'!B:B,ʺselect count(B), B group by Bʺ)
とすると何種類のUAがあるのかがわかります。

できれば日ごとのアクセス数も集計したいのですが、日付フィールドの時間を切り捨てて年月日だけで集計する方法がわからず…SQLの不勉強さがたたっていますね…

IPは?

Retrieve Public IP From Google Web App - Stack Overflow

doGetの中で取得する方法は見つからなかったので、クライアント側で取得してdoPostに投げてロギングする方法などが考えられますね。
そこまでするならAnalyticsを乗っけたほうが早い気がしますが、doGetがすばやくなるのでもろもろをクライアント側とdoPostでやっちゃうことは覚えていてもいいかなと。
非同期で処理できますし。


使ってみたら意外とモバイルからのアクセスがあったりでそっちの動作確認していないなと気づきを得られたりでよかったです。

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

【図解】コールスタックとクロージャーを理解する

スコープ

ある変数のスコープとは、その変数にアクセスできるコード上の範囲を意味する。

var で宣言された変数は function scope であるのに対し、letconst で宣言された変数は block scope と呼ばれるスコープを持っている。

function scope の変数は、自分を含む一番内側の関数がスコープとなる。一方 block scope の変数は、自分が属する一番内側の関数orブロックがスコープとなる。ブロックとは { 処理;処理;... } のことである。

var let scope

Lexical Environment (LE)

JavaScript の実行環境はプログラムを実行する上で「どの変数が何の値を持っているか」というテーブルのようなものを保持しながらコードを一行一行実行する。ただし、変数はスコープというものがあり、スコープの外からはアクセスすることはできない。また、変数を参照するとき、同じ名前の変数が複数あればより内側のスコープに存在するものが優先される(シャドウイング)。

このような性質を実現するために、JavaScript ではテーブルのチェーン構造によって変数を管理している。このテーブル一つ一つを Lexical Environment と呼び、変数の名前と値のペアを格納する。

仕様での定義

Execution Context Stack

Call Stack とも呼ばれるが、これは「今実行中の関数はどの関数から呼び出されて、その関数はどの関数から呼び出されて...」という履歴のようなものを格納するスタック構造である。

Execution Context Stack の要素は Lexical Environment1 である。

仕様での定義

実行の流れ

例えば以下のコードを考えよう。

const x = 1

if (true) {
  const y = x + 1
  const z = f(y)
}

function f(x) {
  return x * 2
}

まずは、トップレベル(一番外側のスコープ)の変数のための LE が作られる。画像右の四角が LE を表す。この時点では変数に値は入っていない(初期化されていない)。関数宣言は hoisting (巻き上げ) が起こるので、既に変数 f の値(関数)は入っている。

context1.png

1行目の右辺が計算され、x に代入される。

context2.png

if の条件式が true なのでブロックの中に入り、新しい LE が作られる

context3.png

y の値が計算される。

context4.png

次に、f 関数が呼ばれる。関数の中に入るときも LE が作られる

緑の LE が 青の LE ではなく赤の LE につながっているが、これは関数 f の外側のスコープが赤の LE だからである。

実引数として渡した y の値 2 が、f の仮引数 x の値となる。

context5.png

x * 2 を計算するときに x が参照されるが、このとき赤の x ではなく緑の x の値が使われることになる。どのように x を見つけているかというと、Call Stack の先頭(図では一番下)の LE からスタートして、矢印でつながったチェーンをたどって一番最初に見つかった変数が選ばれるのである。そのため、緑の LE をまず探して、そこになければ赤の LE を探すことになるが、今の場合緑の LE に x があるのでそれが選ばれる。変数のシャドーイングはこのようにして発生するのである。

よって x の値は 2 なので 4return される。

context6.png

関数を抜け、緑の LE は Execution Stack から削除される。

戻り値 4z に代入される。

context7.png

ブロックを抜け、赤の LE は Execution Stack から削除され、このまま実行が終了する。

context8.png

クロージャー

クロージャーとは外側のスコープに存在する変数を参照する関数のことである。例えば下のコードでは increment 関数がクロージャーである。

const c1 = counter()
const c2 = counter()

console.log(c1())
console.log(c1())
console.log(c1())

console.log(c2())
console.log(c2())

function counter() {
  let x = 0

  function increment() {
    x += 1
    return x
  }

  return increment
}

実行すると以下が出力される。

1
2
3
1
2

counter 関数を呼び出すたびに、別々のカウンターが作成されていることが分かるだろう。つまり、c1c2 は別々の変数 x を持っている。これも、LE で説明することができる。

c1c2 が代入された時点では、LE と Call Stack は次のようになっている。c1c2 はどちらも increment 関数の関数オブジェクトだが、別々の LE(緑) を指していることが分かる。これは、関数が呼び出されるたびに LE が作られるからである。

closure1

c1 が実行されると、その関数オブジェクトが指している LE (左の緑の LE) の下に新しい LE が作られ、そこで increment 関数が実行される。

closure2.png

ここで x += 1 が実行されるわけだが、最初の例で説明したとおり、当然この x は左の緑の LE の x を指すことになる。これにより左の緑の LE の x1 になり、右の緑の LE は影響を受けない。つまり、独立した2つの状態変数となっている。

ところで、この2つの x は対応する increment 関数からしかアクセスできない、いわば「隠れた変数」となっている。最近になって JavaScript にプライベートメンバー変数の構文が追加されたが、それまではプライベート変数はクロージャーでしか実現できなかった。


  1. 仕様では Execution Context Stack の要素は Execution Context であるが、ここでは Execution Context と Lexical Environment を同一視している。 

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

【Ruby on Rails】Google Map APIの導入

はじめに

つくるもの

Google Map APIを導入しページに地図を表示させます。
今回は検索した住所にピンを立て、そこの緯度経度を表示させるまでを実装します。

gmap-step0.gif

コードを書く前に

Google Maps API を利用するには、API キーという許可証のようなものを取得する必要があります。
API キーを取得する為にGoogle アカウントが必要になるので、事前に準備しておきましょう。

Google アカウントの作成はこちらから

そしてGoogle Cloud Platformへアクセスして、APIキーを取得しましょう。

プロジェクトの作成

gmap-01.png

gmap-02.png

gamp-03.png

gmap-04.png

APIの有効化

gmap-05.png

gamp-06.png

APIキーの作成

gmap-07.png

gmap-08.png

gmap-09.png

gmap-10.png

gmap-11.png

gmap-12.png

gmap-13.png

gmap-14.png

gmap-15.png

gmap-17.png

移行ツール

課金設定をしていないと、いざ地図を表示しようとしてもエラーが出てしまいます。
エラー文は以下の通りです。
You have exceeded your request quota for this API.
See https://developers.google.com/maps/documentation/javascript/error-messages?utm_source=maps_js&utm_medium=degraded&utm_campaign=billing#api-key-and-billing-errors

移行ツール にアクセスし、手順にしたがって登録します。

アプリケーション作成

長きにわたる諸々の設定おつかれさまでした。
ここから早速アプリケーションを作っていきましょう。

ファイルを作成する前に先ほど作成したAPIキーを用意しておいてください。

登場人物

今回作成・編集するページは

  • routes.rb
  • maps_controller.rb
  • index.html.erb

この3つです。それぞれのファイルに書き込んでください。

ルーティング

routes.rb
Rails.application.routes.draw do
  get 'maps/index'
  root to: 'maps#index'
  resources :maps, only: [:index]
end

コントローラー

maps_controller.rb
class MapsController < ApplicationController
  def index
  end
end

ビューページ

マップ完成イメージ.png

ビュー作成

地図を表示させる

では以下のコードをファイルにコピーしてみましょう。

index.html.erb
<h2>gmap</h2>
<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 600px;
}
</style>

<script>
let map

function initMap(){
  geocoder = new google.maps.Geocoder()

  map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: 40.7828, lng:-73.9653},
    zoom: 12,
  });

  marker = new google.maps.Marker({
    position:  {lat: 40.7828, lng:-73.9653},
    map: map
  });
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap" async defer></script>

<style>タグでマップの大きさを指定していますが、ここを省いてしまうと地図が表示されないので必ず書きましょう。

<style>#map {height: 600px;width: 600px;}</style>

次に地図の初期設定についてですが、 centerで初期位置の緯度経度を指定し、 zoomで表示領域の大きさを決めます。

function initMap(){
  geocoder = new google.maps.Geocoder()

  map = new google.maps.Map(document.getElementById('map'), {
    //latが緯度、lngが経度を示します
    center: {lat: 40.7828, lng:-73.9653},
    //数値は0〜21まで指定できます。数値が大きいほど拡大されます
    zoom: 12,
  });
  //positionに指定した座標にピンを表示させます
  marker = new google.maps.Marker({
    position:  {lat: 40.7828, lng:-73.9653},
    map: map
  });
}

そして最後の行の YOUR_API_KEYの部分に自分で作成したキーを入れてください。

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap" async defer></script>

ではここで実際にブラウザで地図が表示されるか確認してみましょう。
以下のように表示されたでしょうか?

gmap-step1.png

検索フォームから住所を特定しピンを刺す

次に検索フォームを作成します。

index.html.erb
<h2>gmap</h2>

<!-- ここから追加 -->
<input id="address" type="textbox" value="GeekSalon">
<input type="button" value="Encode" onclick="codeAddress()">
<!-- ここまで追加-->

<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 600px;
}
</style>

<script>
let map
function initMap(){
  geocoder = new google.maps.Geocoder()

  map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: 40.7828, lng:-73.9653},
    zoom: 12,
  });

  marker = new google.maps.Marker({
    position:  {lat: 40.7828, lng:-73.9653},
    map: map
  });
}
// ここから追加 
let geocoder

function codeAddress(){
  let inputAddress = document.getElementById('address').value;

  geocoder.geocode( { 'address': inputAddress}, function(results, status) {
    if (status == 'OK') {
      map.setCenter(results[0].geometry.location);
      var marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location
      });
    } else {
      alert('該当する結果がありませんでした:' + status);
    }
  });   
}
// ここまで追加 
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap" async defer></script>

2箇所追加しました。
ビューに関する部分では検索フォームとボタンを作成しました。

<input id="address" type="textbox" value="GeekSalon">
<input type="button" value="Encode" onclick="codeAddress()">

次に <script>タグ内に検索機能の処理を追加しました。

let geocoder

  //検索フォームのボタンが押された時に実行される
function codeAddress(){
  //検索フォームの入力内容を取得
  let inputAddress = document.getElementById('address').value;

  geocoder.geocode( { 'address': inputAddress}, function(results, status) {
    //該当する検索結果がヒットした時に、地図の中心を検索結果の緯度経度に更新する
    if (status == 'OK') {
      map.setCenter(results[0].geometry.location);
      var marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location
      });
    } else {
      //検索結果が何もなかった場合に表示
      alert('該当する結果がありませんでした:' + status);
    }
  });   
}

では早速ブラウザを開いて見てみましょう。
以下のように検索結果に応じて結果が処理される機能をつけました。

gmap-step2.gif

検索結果から緯度経度を取得し、表示させる

さて初めてのGoogleマップ講座もいよいよ大詰めです。
最後に検索結果の緯度経度をページに表示させましょう。

コードは以下の通りです。

index.html.erb
<h2>gmap</h2>

<input id="address" type="textbox" value="GeekSalon">
<input type="button" value="Encode" onclick="codeAddress()">
<!-- 下の1行を追加 -->
<div id="display">何かが表示される、、、、!</div>

<div id='map'></div>

<style>
#map {
  height: 600px;
  width: 600px;
}
</style>

<script>
let map
let geocoder
// 下の1行を追加 
const display = document.getElementById('display')

function initMap(){
  geocoder = new google.maps.Geocoder()

  map = new google.maps.Map(document.getElementById('map'), {
    center: {lat: 40.7828, lng:-73.9653},
    zoom: 12,
  });

  marker = new google.maps.Marker({
    position:  {lat: 40.7828, lng:-73.9653},
    map: map
  });
}

function codeAddress(){
  let inputAddress = document.getElementById('address').value;

  geocoder.geocode( { 'address': inputAddress}, function(results, status) {
    if (status == 'OK') {
      map.setCenter(results[0].geometry.location);
      var marker = new google.maps.Marker({
          map: map,
          position: results[0].geometry.location
      });
      // 下の1行を追加 
      display.textContent = "検索結果:" + results[ 0 ].geometry.location
    } else {
      alert('該当する結果がありませんでした:' + status);
    }
  });   
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB9Uo-0D2PoQaAECrt9UXceHO7S1gveNd0&callback=initMap" async defer></script>

ビューに関して1箇所、 <script>タグ内に2箇所追加しました。
それぞれ追加する場所を確認し、追加して見てください。
追加した要素・処理は

  • 検索結果を表示させるためのディスプレイ
  • ディスプレイの情報を取得
  • 検索結果をディスプレイに表示

の3つです。なので追加箇所も3箇所です。
これでコードの書き込みは完了です。

ではでは最後にブラウザを開いて確認してみましょう。
検索結果がディスプレイに表示されれば完成です。

gmap-step3.gif

次回に続く!

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

ER図から『Laravel Migrationファイル生成』可能なツールを4日で作った

4日で作った『ER diagram Quick』 Laravel Migration生成可能

◇ツール名: ER diagram Quick

4日で作成したER図をWeb上で作成管理してLaravelのMigrationを生成するツールです。
『Laravel Migrationファイル生成』機能は、まだβ版ですがER図で設計したあとにベーステンプレートのMigrationを作成するだけでも(全て書かなくて良いので)捗るはずです。見た目など、細かい部分などはだいぶ見ないで進めましたが、現時点の進捗としては満足してます。

◇技術

  • HTML / CSS / Vanilla JS
  • Firebase / WWW SQL Designer
  • PHP /Laravel5.5(Migration検証)

ER_diagram_Quick.jpg

リンク: https://venezia-works.com/sql/

ER図をかいたら保存!!

2.jpg

作成したER図はCloud(Firebase) or ブラウザに保存

ER_diagram_Quick.jpg

Migrationファイルができあがるとこんな感じ

2019_10_25_162015_create_gs_user_table_table_php.jpg

早く開発できたのには理由がある

  • API(Firebase)を利用できたこと
  • WWW SQL Designer を利用できたこと(1日コード眺めるくらいでどのような事をやってるかわかったので、必要な修正には時間がかからなかった。)
  • Laravelを少し前に触ってたので、Migrationのドキュメントを読んでいた。そのため、全てではないがER図から生成できるカラム等の調査にも時間がかからなかった(はず)。
  • 作るのが楽しい

今後は

一気に作って少し疲れたので、少しずつ改修していこうと思います。
必要とされるかまだわかりませんが、今後も良いものにしていきたいです。

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

ER図から『Laravel Migrationファイル生成』可能なツールを4日で作ってみた

4日で作った『ER diagram Quick』 Laravel Migration生成可能

◇ツール名: ER diagram Quick

4日で作成したER図をWeb上で作成管理してLaravelのMigrationを生成するツールです。
『Laravel Migrationファイル生成』機能は、まだβ版ですがER図で設計したあとにベーステンプレートのMigrationを作成するだけでも、全て書かなくて良いので捗るはずです。
見た目など、細かい部分などはだいぶ見ないで進めましたが、現時点の進捗としては満足してます。(1日は子供をスカイツリーに連れていったので・・・もう少し少ないかも)
少しでも役に立てば幸いです。

ER_diagram_Quick.jpg

リンク: https://venezia-works.com/sql/

ER図をかいたら保存!!

2.jpg

作成したER図はCloud(Firebase) or ブラウザに保存

ER_diagram_Quick.jpg

Migrationファイルができあがると

Zip圧縮されて全てのMigrationファイルが作成されます。
中身はみなさんご存知の以下のようなCODEです。

2019_10_25_162015_create_gs_user_table_table_php.jpg

早く開発できたのには理由がある

  • API(Firebase)を利用できたこと
  • WWW SQL Designer を利用できたこと(1日コード眺めるくらいでどのような事をやってるかわかったので、必要な修正には時間がかからなかった。)
  • Laravelを少し前に触ってたので、Migrationのドキュメントを読んでいた。そのため、全てではないがER図から生成できるカラムや記法等の調査にも時間がかからなかった。
  • 作るのが楽しい

◇使用した技術

  • HTML / CSS / Vanilla JS
  • Firebase / WWW SQL Designer
  • PHP / Laravel5.5(Migration検証) / MAMP

今後は

一気に作って少し疲れたので、少しずつ改修していこうと思います。
必要とされるかまだわかりませんが、今後も良いものにしていきたいです。
いや、かなり便利なはずなので、是非使ってほしい(^^)

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

ER図をWeb上で作成管理してLaravelのMigrationを生成するツール

4日で作った『ER diagram Quick』 Laravel Migration生成可能

◇ツール名: ER diagram Quick

4日で作成したER図をWeb上で作成管理してLaravelのMigrationを生成するツールです。
『Laravel Migrationファイル生成』機能は、まだβ版ですがER図で設計したあとにベーステンプレートのMigrationを作成するだけでも、全て書かなくて良いので捗るはずです。
見た目など、細かい部分などはだいぶ見ないで進めましたが、現時点の進捗としては満足してます。(1日は子供をスカイツリーに連れていったので・・・もう少し少ないかも)
少しでも役に立てば幸いです。

ER_diagram_Quick.jpg

リンク: https://venezia-works.com/sql/

ER図をかいたら保存!!

2.jpg

作成したER図はCloud(Firebase) or ブラウザに保存

ER_diagram_Quick.jpg

Migrationファイルができあがると

Zip圧縮されて全てのMigrationファイルが作成されます。
中身はみなさんご存知の以下のようなCODEです。

2019_10_25_162015_create_gs_user_table_table_php.jpg

早く開発できたのには理由がある

  • API(Firebase)を利用できたこと
  • WWW SQL Designer を利用できたこと(1日コード眺めるくらいでどのような事をやってるかわかったので、必要な修正には時間がかからなかった。)
  • Laravelを少し前に触ってたので、Migrationのドキュメントを読んでいた。そのため、全てではないがER図から生成できるカラムや記法等の調査にも時間がかからなかった。
  • 作るのが楽しい

◇使用した技術

  • HTML / CSS / Vanilla JS
  • Firebase / WWW SQL Designer
  • PHP / Laravel5.5(Migration検証) / MAMP

今後は

一気に作って少し疲れたので、少しずつ改修していこうと思います。
必要とされるかまだわかりませんが、今後も良いものにしていきたいです。
いや、かなり便利なはずなので、是非使ってほしい(^^)

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

ER図からLaravelのMigrationを生成するツール

『ER diagram Quick』は Laravel Migration生成可能ツールでもある

◇ツール名: ER diagram Quick

実は4日間で作成したER図をWeb上で作成管理してLaravelのMigrationを生成するツールです。たった4日間ですが、アウトプットの量はハンパないので、こういうのは慣れています。
『Laravel Migrationファイル生成』機能は、まだβ版ですがER図で設計したあとにベーステンプレートのMigrationを作成するだけでも、全て書かなくて良いので捗るはずです。

開発の進めかたですが、個人としては「アウトプット・ファースト」を最優先にしています。
大体いつもα版(目的を満たす機能、動作ができてる最低限の状態)を一週間と決めて開発しています。
そこを軸に、大事な目的にあった機能を先行して作り込み、機能動作優先、見た目など細かい部分などはだいぶ見ないで進めました。現時点の進捗としては満足してます。いつもデザインはリリース後に調整していくので、このスピード感を出すためには、この方法が良いと思っています(まずは動くが優先、モチベーションも保ちやすい)。
少しでも役に立てば幸いです。

ER_diagram_Quick.jpg

リンク: https://venezia-works.com/sql/

ER図をかいたら保存!!

2.jpg

作成したER図はCloud(Firebase) or ブラウザに保存

ER_diagram_Quick.jpg

Migrationファイルができあがると

Zip圧縮されて全てのMigrationファイルが作成されます。
中身はみなさんご存知の以下のようなCODEです。

2019_10_25_162015_create_gs_user_table_table_php.jpg

早く開発できたのには理由がある

  • API(Firebase)を利用できたこと
  • WWW SQL Designer を利用できたこと(1日コード眺めるくらいでどのような事をやってるかわかったので、必要な修正には時間がかからなかった。)
  • Laravelを少し前に触ってたので、Migrationのドキュメントを読んでいた。そのため、全てではないがER図から生成できるカラムや記法等の調査にも時間がかからなかった。
  • 作るのが楽しい

◇使用した技術

  • HTML / CSS / Vanilla JS
  • Firebase / WWW SQL Designer
  • PHP / Laravel5.5(Migration検証) / MAMP

今後は

一気に作って少し疲れたので、少しずつ改修していこうと思います。
必要とされるかまだわかりませんが、今後も良いものにしていきたいです。
いや、かなり便利なはずなので、是非使ってほしい(^^)

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