20210221のJavaScriptに関する記事は20件です。

SpringBootでToDoアプリを作ってみよう【+jQuery】

はじめに

SpringBootを使ってTodoアプリを作っていきます。

↓以下の記事の続きです。ここに加えていくので、先に作ってみてください。
SpringBootでToDoアプリを作ってみよう【誰でも作れます・初心者向け】

前回分のソースファイル
https://github.com/tokio-k/TodoApp-springboot/tree/intermediate

前回まででこんな状態です。
無題.png

これから、SpringBootの勉強を始めるという人の役に立てれば、いいなと思っています。
基本的にコードはファイル全部を載せるようにします。自分が勉強している時に、書く場所がわからないことがあったので。
既存のものを少し編集するだけの時は省略します。
Ajaxを使った非同期処理で通信を行っていきます。
SpringBootとjQueryのデータ送受信がメインになるかと思います。

使う技術

  • テンプレートエンジン Thymeleaf
  • データベース PostgreSQL
  • ORマッパー  Mybatis
  • フロント部分 jQuery 、Bootstrap

流れ

  • CSS・JavaScriptを使う準備
  • タスク一覧表示の改修
  • 更新処理の改修
  • 完了済みの表示表示機能の追加
  • 追加機能の表示&追加処理の改修
  • 削除機能の表示&削除処理の改修

CSS・JavaScriptを使う準備

Thymeleafのみだったフロンド部分に、CSSやJavaScriptを追加していきます。
src/main/resources/staticの下に記述する必要があります。

src/main/resources/staticの下に
「css」フォルダと「js」フォルダを作成します。
この中に、作成したCSSファイルやJsファイルを格納してくことにします。
※今回cssファイルはほとんど使わないです。学習用に1つだけ使います。

cssフォルダには、「style.css」を
jsフォルダには、「todo.js」を作成します。

index.htmlのheadの中に以下を追加します。(少し下にindex.html全体を載せてます。)

<link rel="stylesheet" th:href="@{/css/style.css}" />

index.htmlのbodyの最後に以下を追加します。

<script type="text/javascript" th:src="@{/js/todo.js}"></script>

これで、cssフォルダとJsフォルダの中のファイルを適応できます。

今回は、jQueryとBootstrapも使用するので、pom.xmlに以下を追記します。

pom.xml
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>4.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.5.1</version>
        </dependency>

(webjarsで依存追加の書き方を確認できます。)
dependenciesタグの中に追加します。

index.htmlのheadの中に以下を追加します。

<script src="webjars/jquery/3.5.1/jquery.min.js"></script>
<link rel="stylesheet" href="webjars/bootstrap/4.6.0/css/bootstrap.min.css" />
<script src="webjars/bootstrap/4.6.0/js/bootstrap.min.js"></script>

これで、jQueryとBootstrapが使えます。

タスク一覧表示の改修

画面を少し整えていきます。

index.htmlを編集します
今回は、formの送信ではなくjQueryで情報を取得して、Controller側に渡すことにします。

index.html
<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<script src="webjars/jquery/3.5.1/jquery.min.js"></script>
<link rel="stylesheet" href="webjars/bootstrap/4.6.0/css/bootstrap.min.css" />
<script src="webjars/bootstrap/4.6.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" th:href="@{/css/style.css}" />
<title>TodoApp</title>
</head>
<body>
  <h1>TodoList</h1>
  <h3>マイタスク</h3>
  <!--↓編集箇所  -->

  <table class="table table-borderless">
    <thead>
    </thead>
    <tbody id="todes">
      <tr class="todo" th:each="todo : ${todos}">
        <td style="width:2rem">
          <input type="checkbox"name="done_flg"/>
          <input type="hidden" name="id" th:value="${todo.id}" />
        </td>
        <td  style="width:22rem"><input type="text" style="border:none;width:22rem" name="title" th:value="${todo.title}"/></td>
        <td><input type="date" style="border:none" name="time_limit" th:value="${todo.time_limit}" /></td>
      </tr>
    </tbody>
  </table>

  <div style="font-size:20px">
    <span id="done_num">1</span>
    <span class="pr-3">件完了</span>
    <span class="button_for_show" style="display:inline-block"></span>
  </div>
  <table class="table table-borderless">
  <thead>
  </thead>
    <tbody id="donetodes">
      <tr class="todo" th:each="todo : ${doneTodos}">
        <td style="width:2rem">
          <input type="checkbox"name="done_flg" checked/>
          <input type="hidden" name="id" th:value="${todo.id}" />
        </td>
        <td  style="width:22rem"><input type="text" style="border:none;width:22rem;text-decoration:line-through" name="title" th:value="${todo.title}"/></td>
        <td><input type="date"style="border:none;display:none" name="time_limit" th:value="${todo.time_limit}" /></td>
      </tr>
    </tbody>
  </table>

  <!--↑編集箇所  -->

  <h3>新しいタスクを追加</h3>
  <form method="post" th:action="@{/add}">
    <input type="text" name="title" />
    <input type="date" name="time_limit"/>
    <input type="submit" value="追加" />
  </form>

  <form method="post" th:action="@{/delete}">
    <input type="submit" value="完了済みを削除" />
  </form>

  <script type="text/javascript" th:src="@{/js/todo.js}"></script>
</body>
</html>

cssも1つだけ

style.css
.button_for_show {
    border-right : solid 2px #000;
    border-bottom : solid 2px #000;
    width:1rem;
    height:1rem;
    transform : rotate(45deg);
    position:relative;
    bottom:4px
}

こんな感じになりました。
jsで使うために名前がついてたりしますが、今は気にしないでください。
見出しなど自由にカスタマイズしてみてください。
無題.png
※写真イメージには、<span id="done_num">2</span>と直接2を書いています。
※上のコードでは2は表示されません。今からJsで実装します

完了件数の数を取得して表示します。
todo.jsを編集します

<tbody id="donetodes">の子要素の数を数えて表示しています

index.js
/**
 *
 */
$(function(){

//完了済みの個数取得・表示
  let doneCount = $("#donetodes").children("tr").length;
  $("#done_count").text(doneCount);

})

更新処理の改修

index.htmlを編集したので、それに合わせて更新をできるようにしていきます。
編集をしたらすぐに更新できるようにします。

todo.jsを編集します

ここでやることは以下です。

  • 更新があるたびに処理を実行する
  • 1つのタスクの情報(id,title,time_limit,done_flg)を取得する
  • 取得した値をController側に送って/updateの処理を実行する
  • 完了(未完了)ボタンを押した際の処理を実行する
    • 完了済み(未完了)へ移動する
    • 文字に打ち消し線をつける(消す)
    • 日付を隠す(表示する)
    • 完了件数を更新する
todo.js
$(function(){

//更新処理
$('.todo input').change(function(){
    const todo = $(this).parents('.todo');
    const id = todo.find('input[name="id"]').val();
    const title = todo.find('input[name="title"]').val();
    const time_limit = todo.find('input[name="time_limit"]').val();
    const is_done = todo.find('input[name="done_flg"]').prop("checked");
    let done_flg;
    if(is_done == true) {
      done_flg = 1;
    }else{
      done_flg = 0;
    }

    const params = {
        id : id,
        title : title,
        time_limit : time_limit,
        done_flg : done_flg
    }
    $.post("/update",params);

    //完了ボタンを押した際の処理
    doneCount =  $("#done_count").text();

    if($(this).prop('name') == "done_flg"){
      if(isDone == true){
        $(todo).appendTo("#donetodes");
        todo.find('input[name="title"]').css('text-decoration','line-through')
        todo.find('input[name="time_limit"]').hide();
        doneCount ++;
      }else{
        $(todo).appendTo("#todes");
        todo.find('input[name="title"]').css('text-decoration','none')
        todo.find('input[name="time_limit"]').show()
        doneCount --;
      }

      $("#done_count").text(doneCount);
    }

})

})

1つ目のconstで<tr>単位の要素(todo単位)を取得しています。
2~5つ目のconstでそれぞれのinputの要素を取得しています。
5つ目のconstでそれぞれの値をオブジェクトに格納し、その後postメソッドでcontrollerに送っています。
※postメソッドに続けて失敗時の処理、controllerからの戻り値を扱う処理等も書くことができます。
※ajaxやpostメソッドなどで調べてみてください。

「完了(未完了)ボタンを押した際の処理」以下では、以下の処理を記載しています。

  • 完了(未完了)ボタンを押したかどうかで条件分岐
  • 完了(未完了)ボタンを押しており完了済みの場合の処理を記載
  • 完了(未完了)ボタンを押しており未完了の場合の処理を記載

これで更新ができるようになりました。

TodoController.javaを編集する
非同期処理で画面遷移もなくしているので、Controllerのupdateメソッドも以下のように変更します。

TodoController.java
    @RequestMapping(value="/update")
    @ResponseBody
    public void update(Todo todo) {
        todoMapper.update(todo);
    }

Controllerはビュークラスを返すのが基本ですが、@ResponseBodyをつけることでコンテンツが返せます。
今回は、何も返していないのですが、つけなかったらエラーになったのでつけておきます。


【折り畳み(本文とは関係のない内容)】
フォームの入力チェックでのエラー内容や、更新処理の成功かどうかのデータ等を返すこともあります。

更新処理のエラーかどうか返す例

(例)Controller.java
    @RequestMapping(value="/update")
    @ResponseBody
    public String update(Entity entity) {
        String succes = false;
        try {
            Mapper.update(Entity);
            succes = "true";
        } catch(Exception e) {
            succes = "false";
        }
        return succes;
    }

Mapなどを使って、内容を返したりなどもできます。
String→Map<String,Object> 、 Stringに"errMsg",Objectにエラーメッセージのリスト等
この戻り値はjQueryのpostメソッドに連結させたメソッドの引数となります。

完了済みの表示非表示機能の追加

完了しているものは基本的に消しておいて必要な時のみ表示できるようにします。

index.htmlを編集する

2つ目のtableタグ(完了済みを表示している方)にstyleとidをつけます。
※変更がここだけなので、他省略しました。

index.html
<table class="table table-borderless" style="display:none"id="done_table">

これで、完了済みのタスクが表示されなくなりました。

todo.jsを編集する

「完了済みタスク表示/非表示切り替え」以降が編集箇所です。
showStateには、done_tableのdisplayプロパティの値を格納しています。
displayプロパティがnoneの場合は、表示させるための処理を
displayプロパティがnone以外の場合は、非表示にするための処理を書きます
cssを変更して、ボタンの向きと位置も変更しています。

todo.js
$(function(){

//完了済みの個数取得・表示
  let doneCount = $("#donetodes").children("tr").length;
  $("#done_count").text(doneCount);

//更新処理
$('.todo input').change(function(){
    const todo = $(this).parents('.todo');
    const id = todo.find('input[name="id"]');
    const title = todo.find('input[name="title"]');
    const timeLimit = todo.find('input[name="time_limit"]');
    const isDone = todo.find('input[name="done_flg"]').prop("checked");
    let doneFlg;
    if(isDone == true) {
      doneFlg = 1;
    }else{
      doneFlg = 0;
    }

    const params = {
        id : id.val(),
        title : title.val(),
        time_limit : timeLimit.val(),
        done_flg : doneFlg
    }
    $.post("/update",params);

    //完了ボタンを押した際の処理
    doneCount =  $("#done_count").text();

    if($(this).prop('name') == "done_flg"){
      if(isDone == true){
        $(todo).appendTo("#donetodes");
        todo.find('input[name="title"]').css('text-decoration','line-through')
        todo.find('input[name="time_limit"]').hide();
        doneCount ++;
      }else{
        $(todo).appendTo("#todes");
        todo.find('input[name="title"]').css('text-decoration','none')
        todo.find('input[name="time_limit"]').show()
        doneCount --;
      }

      $("#done_count").text(doneCount);
    }


})

//完了済みタスク表示/非表示切り替え
$('.button_for_show').click(function(){
    let showState = $('#done_table').css('display');
    if(showState == "none") {
        $('#done_table').show();
        $(this).css({ transform: ' rotate(225deg)','bottom':'-4px' });
    }else{
        $('#done_table').hide();
        $(this).css({ transform: ' rotate(45deg)','bottom':'4px' });
    }
})

})

これで、完了済みタスクの表示/非表示の切り替えができるようになりました。
無題.png

追加機能の表示と追加処理の改修

index.htmlを編集する
追加機能のフォームには、BootStrapのモーダルを使用します。

モーダルは以下のようにすることで作ることができます。

<button data-toggle="modal" data-target="#modal">
  モーダルを開くボタン
</button>
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h3 class="modal-title" id="modalLabelId">タイトル</h3>
            </div>
            <div class="modal-body">
              <!--ここにbody要素を入れる-->
            </div>
            <div class="modal-footer">
                <!--ここにfooter要素を入れる-->
            </div>
        </div>
    </div>
</div>

参考:https://www.fenet.jp/dotnet/column/language/6549/
modal-headerには、タイトルなどのヘッダー要素、
modal-bodyには、内容となるボディ要素、
modal-footerには、ボタンなどのフッター要素、を入れます。

今回は、ボディにフォームと追加ボタンを作ります。
タイトルとフッダーは無しにします。
add_formの値をjQueryで取得して、Controller側に送ります。

index.html
<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<script src="webjars/jquery/3.5.1/jquery.min.js"></script>
<link rel="stylesheet" href="webjars/bootstrap/4.6.0/css/bootstrap.min.css" />
<script src="webjars/bootstrap/4.6.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" th:href="@{/css/style.css}" />
<title>TodoApp</title>
</head>
<body>
  <h1>TodoList</h1>
  <h3>マイタスク</h3>
  <table class="table table-borderless">
    <thead>
    </thead>
    <tbody id="todes">
      <tr class="todo" th:each="todo : ${todos}">
        <td style="width:2rem">
          <input type="checkbox"name="done_flg"/>
          <input type="hidden" name="id" th:value="${todo.id}" />
        </td>
        <td  style="width:22rem"><input type="text" style="border:none;width:22rem" name="title" th:value="${todo.title}"/></td>
        <td><input type="date" style="border:none" name="time_limit" th:value="${todo.time_limit}" /></td>
      </tr>
    </tbody>
  </table>

  <div style="font-size:20px">
    <span id="done_count"></span>
    <span class="pr-3">件完了</span>
    <span class="button_for_show " style="display:inline-block"></span>
  </div>
  <table class="table table-borderless" style="display:none"id="done_table">
  <thead>
  </thead>
    <tbody id="donetodes">
      <tr class="todo" th:each="todo : ${doneTodos}">
        <td style="width:2rem">
          <input type="checkbox"name="done_flg" checked/>
          <input type="hidden" name="id" th:value="${todo.id}" />
        </td>
        <td  style="width:22rem"><input type="text" style="border:none;width:22rem;text-decoration:line-through" name="title" th:value="${todo.title}"/></td>
        <td><input type="date"style="border:none;display:none" name="time_limit" th:value="${todo.time_limit}" /></td>
      </tr>
    </tbody>
  </table>

  <!--↓編集箇所  -->

   <button type="button" class="btn btn-light rounded-circle p-0 text-muted font-weight-bold" data-toggle="modal"data-target="#modal"
    style="width:2.5rem;height:2.5rem;"></button>
   <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true">
       <div class="modal-dialog">
           <div class="modal-content">
               <div class="modal-body">
                   <form id="add_form">
                       <div class="form-group">
                           <input type="text" name="title" class="form-control"placeholder="新しいタスク"/>
                       </div>
                       <div class="form-group">
                           <input type="date" name="time_limit"class="form-control" style="width:60%;" />
                       </div>
                       <button type="button" class="btn btn-primary float-right " data-dismiss="modal" id="add">追加</button>
                   </form>
               </div>
           </div>
       </div>
   </div>

  <!--↑編集箇所  -->


  <form method="post" th:action="@{/delete}">
    <input type="submit" value="完了済みを削除" />
  </form>

  <script type="text/javascript" th:src="@{/js/todo.js}"></script>
</body>
</html>

無題.png

+ボタンを押すと、こんな感じになります。

TodoController.javaを編集する

非同期処理にあわせてControllerを編集していきます。
引数としてTodoのタイトルと期間をわたし、データベースに登録
戻り値として、引数のデータに対応するidを加えたデータを返します。

idはデータベース登録時に自動で値が登録されるものなので、その値を取得する必要があります。

TodoController.java
    @RequestMapping(value="/add")
    @ResponseBody
    public Todo add(Todo todo) {
        todoMapper.add(todo);
        return todo;
    }

todoMapper.addを実行するとtodoにidが自動で追加されるように、
TodoMapper.xmlを編集していきます。

TodoMapper.xmlを編集する
insert時に自動採番されたidを取得できるようにしていきます。
この時、idは戻り値ではなく、引数に渡したオブジェクトにマッピングされます。

つまり、戻り値などを受け取りセットする必要もなく、自動でtodoに追加されます。
(戻り値は更新件数になります。ここでは1行insertなので1)

TodoMapper.xml
<insert id="add"  useGeneratedKeys="true" keyProperty="id" parameterType="com.todo.app.entity.Todo">
    insert into todo_items (title,time_limit)
    values (#{title},to_date(#{time_limit},'yy-mm-dd'))
  </insert>

addメソッドの実装をしている、このinsert文に「useGeneratedKeys="true" keyProperty="id"」を設定します。
これで、自動採番された値をidに入れることができるようになりました。(keyPropertyの値はフィールド名)

todo.jsを編集する

追加ボタンを押してから、Controllerとデータのやり取りをし、表示するまでを実装していきます。

todo.js
$(function(){

//完了済みの個数取得・表示
  let doneCount = $('#donetodes').children("tr").length;
  $('#done_count').text(doneCount);

//更新処理
$('.todo input').change(function(){
    const todo = $(this).parents('.todo');
    const id = todo.find('input[name="id"]');
    const title = todo.find('input[name="title"]');
    const timeLimit = todo.find('input[name="time_limit"]');
    const isDone = todo.find('input[name="done_flg"]').prop("checked");
    let doneFlg;
    if(isDone == true) {
      doneFlg = 1;
    }else{
      doneFlg = 0;
    }

    const params = {
        id : id.val(),
        title : title.val(),
        time_limit : timeLimit.val(),
        done_flg : doneFlg
    }
    $.post("/update",params);

    //完了ボタンを押した際の処理
    doneCount =  $('#done_count').text();

    if($(this).prop('name') == "done_flg"){
      if(isDone == true){
        $(todo).appendTo('#donetodes');
        todo.find('input[name="title"]').css('text-decoration','line-through')
        todo.find('input[name="time_limit"]').hide();
        doneCount ++;
      }else{
        $(todo).appendTo('#todes');
        todo.find('input[name="title"]').css('text-decoration','none')
        todo.find('input[name="time_limit"]').show()
        doneCount --;
      }

      $("#done_count").text(doneCount);
    }

})

//完了済みタスク表示/非表示切り替え
$('.button_for_show').click(function(){
    let showState = $('#done_table').css('display');
    if(showState == "none") {
        $('#done_table').show();
        $(this).css({ transform: ' rotate(225deg)','bottom':'-4px' });
    }else{
        $('#done_table').hide();
        $(this).css({ transform: ' rotate(45deg)','bottom':'4px' });
    }
})

//追加処理
$('#add').click(function() {
    const params = $('#add_form').serializeArray();
    $.post("/add",params).done(function(json){
        const clone = $('#todes tr:first').clone(true);
        clone.find('input[name="id"]').val(json.id);
        clone.find('input[name="title"]').val(json.title);
        clone.find('input[name="time_limit"]').val(json.time_limit);
        $('#todes').append(clone[0]);
    })
})


})

追加処理でやっていることは以下です。

  • serializeArray()でformの値を取得
  • 取得した値をControllerに送る
  • Controllerから値を取得
  • タスク一覧の1つ目の要素をコピー
  • コピーした要素の値をコントローラーから取得した値に変更
  • タスク一覧の最後に追加

これで、非同期にデータを追加することができるようになりました。

削除機能の表示&削除処理の改修

最後に削除ボタンも画面遷移なしで実行できるようにしていきます。

index.htmlを編集する
「完了済みを削除」をボタンにするだけです。
ほとんど変更はありません。

index.html
<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<script src="webjars/jquery/3.5.1/jquery.min.js"></script>
<link rel="stylesheet" href="webjars/bootstrap/4.6.0/css/bootstrap.min.css" />
<script src="webjars/bootstrap/4.6.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" th:href="@{/css/style.css}" />
<title>TodoApp</title>
</head>
<body>
  <h1>TodoList</h1>
  <h3>マイタスク</h3>
  <table class="table table-borderless">
    <thead>
    </thead>
    <tbody id="todes">
      <tr class="todo" th:each="todo : ${todos}">
        <td style="width:2rem">
          <input type="checkbox"name="done_flg"/>
          <input type="hidden" name="id" th:value="${todo.id}" />
        </td>
        <td  style="width:22rem"><input type="text" style="border:none;width:22rem" name="title" th:value="${todo.title}"/></td>
        <td><input type="date" style="border:none" name="time_limit" th:value="${todo.time_limit}" /></td>
      </tr>
    </tbody>
  </table>

  <div style="font-size:20px">
    <span id="done_count"></span>
    <span class="pr-3">件完了</span>
    <span class="button_for_show " style="display:inline-block"></span>
  </div>
  <table class="table table-borderless" style="display:none"id="done_table">
  <thead>
  </thead>
    <tbody id="donetodes">
      <tr class="todo" th:each="todo : ${doneTodos}">
        <td style="width:2rem">
          <input type="checkbox"name="done_flg" checked/>
          <input type="hidden" name="id" th:value="${todo.id}" />
        </td>
        <td  style="width:22rem"><input type="text" style="border:none;width:22rem;text-decoration:line-through" name="title" th:value="${todo.title}"/></td>
        <td><input type="date"style="border:none;display:none" name="time_limit" th:value="${todo.time_limit}" /></td>
      </tr>
    </tbody>
  </table>


   <button type="button" class="btn btn-light rounded-circle p-0 text-muted font-weight-bold" data-toggle="modal"data-target="#modal"
    style="width:2.5rem;height:2.5rem;"></button>
   <div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true">
       <div class="modal-dialog">
           <div class="modal-content">
               <div class="modal-body">
                   <form id="add_form">
                       <div class="form-group">
                           <input type="text" name="title" class="form-control"placeholder="新しいタスク"/>
                       </div>
                       <div class="form-group">
                           <input type="date" name="time_limit"class="form-control" style="width:60%;" />
                       </div>
                       <button type="button" class="btn btn-primary float-right"  data-dismiss="modal" id="add">追加</button>
                   </form>
               </div>
           </div>
       </div>
   </div>
  <!--↓編集箇所  -->
  <button type="button" class="btn btn-outline-secondary" id="delete">完了済みを削除</button>
  <!--↑編集箇所  -->
  <script type="text/javascript" th:src="@{/js/todo.js}"></script>
</body>
</html>

TodoController.javaを編集する

TodoController.java
    @RequestMapping(value="/delete")
    @ResponseBody
    public void delete() {
        todoMapper.delete();
    }

todo.jsを編集する

行う事は以下です。

  • Controllerのdeleteメソッドの実行
  • 完了済みタスクの表示削除
  • 完了件数のリセット
todo.js
$(function(){

//完了済みの個数取得・表示
  let doneCount = $('#donetodes').children("tr").length;
  $('#done_count').text(doneCount);

//更新処理
$('.todo input').change(function(){
    const todo = $(this).parents('.todo');
    const id = todo.find('input[name="id"]');
    const title = todo.find('input[name="title"]');
    const timeLimit = todo.find('input[name="time_limit"]');
    const isDone = todo.find('input[name="done_flg"]').prop("checked");
    let doneFlg;
    if(isDone == true) {
      doneFlg = 1;
    }else{
      doneFlg = 0;
    }

    const params = {
        id : id.val(),
        title : title.val(),
        time_limit : timeLimit.val(),
        done_flg : doneFlg
    }
    $.post("/update",params);

    //完了ボタンを押した際の処理
    doneCount =  $('#done_count').text();

    if($(this).prop('name') == "done_flg"){
      if(isDone == true){
        $(todo).appendTo('#donetodes');
        todo.find('input[name="title"]').css('text-decoration','line-through')
        todo.find('input[name="time_limit"]').hide();
        doneCount ++;
      }else{
        $(todo).appendTo('#todes');
        todo.find('input[name="title"]').css('text-decoration','none')
        todo.find('input[name="time_limit"]').show()
        doneCount --;
      }

      $("#done_count").text(doneCount);
    }


})

//完了済みタスク表示/非表示切り替え
$('.button_for_show').click(function(){
    let showState = $('#done_table').css('display');
    if(showState == "none") {
        $('#done_table').show();
        $(this).css({ transform: ' rotate(225deg)','bottom':'-4px' });
    }else{
        $('#done_table').hide();
        $(this).css({ transform: ' rotate(45deg)','bottom':'4px' });
    }
})

//追加処理
$('#add').click(function() {
    const params = $('#add_form').serializeArray();
    $.post("/add",params).done(function(json){
        const clone = $('#todes tr:first').clone(true);
        clone.find('input[name="id"]').val(json.id);
        clone.find('input[name="title"]').val(json.title);
        clone.find('input[name="time_limit"]').val(json.time_limit);
        $('#todes').append(clone[0]);
    })
})

//削除処理
$('#delete').click(function(){
    $.post("/delete").done(function(){
        $('#donetodes').empty();
        $('#done_count').text(0);
    })
})

})

これで削除もできました。

まとめ

画面遷移なしで、SpringBootとフロント側でデータの送受信を行えるように実装していきました。

それぞれの要素がすべて左寄席1列で並んでいるのでカスタマイズしたり、
例外処理やバリデーションチェック等の機能も実装してみたり、
色々と遊んでみてください。

1から作るよりも、何かを改修していく方がハードルが低いと思っています。

「SpringBootの勉強を始めた!」という人の役に立てればいいなと思ってます。
思ったより長くなってしまいました。
慣れないながらも書くの頑張ったので、LGTMもよかったら押してください。
めっちゃ喜びます。

↓ソースファイルです
https://github.com/tokio-k/TodoApp-springboot

参考文献

https://www.fenet.jp/dotnet/column/language/6549/
https://qiita.com/fukasawah/items/eb0f7f067f8b347cbb2a
https://getbootstrap.jp/docs/4.2/getting-started/introduction/
https://www.webjars.org/

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

関数宣言と関数式では読み込まれるタイミングが異なる

JavaScriptの関数宣言と関数式

関数宣言とは

関数(Rubyにおけるメソッド)を定義する方法の一つ

書き方

function 関数名( 引数 ){
  // 関数内の処理
}

関数式とは

関数を定義する方法の一つ
関数宣言とは違い、function(){}という無名の関数を変数に代入して関数を定義する方法

書き方

変数 = function( 引数 ){
  // 関数内の処理
}

関数宣言と関数式の違い

関数宣言と関数式では読み込まれるタイミングが異なります。

JavaScriptの関数宣言は関数の呼び出しより先に読み込まれます。一方で、関数式であれば先に読み込まれることはありません。なので以下のように記述するとエラーが起きます。

hello()

const hello = function(){
  console.log('hello')
}

このコードを実行すると Uncaught ReferenceError... と表示されます。
これは、1行目の時点で関数helloが定義されていないためです。

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

AudioAPI調べ

ここでは

私が調べたことについての備忘録です。
内容について、検証が出来ていないことも多々あると思います。

Web Audio Apiについて

web audio apiとは

現在の認識は、
"JavaScriptを用いて、HTMLが持つの既存のAudio要素を細かく操作する技術"
といった感じ。

JavaScriptでのクラス

web audio apiを扱うにあたって、そもそもJavaScript(以下"JS")の知識が必要と思われました。
わかりやすそうなコードを参考にさせていただき、読み解く中で、
最初にぶち当たったのが、crass と constructerの記述。

クラスについては、Rubyなど、オブジェクト指向と呼ばれる言語で利用するもので、
"車でいうところの設計図" "人間で言うところの個人と言う概念"
のような例えをされると思います。
扱いたいデータで、「同じ属性で何種類も出現する可能性があるもの」に使うのかな くらいの認識です。

正直、筆者はクラスの使いどころが曖昧である。

で。

JSではclass ~でクラスを用意して、
classの中で、constructor()メソッドを定義、 
classがnewされる際に、constructor()で定義したデータの属性?をインスタンスのデータとして取得してインスタンスを利用していく
と言う流れになるのかなと思う。

余談と言うか、メモ

JSではclassと言う機能はなく、「クラスっぽい書き方ができるよ。」って感じ。らしい。
JSではclassと言う機能に、クラスっぽい機能が働くようなコードが親にある。それをclassってのにまとめてるだけ。
なので、物によってはclass使うよりサッパリかけることもある。

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

WebAudioAPI調べ中

ここでは

私が調べたことについての備忘録です。
内容は徐々に増えると思います。

内容について、
調べたことをメモに近い形で記述していきます。
検証が出来ていないことも多々あると思います。
認識の間違いなどあればご指摘、ご教授いただけると幸いです。

WebAudioApiについて

WebAudioApiとは

現在の認識は、
"JavaScriptを用いて、HTMLが持つ既存のAudio要素を細かく操作する技術"
といった感じ。

audioContextインスタンス

audioContextはブラウザ上でオーディオデータを扱う際に始めに生成するべきインスタンス
audioContextを生成することで、ブラウザ上のオーディオデータへのアクセスが容易になると言うイメージです。

audioContextを利用する際の仕組みというか、流れの部分には、
input → 処理 → output
という流れがあるということ。

inputは、以下のようなものかと思われる
・自身のPC上のファイル 
・生成するオシレーター音源 
・URLから取得してくるファイル

取得したオーディオデータを、処理(フィルターをかけたり)して、
outputに接続すると音声が出力される

といった感じかと。

JavaScriptでのクラス

WebAudioApiを扱うにあたって、そもそもJavaScript(以下"JS")の知識が必要と思われました。
わかりやすそうなコードを参考にさせていただき、読み解く中で、
最初にぶち当たったのが、class と constructerの記述。

クラスについては、Rubyなど、オブジェクト指向と呼ばれる言語で利用するもので、
"車でいうところの設計図" "人間で言うところの個人と言う概念"
のような例えをされると思います。
扱いたいデータで、「同じ属性で何種類も出現する可能性があるもの」に使うのかな 程度の認識です。

正直、筆者はクラスの使いどころが曖昧である。

で。

JSではclass ~ でクラスを用意して、
classの中で、constructor()メソッドを定義、 
classがnewされる際に、constructor()で定義したデータの属性?をインスタンスのデータとして取得してインスタンスを利用していく
と言う流れになるのかなと思う。

余談と言うか、メモ

JSではclassと言う機能はなく、「クラスっぽい書き方ができるよ。」って感じ。らしい。
JSではclassと言う機能に、クラスっぽい機能が働くようなコードが親にある。それをclassってのにまとめてるだけ。
なので、物によってはclass使うよりサッパリかけることもある。 のかな?

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

LaravelでstripeのJavaScriptがbladeで動かない!! 原因はapp.jsとバッティングしていました。

はじめに

駆け出しエンジニアです。現在、Laravel5.5でECサイトを作成しています。その際にstripeによる決済機能を追加しているときに苦戦したことを記録していきます。
少しでも皆様の参考になったらと思います。

やろうとしていること

Laravel5.5ので作成しているECサイトにstripe決済を追加しようとしています。
そのさいに、bladeテンプレートにstripeのcheckoutをJavaScriptで読み込ませようとしています。

どんな現象が起きたか

カートに入れた商品一覧を表示するviewにstripe決済のJavaScriptを導入するために下記コードを記載しました。

@extends('layouts.app')

@section('content')
<h1>カート一覧ページ</h1>
@if (session('flash_message'))
<p style="color:red;">{{session('flash_message')}}</p>
@endif

@if (empty($user_carts))
<p>カートが空です</p>
@else

<table>
<tr>
<th>商品名</th>
<th>価格</th>
<th>購入数</th>
<th>この商品の合計金額(小計)</th>
<th>カートから削除するボタン</th>
</tr>
@foreach ($user_carts as $user_cart)

<tr>
<td>{{$user_cart['name']}}</td>
<td>{{$user_cart['price']}}円</td>
<td>{{$user_cart['item_count']}}</td>
<td>{{$user_cart['item_total']}}円</td>
<td><a href="{{route('cart.delete', ['id' => $user_cart['id']])}}">商品を削除する</a></td>

</tr>
@endforeach
</table>
<p>全商品の合計金額</p>
<p>{{$total}}円</p>
@endif

@if (empty($select_address))
<p>住所は選択されいません<p>

@else
<h2>選択されている住所</h2>
<table>
<tr>
<th>氏名</th>
<th>郵便コード</th>
<th>都道府県</th>
<th>市区町村</th>
<th>住所</th>
<th>電話番号</th>
</tr>
<tr>
<td>{{$select_address['name']}}</td>
<td>{{$select_address['postal_code']}}</td>
<td>{{$select_address['prefectures']}}</td>
<td>{{$select_address['municipality']}}</td>
<td>{{$select_address['address']}}</td>
<td>{{$select_address['phone_number']}}</td>
</tr>
</table>
@endif

@if (!empty($user_carts) && !empty($select_address) && $total > 50)
<p style="color:red;">合計金額{{$total}}円。下記ボタンを押すと購入することができます.</p>
<div class="content">
<form action="{{ route('cart.charge') }}" method="POST">
{{ csrf_field() }}
<script
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="{{ env('STRIPE_KEY') }}"
data-amount="{{ $total }}"
data-name="Stripe Demo"
data-label="購入をする"
data-description="Online course about integrating Stripe"
data-image="https://stripe.com/img/documentation/checkout/marketplace.png"
data-locale="auto"
data-currency="JPY">
</script>
</form>
</div>
@else
<p>以下の条件に当てはまったら購入画面に進めません</p>

<p>カートに商品がない。住所が選択されてない。合計金額が50円以下</p>
@endif


<a href="{{route('index')}}">商品一覧ページ</a>
<a href="{{route('address.showSelectAddress')}}">住所選択ページ</a>
@endsection

その結果以下のような見た目になりました。

image.png

「決済をする」を押すと以下のようなエラーを吐き出します。

image.png

予定だと以下のような画面になってほしいのに、scriptの部分が動いていないことがわかります。

image.png

解決方法

以下の記事を参考にLaravelのデフォルトにある、app.jsとバッティングしている事がわかりました。
そのapp.jsを読み込んでいるのが、layout/app.blade.phpで読み込まれているのがわかりました。

そのため、viewでlayoutを読み込んでいる以下3行を削除しました。

@extends('layouts.app')

@section('content')

//省略

@endsection

無事動きました。めでたしめでたし

参考にした神記事

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

初心者がRailsとVue.jsでポートフォリオを作成してみた

こんばんは
アロハな男、やすのりです!

私は、自身のポートフォリオとして『Vue.js』と『Rails』を使用した、SPAアプリを作成しました。

この記事ではポートフォリオの内容や、実際に様々な機能実装をする上で『工夫したこと・苦労したこと』や、Webアプリ開発を通しての感想等をお伝えしていきたいと思います!

アプリ概要

一言で説明すると、ハワイに特化した『じゃらん』や『一休.com』 です。

Image from Gyazo

今のご時世では気軽に旅行することも難しいのですが、私自身や周りに旅行することが好きで、特にハワイが好きな方々がおり、実際に旅行した際の思い出やおすすめのお店等を共有し合うことがありました。

そこで、自身の思い出の場所やおすすめ店等を共有したいと思い、様々なサイトを探してみましたが、『管理人のおすすめ店をランキング形式で紹介』というものが多く、おすすめの場所が複数ある場合は、その分複数のサイトを共有しながら話し合うという様な形でした。

そういった経験から『もし、ハワイのお店やアクティビティ等がまとめられ、口コミ等でユーザーの評価がわかる様なサイトがあればお気に入りの場所もおすすめしやすくていいな』という想いから、このアプリを作成することにしました。

また、アプリ作成をする上で『説明がなくてもユーザーが使える見た目』であることや『ユーザーとしてアプリを使用する上で欲しい機能』を常に考え、『ユーザーが使いやすいアプリ』をコンセプトにアプリを作成しました。

下記ページにて公開中ですので、よろしければ実際に触ってみてください!
https://www.lets-enjoy-hawaii.com

使用技術等

  • フロントエンド
    • Vue.js 2.6.12
    • Vue Router
    • axios
    • JavaScript
    • HTML / CSS
    • Haml / SCSS※Vue.jsの導入に伴い使用中止
    • jQuery※Vue.jsの導入に伴い使用中止
  • バックエンド
    • Ruby 2.6.5
    • Rails 6.1.2.1
  • テスト
    • Rspec
    • FactoryBot
    • faker
    • rubocop
  • インフラ
    • AWS (VPC, EC2, S3, RDS, Route53)
    • https化
    • Nginx / Unicorn
  • ソースコード管理
    • GitHub

機能一覧

以降では、主要機能の詳細や工夫点等について紹介させていただきます!

入力フォームの入力不備表示機能

機能詳細
入力フォームの各項目に対して、モデルのバリデーション設定に反した入力がされるとエラーメッセージが表示され、入力欄の表示が変更されます。
※バリデーション設定に沿った入力がされた段階で、エラーメッセージや入力欄の表示が元に戻ります。

Image from Gyazo

工夫点

①入力不備の場所を判断できる様にする

入力項目が複数あるため、エラーメッセージを表示しただけでは、どの入力欄で入力不備が起こっているのか判断がつけづらいと考え、入力欄自体の表示も変更することにより、どの入力欄で不備が発生しているのかがすぐ判断できる様にしました。

②入力不備が解消されたかどうか一目でわかる配色にする

ページに使用している配色とは異なった色で入力不備の表示をすることにより、入力不備が解消されているかが一目でわかる様にしました。

会員登録済みメールアドレスお知らせ機能

機能詳細
会員登録時に入力されたメールアドレスが既に登録済みか判定し、登録済みであればエラーメッセージを表示させ、別のアドレスを入力するか、ログインを試すかを促す様にしています。

Image from Gyazo

懸念点

ユーザー登録数が増えてきた場合の処理速度問題

メールアドレスの入力がされ入力欄からフォーカスが外れた際に、axiosを使用した非同期通信を行い、メールアドレス判定用メソッドへリクエストを飛ばします。
現在は登録数が少なくレスポンスも早いですが、登録数が膨大になった際は時間がかかってしまうことが考えられますので、判定中なことをアイコンで表示させる等の処理が必要そうです。

詳しいコード解説等は別記事にて紹介しています。
RailsとVue.jsでメールアドレスが登録済みか判定する方法

登録済みのお気に入り・訪問記録へのメモ書き機能

機能詳細
登録済みのお気に入り訪問履歴に対して『〜が美味しかった!』や『次は〜動物をメインに見たい』等のコメントを、メモ書き感覚で保存することができます。

Image from Gyazo

工夫点

保存ボタンを押すことにより、コメント保存がされる様に設定

最初は各お店等へコメントでのメモ書きをする際に、『保存ボタン』をクリックしなくても、入力欄のフォーカスが外れた段階で保存処理をかければ、ユーザーの手間が1つ減るのではないかと考えていました。
しかしその仕様にした場合、『入力したけど、元々残していたメモ書きの方がやっぱりいい』や『誤ってメモ書きを削除してしまった』等といった場合に対応できないと考えを改め、『保存ボタン』も一緒に実装しました。

地図検索時、選択場所の表示変更機能

機能詳細
トップページの『地図から探す欄』と『島名から探す欄』にて島名等をホバーすると、対応した地図上の島の表示が変更されます。

Image from Gyazo

工夫点

①選択している島を視覚的にわかる様に設定

最初は地図上をホバーしても、マウスの表示が変わって地図を選択していることをお知らせしているだけでした。
しかし、地図上の表示も合わせて変更することによって、実際にユーザーが選択している箇所を把握することも容易になるのではないかと考え、地図上の表示変更機能を実装しました。

②島名にどこの島が対応しているのかわかる様に設定

島名検索をする際に、『オアフ島やハワイ島は聞いたことがあるけど、実際地図上ではどこになるんだろう』という声をいただき、実際に島名選択時にどの島と対応しているのかを地図表示を変更することでわかる様に設定しました。
これによって、『今回はオアフ島へ旅行に行くから、その横の島も時間があれば観光をする予定を立てよう』等の新たな計画を立てることもできるのではないかと思っています。

検索結果の並び替え機能

機能詳細
並び替え欄にある口コミランク順等をクリックすることで、表示されているお店等の検索結果一覧の並び替えをすることができます。

Image from Gyazo

工夫点

①初期状態の並び順をお気に入り数の多い順に設定

ユーザーが目的の場所を検索する際に評価の高い場所を優先的に見ていくと考え、口コミランク順かお気に入り数の多い順のどちらかを初期状態にしようと考えました。
その際に、『口コミ投稿をするより、お気に入りボタンを押す方が簡単でユーザーが使用する割合が高い』と考え、口コミ投稿に影響される『口コミランク順』ではなく、お気に入り数順を初期状態とすることにしました。

②並び替えボタンに使用頻度が高そうなものを用意

ユーザーが実際に並び替えをしたい項目にどういったものがあるかと考えました。
その際に『口コミランク順』と『お気に入り数順』は①でも説明した様に、ユーザーは評価の高いところを知りたいのではないかと思い、追加しました。
そして更に『口コミ数が多く、その場所に対しての感想が多い方が、よりその場所へのイメージがしやすい』のではないかと考え、『口コミ数順ボタン』を実装しました。

お店等への画像投稿機能(複数枚投稿可能)

機能詳細
お店等への口コミと一緒に、写真を投稿することもできます。
1回の投稿につき複数枚選択していただくことができ、現在どんな写真を選択しているのかをプレビューすることもできます。
そして、写真削除ボタンをクリックすることで、クリックした画像を選択画像から除外することができます。

Image from Gyazo

工夫点

写真削除ボタンをホバーした際に、ホバーした写真削除ボタンの表示を変更する。

写真を複数枚プレビューさせるためにVue.js上で繰り返し処理を行い各要素を表示させているため、そのままmouseoverイベントを発火させてしまうと、全ての写真削除ボタンの表示が変更されてしまう状態になっていました。
なので、削除ボタンにref属性を付与して、繰り返し処理のindex番号を与え、そのref属性とindex番号でホバーした要素を判断し表示を変更させることができました。

棒グラフ表示機能

機能詳細
お店等へ投稿された口コミの評価点の分布を、棒グラフで表示させています。
また、口コミが投稿されるとパーセントも更新され、グラフの表示も変動します。

Image from Gyazo

工夫点

パーセント表記に小数点を使用しない

各評価点のパーセンテージを計算した際に、どうしても割り切れず小数点が発生してしまいましたが、表示上はすっきりさせている方が見た際にわかりやすいと考え、小数点以下は四捨五入し表示をさせています。

Google APIを使用したお店等の画像取得機能

機能詳細
今のご時世ですので、実際に現地に行かなくても、お店の画像を取得し使用することができる『Google Street View API』を使用しました。

Image from Gyazo

問題点

①全ての場所は対応していない

画像自体はGoogle Streetにあれば取得することができますが、そもそもショッピングモールの中のお店等は表示されなかったりするので、画像が取得できずに掲載できないお店等も存在します。

②データベースに保存するカラムが増える

『Google Street View API』を使用するために、該当場所の座標等の情報をデータベースに保存しなければならないため、1つのレコードに使用される容量が増えてしまう。

以上のことから、可能であれば少しずつでも実際に現地で撮影した写真等を入手する必要があると考えています。

追加実装予定

GithubのIssuesに、実装内容と目的をまとめています。
https://github.com/Yasunori-aloha/lets-enjoy-hawaii/issues

ポートフォリオ開発で苦労したこと

特に苦労したことは、フロント部分とバック部分との連携でした。

当初はRailsだけで開発しており、フロント部分もRailsのビューを使用し実装していました。
しかし、現在主流なのは『フロント部分はVue.jsやReact等のJavaScriptフレームワークを使用してバック部分と切り分けて実装されるアプリケーション』であることや『フロントとバック両方の知識・技術があれば、実務に入った際のフロントとバック両チームのコミュニケーションの架け橋になれるのではないか』と考え、フロントをVue.jsへ切り替えを決心しました。

しかし、1度完成させていたものを新たに『APIモード』として作り替えるために、

  • APIモードでの必要情報の取り出し方
  • ビューをVue componentsとして移行

等々を新たに学習し直して、実装する必要がありました。

参考文献も少なく、スクール等では学べない内容でしたので、自分自身の成長により繋がったのではないかと思います。

ポートフォリオ開発を通しての得たもの

  • 自走力
    Railsだけでポートフォリオ完成させた段階では、どうやってVue.jsを連携させればいいのか全く知識がなかった状態からのスタートでしたので、導入の部分から実装部分に至るまで様々なエラー・不具合に遭遇してきました。
    しかし、Udemyの教材等を活用し、基礎的な内容から実践的なものまで学習を続けたことで、学習開始からポートフォリオへの導入まで約1ヶ月でVue.jsを実装することができました。
    そして、そのほとんど全てのタスクを自力で実装することができるまでになっていました。

  • デバック力
    ポートフォリオを作成する中で、様々なエラーに何度も遭遇してきました。
    ですが、その度にbinding.pryやdebugger等のデバックツールを使用して、『どこまで自身の想定内の動作をしているのか?』を明確にし、エラーの原因を絞り込み特定し解決、というサイクルを何度も経験し乗り越えてきました。
    今では、エラーに遭遇しても『今回はどういった理由でエラーが出てるのかな?』と、原因特定するこのサイクル自体を楽しんでいる自分がいました。

  • とにかく楽しむこと
    このポートフォリオを作成する中で、実装したい機能を実現するための方法を考えたり、考えたロジックをコードに落とし込んだり、エラーが発生した箇所はデバックをすることで原因を追及し解決する。
    こういった開発の流れ全てを楽しみながら開発を続けることができました。
    RailsだけでなくVue.jsも取り入れることで『フロント側ではこの情報が必要だからレスポンスで渡せる様にしよう』と考えたり、『バック側の処理でこの値が必要だからリクエストで送ってあげよう』と考えながら実装していくことでお互いの知識・技術も向上され、更にお互いのことを考えるための思考も養われたのではないかと思います。

最後に

ポートフォリオ開発を通して1番はじめに思い返されるのが『楽しくすることができた』ことです。
特に新たに実装したい機能が浮かんできて、その機能を実装するための方法を考えて、様々な教材で学習し実際に機能を実装できた時はとても大きな感動と達成感があったことを覚えています。

開発を始めたばかりの頃は、わからないことも多くエラーや実装方法で苦労することも多々ありましたが、実際に当初自身で実装したいと思っていた機能が実装することができて本当に良かったと思います。

まだまだ、開発過程で実装したいと思った機能がありますので、これからもアップデートを続けて、よりユーザーの使いやすいアプリケーションにできる様、そして自身の知識・技術が向上できる様に学習を続けていきたいと思います!

長い記事となってしまいましたが、ここまでご覧くださりありがとうございました!
もし、この記事ひいては開発したアプリケーションが皆様のお役に立つことができたのなら幸いです。

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

JavaScriptでcsvダウンロードを実装する方法

はじめに

フロント(React)でcsvダウンロードを実装する機会があったため、備忘録です。
どうぞご活用ください。

実装

  const handleDLcsv = async () => {
    //アイテムの定義
    const download_items = [
            {'id': 1, 'name': 'apple', 'price': 100},
            {'id': 2, 'name': 'orange', 'price': 120},
            {'id': 3, 'name': 'melon', 'price': 800}
    ];
    //csvヘッダー
    const array_data = [['id', 'name', 'price']];

    //文字コード
    const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);

    //csv用データ作成
    download_items.map((item) => {
      const item_data = [item.id, item.name, item.price];
      array_data.push(item_data);
    })
    let csv_data = array_data.map(function(l){return l.join(',')}).join('\r\n');

    //BLOB生成してダウンロード実行
    const blob = new Blob([bom, csv_data], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'csv_file.csv';
    const clickHandler = () => {
      setTimeout(() => {
        URL.revokeObjectURL(url);
        a.removeEventListener('click', clickHandler);
      }, 150);
    };
    a.addEventListener('click', clickHandler, false);
    a.click();
  }

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

ISR(Incremental Static Regeneration)とは?

ISR(Incremental Static Regeneration)とは?

Next.js のビルドにはいくつかパターンがあります。その中でNext.js 9.4 からIncremental Static Regeneration という機能が導入されました。

直訳すると、(段階的な静的サイト生成)となります。
簡単に説明すると、リクエストに対して静的にビルドされたページを返す。かつ、有効期限を超えたら非同期で静的ページの再生成をSSRで行うことです。

図1.png

メリットって?

  • 事前にすべてのページ生成はせず、1度リクエストされた際のレスポンス内容が生成される。
  • アクセス時に初めて生成されるので初回ビルドが高速になる。
  • 一定期間ごとにSSRを行うので、描画が高速になる。
  • CDNのキャッシュを有効活用しつつ、静的ページの更新を自動的に行え、一定時間後再度リクエストがあった場合、次回以降の内容をビルドするので内容が更新される。

VercelにDeployしてみる

ISRのpagesコンポーネントを作成

  • getStaticProps で revalidate を設定すると ISR になります。
  • revalidate の値は、前回から何秒以内のアクセスを無視するかを指定します。
pages/index.tsx
export default function Index({current}) {
  return (
    <div>
        現在時刻は{current}です
    </div>
  );
}

export async function getStaticProps() {
    const date = new Date();
    const current = date.toLocaleString()
  return {
    props: {
      current,
    },
    revalidate: 10,
  };
}

VercelにDeployしてみる

上記のコードをVercelにdeployして挙動を確認してみました。
コード的には、10秒間はキャッシュされたデータが返却され、10秒後に再描写され内容が更新されていることが確認できます。

output.gif

参考

Zenn 個人開発の限界に挑んだ話
Next.jsのIncremental Static RegenerationをVercel以外でやってみる

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

Zoomable Scatterplot (d3.js)

こんにちは。
"Zoomable Scatterplot" という記事を見つけたので、ウィンドウのリサイズにも対応させてみました。

  • Zoomable Scatterplot (observablehq.com): "The scatterplot allows zooming using the mouse or touch."

scatter.jpeg

scatterplot.html
<!DOCTYPE html>
<meta charset="utf-8">
<style>
    .axis path {
        display: none;
    }
    .axis line {
        stroke-opacity: 0.1;
        shape-rendering: crispEdges;
    }
    svg,
    #canvas {
        width: 100%;
        height: 100vh;
        display: block;
    }
</style>
<body>
    <div id="canvas"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.js"></script>
    <script>
    const nTick = 10;
    const rndXY = () => {
        const rnd = () => Math.random() * 2 - 1;
        const x = rnd()*100;
        const y = rnd()*100;
        return [x , y, (x>0?0:1)+(y>0?0:2)]};
    const data = Array.from({length: 100}, rndXY);

    const canvas = document.querySelector('#canvas');
    const svg = d3.select("#canvas").append('svg');
    const gX = svg.append("g").attr("class", "axis axis--x");
    const gY = svg.append("g").attr("class", "axis axis--y");
    const zScale = d3.scaleOrdinal()
        .domain(data.map(d => d[2]))
        .range(d3.schemeCategory10);
    const gDot = svg.append("g")
        .attr("fill", "none")
        .attr("stroke-linecap", "round")
        .selectAll("path")
        .data(data)
        .join("path")
        .attr("stroke", d => zScale(d[2]));
    let transform = d3.zoomIdentity;

    const render = () => {
        const { width, height } = canvas.getBoundingClientRect();
        const xScale = d3.scaleLinear()
            .domain([-width / 2, width / 2])
            .range([0, width]);
        const yScale = d3.scaleLinear()
            .domain([-height / 2, height / 2])
            .range([height, 0]);

        const xAxis = d3.axisBottom(xScale)
            .ticks((width + 2) / (height + 2) * nTick)
            .tickSize(height)
            .tickPadding(-18);
        const yAxis = d3.axisRight(yScale)
            .ticks(nTick)
            .tickSize(width)
            .tickPadding( -width);
        gDot.attr("d", d => `M${xScale(d[0])},${yScale(d[1])}h0`);

        const zoomed = (event, d) => {
            transform = event.transform;
            gX.call(xAxis.scale(transform.rescaleX(xScale)));
            gY.call(yAxis.scale(transform.rescaleY(yScale)));
            gDot.attr("transform", transform).attr("stroke-width", 5 / transform.k);
        }

        const zoom = d3.zoom().on("zoom", zoomed);
        svg.call(zoom).call(zoom.transform, transform);
    };

    render();
    window.addEventListener('resize', render);
    </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

漢字(中国語)を左右に分割して表にしてみた&百度百科(≒中国版wikipedia)へリンクしてみた/グレートファイアウォールの体験談

1. つくってみた

1.1. 動機 ~ 中国の漢字が読めない・・・

中国関連の仕事にどっぷり浸かっているのですが、漢字がわからん。。。ということで整理がてら作ってみた。
※へん(偏)とつくり(旁)の分割は完全に手作業です。辞書の索引見ながらつくりました。ピンイン入力も手作業なので間違ってる可能性あります。
数が多すぎるので、4画までしか作っていない。偏の選定はテキトウ。

1.2. できたもの

See the Pen ExNXVGJ by kob58im (@kob58im) on CodePen.

1.3. テーブル変換用ツール(C#)

漢字テーブルを持たせるにあたり、JavaScriptの配列を手入力で作成するのはしんどいので、1.4章のテキストを1.3章のプログラムで変換をかけた。

コマンドプロンプトにおいてchcp 65001でUTF-8にしておかないと、consoleにメッセージ吐いた場合に、日本語にない漢字が文字化けする。(ただし、IOException発生時にエラーメッセージのToString()に失敗する副作用がある模様。。)

テーブル変換用ツール(C#)
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

class ChConverter
{
    [STAThread]
    static void Main(string[] args)
    {
        Regex rSection = new Regex(@"^===(.*)===$");
        Regex rNumberOfStroke = new Regex(@"^---([0-9]+)---$");
        Regex rKeyCharPair = new Regex(@"^([^\t]+)(?:\t([^\t]+)(?:\t([^\t]+))?)?$");
        Regex rCharPair = new Regex(@"^([^\t]+)\t([^\t]+)\t([^\t]+)(?:\t([^\t]+))?$");

        string[] lines = File.ReadAllLines("Ch.txt");
        var sb = new StringBuilder();
        int state = 0;
        int lineIndex = 1;
        int lastStrokeN = 0;
        var keyDict = new Dictionary<string,bool>();

        sb.Append("const kt = [");
        for ( ; lineIndex < lines.Length; lineIndex++ ) {
            string s = lines[lineIndex].Trim();
            Match m;
            if ( s == "" ) {
                // do nothing
            }
            else if ( s == "#KeyOrderEnd" ) {
                lineIndex++;
                break;
            }
            else if ( s.StartsWith("#") ) {
                // do nothing
            }
            else if ( (m = rNumberOfStroke.Match(s)).Success ) {
                // do nothing
            }
            else if ( (m = rKeyCharPair.Match(s)).Success ) {
                string keyKanji = m.Groups[1].Value;
                string pinyin  = GetNthValue(m, 2);
                string baikeId = GetNthValue(m, 3);

                sb.Append("['" + keyKanji + "'");

                if ( !String.IsNullOrEmpty(pinyin) ) {
                    sb.Append(",'" + pinyin + "'");
                    if ( !String.IsNullOrEmpty(baikeId) ) {
                        sb.Append(",'" + baikeId + "'");
                    }
                }
                sb.Append("],");

                if ( keyDict.ContainsKey(keyKanji) ) {
                    Console.WriteLine("Warning: duplicated key. line " + (lineIndex+1).ToString());
                }
                else {
                    keyDict.Add(keyKanji, true);
                }
            }
        }
        sb.AppendLine("];");

        sb.AppendLine("const ct = [");
        for ( ; lineIndex < lines.Length; lineIndex++ ) {
            string s = lines[lineIndex].Trim();
            Match m;
            if ( s == "" ) {
                // do nothing
            }
            else if ( s.StartsWith("#") ) {
                // do nothing
            }
            else if ( (m = rSection.Match(s)).Success ) {
                if ( state != 0 ) {
                    sb.AppendLine("]],");
                }
                state = 1;
                sb.Append("['");
                sb.Append(m.Groups[1].Value);
                sb.Append("',[");
            }
            else if ( (m = rNumberOfStroke.Match(s)).Success ) {
                if ( state == 0 ) {
                    Console.WriteLine("Format error. line " + (lineIndex+1).ToString());
                    return;
                }
                state = 2;
                lastStrokeN = Convert.ToInt32(m.Groups[1].Value);
            }
            else if ( (m = rCharPair.Match(s)).Success ) {
                if ( state == 0 || state == 1 ) {
                    Console.WriteLine("Format error. line " + (lineIndex+1).ToString());
                    return;
                }
                string key    = m.Groups[1].Value;
                string kanji  = m.Groups[2].Value;
                string pinyin = m.Groups[3].Value;
                string baikeId = GetNthValue(m, 4);
                sb.Append("['" + key + "','" + kanji + "','" + pinyin + "'");
                if ( !String.IsNullOrEmpty(baikeId) ){
                    sb.Append(",'" + baikeId + "'");
                }
                sb.Append("],");
                if ( !keyDict.ContainsKey(key) ) {
                    Console.WriteLine("Warning: no key. line " + (lineIndex+1).ToString() + "\t" + lastStrokeN.ToString()+ "\t"+key);
                    keyDict.Add(key,false);
                }
            }
            else {
                Console.WriteLine("Format error. line " + (lineIndex+1).ToString());
                return;
            }
        }
        if ( state != 0 ) {
            sb.AppendLine("]]");
        }
        sb.AppendLine("];");

        try {
            File.WriteAllText("ch_for_js.txt", sb.ToString());
        }
        catch ( IOException e ) {
            Console.WriteLine( "IOException" );
            Console.WriteLine( e );
        }
    }

    static string GetNthValue(Match m, int index)
    {
        if ( m.Groups.Count > index && m.Groups[index] != null && m.Groups[index].Value != null ) {
            return m.Groups[index].Value;
        }
        return null;
    }
}

1.4. 入力データ(抜粋)

入力データ(抜粋)
ch.txt
#KeyOrderBegin
---0---
x
---1---
乙 yǐ
x扎
---2---
丁 dīng
丩 jiū
乃 nǎi
九 jiǔ
了 le/liào
二 èr
人 rén
八 bā
几 jī
刀 dāo
刁 diāo
力 lì
十 shí
卜 bǔ
又 yòu
x汇
x朽
x化
x叩
(中略)
反 fǎn
壬 rén    5878659
(中略)
#KeyOrderEnd

===言===
---0---
x   言 yán
---2---
丁 订 dìng
卜 讣 fù
几 讥 jī
十 计 jì
人 认 rèn
---3---
己 记 jì
上 让 ràng
寸 讨 tǎo
x讬    讬 tuō
(以下略)

1.5. 参考サイト

2. グレートファイアウォールの話

※環境に依存する可能性があります。また、今後どうなるかも分かりません。
※事前に調べてみた感じだと、VPNのサーバーによっては、締め出しをくらっている場合があるという記載を見かけました。

なお、かなり遅い&たびたび切断される。。

2.1. VPNつないでもダメだったこと

  • PCからだとgoogle検索(google.co.jp, google.com)にアクセスできず。。
  • PCからだとgmailのメールが送信エラーとなる。。めちゃくちゃ時間経ってから送信できたりもする。(受信は(VPNつなげば)普通にできた。)

2.2. VPNつながないとダメだったこと

  • スマホ/タブレットからgoogle検索
  • スマホ/タブレットからYouTube

2.3. VPNつながなくてもできたこと

  • Qiitaへのアクセス
  • CodePen(codepen.io)へのアクセス
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptでモーダルを作ろう!

モーダルとは

 最初に、本記事で作成したいものの完成形を、画像で見ていきましょう。

 ①最初の画面では、以下のようにボタンが1つだけある状態です。

②「画像を表示」ボタンをクリックすると以下のように画像が表示されます。
スクリーンショット 2021-02-21 16.40.32.png

モーダルの構造

 モーダルは、ボタンを押したときに、黒い背景、画像、閉じるボタンの3つの要素を表示し、閉じるボタンを押すと3つの要素が表示さなくなるような構造になっています。
 このような動きは、javascriptでHTMLの要素を取得し、classを付けたり消したりすることで実装できます。

コードの全体像

 それではモーダル実装するために必要なコードの全体を見ていきましょう。
まずはHTMLです。

index.html
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>modal</title>
    <script src="https://kit.fontawesome.com/f08e8f441f.js " crossorigin="anonymous"></script>
    <link rel="stylesheet" type="text/css" href="app.css">
</head>
<body>
    <div id="modal" class="modal">
        <div class="modal_inner">
            <div id="modal_close" class="modal_close"><i class="fas fa-times"></i></div>
            <img src="image001.jpg">
        </div>
        <div id="modal_background" class="modal_background"></div>
    </div>
    <button id="modal_show" class="button" >画像を表示</button>

<script src="main.js"></script>   
</body>
</html>

 次にCSSです。

app.css
.modal {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  -webkit-transition: .6s;
  transition: .6s;
}

.modal.is-show {
  opacity: 1;
  visibility: visible;
}

.modal_inner {
  position: absolute;
  left: 50%;
  top: 50%;
  -webkit-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
  width: 80%;
  max-width: 600px;
  padding: 50px;
  background-color: #fff;
  z-index: 2;
}

.modal_inner img {
  width: 100%;
}

.modal_close {
  position: absolute;
  right: 0;
  top: 10px;
  width: 50px;
  height: 50px;
  line-height: 50px;
  text-align: center;
  cursor: pointer;
}

.modal_close i {
  font-size: 20px;
  color: #333;
}

.modal_background {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.8);
  z-index: 1;
  cursor: pointer;
}

.button{
  font-size: 20px;
}

 最後にjavascriptです。

main.js
'use strict';
const modal = document.getElementById('modal');
const show = document.getElementById('modal_show');
const close = document.getElementById('modal_close');
const backGround = document.getElementById('modal_background');

show.addEventListener('click',() => {
    modal.classList.add('is-show');
    backGround.classList.add('is-show');
})

close.addEventListener('click',() => {
    modal.classList.remove('is-show');
    backGround.classList.remove('is-show');
})

backGround.addEventListener('click',() => {
    close.click();
})

コードの解説

 全体を確認していただいたので、次にコード内容を詳しく見ていきます。

HTMLについて

HTMLは大きく2つに別れています。
 
①表示される中身。
②画像を表示するためのボタン。
 モーダルの中身も2つに別れており、modal_innerの中に、「画像」と「閉じるボタン」のアイコンが入っています。アイコンはフォントオーサムのもの使っていますので、head部分で読み込みをしています。また、表示する画像については好きなものを使用してください。

index.html
<!-- ① -->
<div id="modal" class="modal">
        <div class="modal_inner">
            <div id="modal_close" class="modal_close"><i class="fas fa-times"></i></div>
       <!-- ↑閉じるボタンのアイコン -->
            <img src="">
       <!-- ↑表示する画像 -->
        </div>
        <div id="modal_background" class="modal_background"></div>
       <!-- ↑背景 -->
 </div>
 <!-- ① -->
 <button id="modal_show" class="button" >画像を表示</button>

javascriptについて
 
javascriptは大きく3つの部分に別れています。
①HTMLから②③で、clickイベントを設定するためにDOM要素を取得し、定数として設定している。

②modal_showのidを持つdivタグをクリックすると、is-showのclassをつけるようイベントを設定している。

③modal_closeのidを持つdivタグにクリックすると、②でつけたis-showのclassを消すようイベントを設定している。

 main.js
//①
const modal = document.getElementById('modal');
const show = document.getElementById('modal_show');
const close = document.getElementById('modal_close');
const backGround = document.getElementById('modal_background');

//②
show.addEventListener('click',() => {
    modal.classList.add('is-show');
    backGround.classList.add('is-show');
})

//③
close.addEventListener('click',() => {
    modal.classList.remove('is-show');
    backGround.classList.remove('is-show');
})

cssについて

 cssについては重要な箇所のみ解説します。


 以下の箇所ではmodalの表示する部分を装飾しています。
 visibility: hidden;
として、普段はモーダルが見えないようにしています。
 -webkit-transition: .6s;
transition: .6s; として、クリックされた時にすぐに画像が表示されるのではなく、0.6秒かけてゆっくり表示されるように設定しています。

app.css
.modal {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  -webkit-transition: .6s;
  transition: .6s;
}


 以下の箇所では①で見えないようにしていた箇所を見えるようにするよう装飾しています。
 visibility: visible; として、①で見えなくしていたモーダルの中身を見えるように上書きしています。
 is-show のclassについてはjavascriptでイベントを設定し、クリックすることで追加あるいは削除できるようにしています。

app.css
.modal.is-show {
  opacity: 1;
  visibility: visible;
}


 modal_backgroundのclassを持つ部分のz-index: 1; に、modal_innerのclassを持つ部分をz-index: 2; として、背景の上に画像が表示されるように設定しています。

最後に

 いかがでしょうか、説明は最低限となってしまいましたが、この記事を参考にして、ぜひモーダルを作ってみてください。

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

<初心者向け>javascriptでモーダルを作ろう!

モーダルとは

 最初に、本記事で作成したいものの完成形を、画像で見ていきましょう。

 ①最初の画面では、以下のようにボタンが1つだけある状態です。

②「画像を表示」ボタンをクリックすると以下のように画像が表示されます。
スクリーンショット 2021-02-21 16.40.32.png

モーダルの構造

 モーダルは、ボタンを押したときに、黒い背景、画像、閉じるボタンの3つの要素を表示し、閉じるボタンを押すと3つの要素が表示さなくなるような構造になっています。
 このような動きは、javascriptでHTMLの要素を取得し、classを付けたり消したりすることで実装できます。

コードの全体像

 それではモーダル実装するために必要なコードの全体を見ていきましょう。
まずはHTMLです。

index.html
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>modal</title>
    <script src="https://kit.fontawesome.com/f08e8f441f.js " crossorigin="anonymous"></script>
    <link rel="stylesheet" type="text/css" href="app.css">
</head>
<body>
    <div id="modal" class="modal">
        <div class="modal_inner">
            <div id="modal_close" class="modal_close"><i class="fas fa-times"></i></div>
            <img src="image001.jpg">
        </div>
        <div id="modal_background" class="modal_background"></div>
    </div>
    <button id="modal_show" class="button" >画像を表示</button>

<script src="main.js"></script>   
</body>
</html>

 次にCSSです。

app.css
.modal {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  -webkit-transition: .6s;
  transition: .6s;
}

.modal.is-show {
  opacity: 1;
  visibility: visible;
}

.modal_inner {
  position: absolute;
  left: 50%;
  top: 50%;
  -webkit-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
  width: 80%;
  max-width: 600px;
  padding: 50px;
  background-color: #fff;
  z-index: 2;
}

.modal_inner img {
  width: 100%;
}

.modal_close {
  position: absolute;
  right: 0;
  top: 10px;
  width: 50px;
  height: 50px;
  line-height: 50px;
  text-align: center;
  cursor: pointer;
}

.modal_close i {
  font-size: 20px;
  color: #333;
}

.modal_background {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.8);
  z-index: 1;
  cursor: pointer;
}

.button{
  font-size: 20px;
}

 最後にjavascriptです。

main.js
'use strict';
const modal = document.getElementById('modal');
const show = document.getElementById('modal_show');
const close = document.getElementById('modal_close');
const backGround = document.getElementById('modal_background');

show.addEventListener('click',() => {
    modal.classList.add('is-show');
    backGround.classList.add('is-show');
})

close.addEventListener('click',() => {
    modal.classList.remove('is-show');
    backGround.classList.remove('is-show');
})

backGround.addEventListener('click',() => {
    close.click();
})

コードの解説

 全体を確認していただいたので、次にコード内容を詳しく見ていきます。

HTMLについて

HTMLは大きく2つに別れています。
 
①表示される中身。
②画像を表示するためのボタン。
 モーダルの中身も2つに別れており、modal_innerの中に、「画像」と「閉じるボタン」のアイコンが入っています。アイコンはフォントオーサムのもの使っていますので、head部分で読み込みをしています。また、表示する画像については好きなものを使用してください。

index.html
<!-- ① -->
<div id="modal" class="modal">
        <div class="modal_inner">
            <div id="modal_close" class="modal_close"><i class="fas fa-times"></i></div>
       <!-- ↑閉じるボタンのアイコン -->
            <img src="">
       <!-- ↑表示する画像 -->
        </div>
        <div id="modal_background" class="modal_background"></div>
       <!-- ↑背景 -->
 </div>
 <!-- ① -->
 <button id="modal_show" class="button" >画像を表示</button>

javascriptについて
 
javascriptは大きく3つの部分に別れています。
①HTMLから②③で、clickイベントを設定するためにDOM要素を取得し、定数として設定している。

②modal_showのidを持つdivタグをクリックすると、is-showのclassをつけるようイベントを設定している。

③modal_closeのidを持つdivタグにクリックすると、②でつけたis-showのclassを消すようイベントを設定している。

 main.js
//①
const modal = document.getElementById('modal');
const show = document.getElementById('modal_show');
const close = document.getElementById('modal_close');
const backGround = document.getElementById('modal_background');

//②
show.addEventListener('click',() => {
    modal.classList.add('is-show');
    backGround.classList.add('is-show');
})

//③
close.addEventListener('click',() => {
    modal.classList.remove('is-show');
    backGround.classList.remove('is-show');
})

cssについて

 cssについては重要な箇所のみ解説します。


 以下の箇所ではmodalの表示する部分を装飾しています。
 visibility: hidden;
として、普段はモーダルが見えないようにしています。
 -webkit-transition: .6s;
transition: .6s; として、クリックされた時にすぐに画像が表示されるのではなく、0.6秒かけてゆっくり表示されるように設定しています。

app.css
.modal {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  -webkit-transition: .6s;
  transition: .6s;
}


 以下の箇所では①で見えないようにしていた箇所を見えるようにするよう装飾しています。
 visibility: visible; として、①で見えなくしていたモーダルの中身を見えるように上書きしています。
 is-show のclassについてはjavascriptでイベントを設定し、クリックすることで追加あるいは削除できるようにしています。

app.css
.modal.is-show {
  opacity: 1;
  visibility: visible;
}


 modal_backgroundのclassを持つ部分のz-index: 1; に、modal_innerのclassを持つ部分をz-index: 2; として、背景の上に画像が表示されるように設定しています。

最後に

 いかがでしょうか、説明は最低限となってしまいましたが、この記事を参考にして、ぜひモーダルを作ってみてください。

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

フロントエンド新規プロジェクトに採用したほうがいいライブラリ

1_HSisLuifMO6KbLfPOKtLow.jpeg
この記事では良さそうなフロントエンドライブラリをいくつか紹介したいと思います。
Reactの画像を拾ってきましたが、Vueのプロジェクトでも使えます。

筆者の実行環境

node npm react
v14.15.0 v6.14.8 v17.0.1
  • Reactの使用言語はTypeScriptにします。
npx create-react-app [プロジェクト名] --template typescript 

prettier

prettierはコード整形ツール、馴染身のある人はかなり多いと思います。
TypeScript使用上の変更点とESLintとの併用などを紹介します。
公式ドキュメント

ライブラリインストール。

npm install --save-dev --save-exact prettier
or
yarn add --dev --exact prettier

prettier用のconfigファイルを作成。

echo {}> .prettierrc.json

整形対象外のものは.prettierignoreを新規作成して、中に対象ディレクトリ/ファイルを指定できます。

整形の自動化

自動化できるものは自動化してきましょう。

自動化用のライブリをインストール。

npx mrm lint-staged

インストール完了後、package.jsonに以下の内容が追加されたと思います。

:package.json
  ...
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
    }
  },
  "lint-staged": {
    "*.{js,css,md}": "prettier --write"
  }

huskyはgitフックの管理ツール。
追加された内容は git pre-commitの段階でprettierを実行し、コード整形してくれます。
ReactでTypeScriptを使用する場合には、以下の追加修正を加えましょう。
Vueの場合にはtsxは不要です。

...
"lint-staged": {
  "*.{js,css,md,ts,tsx}": "prettier --write"
}
...

ESLintとの併用処理

ESLintと併用処理するためのライブラリのインストール。

npm install --save-dev eslint-config-prettier

package.jsonのeslintConfigの項目に prettierを足して完了です。

  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest",
      "prettier"
    ]
  },

commitlint

コミットメッセージは、たとえ個人開発でも、ちゃんとした意味を持たせるべきです。
単にfixのようなメッセージでコミットしたら、たとえ本人でも、時間が経てばどんな変更をしたのかすぐに忘れてしまい、わからなくなるでしょう。
そういったコミットメッセージを自動で弾いてくれるライブラリがcommitlintです。
リポジトリ

インストール。

npm install --save-dev @commitlint/{config-conventional,cli}
or
yarn add @commitlint/{config-conventional,cli}

configファイルを初期化します。

echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

package.jsonhuskyhooks項目に下記の内容を追記します。

package.json
{
  "husky": {
    "hooks": {
      ...
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

設定は以上で完了しました。
この状態ではコミットメッセージは以下のルールを従う必要があります。

タグ名: コミットメッセージ

使用できるタグ名は以下から選べます。

- build
- ci
- chore
- docs
- feat
- fix
- perf
- refactor
- revert
- style
- test

commitlintが機能してる状態で、
タグを使用せずにコミットメッセージhogeでコミットしてみたら、ちゃんと弾いてくれました。

> git commit -m "hoge"
husky > pre-commit (node v14.15.0)
✔ Preparing...
✔ Hiding unstaged changes to partially staged files...
✔ Running tasks...
✔ Applying modifications...
✔ Restoring unstaged changes to partially staged files...
✔ Cleaning up...
husky > commit-msg (node v14.15.0)
⧗   input: hoge
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings

また、コミットメッセージのルールカスタマイズもできます。

例えば、コミットメッセージを全部大文字に限定したい場合、commitlinnt.config.jsを下記のように修正を加えます。

commitlint.config.js
module.exports = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "type-case": [2, "always", ["upper-case"]],
  },
};

この状態で小文字のコミットメッセージでしたら、弾かれます。

> git commit -m "fix: xxx"
husky > pre-commit (node v14.15.0)
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up...
husky > commit-msg (node v14.15.0)
⧗   input: fix: xxx
✖   type must be upper-case [type-case]

✖   found 1 problems, 0 warnings

その他のルールはこちらを参考にしてください。

json-server

mockの方法、色々試しましたが、結果json-serverにたどり着きました。
公式の説明に下記もの書かれてました。

5A3370B9-CCF6-403C-B4A3-438B1D3D87C3_4_5005_c.jpeg

30秒未満のコーディングでゼロから完全なRESTAPI mockサーバーを入手できる、
実際試してみましたが、確かに30秒未満でした。

インストール

npm install -g json-server

インストール終了後、プロジェクトに db.jsonというファイルを作ってください。
そして中身に下記の内容を追加してください。

db.json
{
  "users": []
}

json-serverを起動します。

json-server --watch db.json

起動したら、下記のログが表示されます。

  ...
  Resources
  http://localhost:3000/users
  ...

この状態でサーバにリクエスト送ると、空のレスポンスが返ってきます。
db.jsonは空だから当然の結果です。

17067A2E-1A67-4E09-92F8-9B65E6D547CA.jpeg

次はデータを追加してみます。

C47AE9CC-F73E-4970-9867-E9A5596904D6.jpeg

レスポンスを見ればわかりますが、idは自動的に付与されました、非常に便利です。
db.jsonにも追加されたデータが反映されました。

db.json
{
  "users": [
    {
      "name": "jack",
      "id": 1
    }
  ]
}

データを修正してみます。
id = 1nameroseに修正し、PUTリクエストを送信します。

3969E9C3-E58D-4928-BC87-EEA3517878D6.jpeg

db.jsonのデータが修正されたことが確認できました。

db.json
{
  "users": [
    {
      "name": "rose",
      "id": 1
    }
  ]
}

json-serverのプロダクト中の運用

あくまで個人的にやっていることです、参考までに読んでいただけると幸いです。

db.jsonの扱い方

json-serverにとって、db.jsonは仮のデータベースのようなものです。
プロジェクトのディレクトリに置いてるだけだとやや不適切です。

__json_server_mock__フォルダを親ディレクトリに作っておき、db.jsonを中に入れましょう。
入れたら下記のようなディレクトリ構成になります。

|-- [プロジェクト名]
...
|-- |-- src など
...
|-- |-- __json_server_mock__
|-- |-- |-- db.json

フォルダ名を __で囲むことで、中に入ってる物はプロジェクトのサポート役として直接影響しないことを明記するという意味です。

次はpackage.jsonにscriptを足します。

package.json
...
  "scripts": {
    ...
    "json-server": "json-server __json_server_mock__/db.json --watch --port 3001"
  }
...

追加後 npm run json-serverコマンドでjson-serverを立ち上げられます。

他にswaggerのようなmockサーバも存在したりしますが、
基本バックエンドエンジニアが用意していただけないと無理そうなので、納期が厳しい仕事でしたら、あんまり期待しない方が良いですね。

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

詳細画面での進捗率ゲージの作成

はじめに

オリジナルアプリでクラウドファンディングサイトを作成した際に、
jQueryを使って進捗率ゲージを作成したので紹介します。
初学者なのでより良い方法が他にもあるかもしれませんが、どなたかの参考になれば嬉しいです。
Image from Gyazo

環境

・Rails 6.0.0
・Ruby 2.6.5
・jQuery

作成手順

【view】

details.html.haml
.contents-pacent
  .contents-pacent__box1
    進捗率
  .contents-pacent__box2
    - @percent = number_to_percentage(@total.to_f/@project.target_amount*100,precision: 1)
    = @percent
    .contents-pacent__box2__graph1
      .contents-pacent__box2__graph2
        %input{name: "percent", type: "hidden", value:@percent, class: 'percent' }

①ゲージを挿入したい部分にクラスを設定します。
 →.contents-pacent_box2_graph2の部分

②inptを用いてDBのインスタンス変数の値を取得します。
 →inputのvalueに変数を設定することでjQueryで変数を使えるようにします。
  ※今回は変数の詳細は割愛します。
 参考記事:https://qiita.com/Kohei_Kishimoto0214/items/d919b00d75dec0699cf0

【CSS】

_projects_details.scss
.contents-pacent__box2__graph2{
  background-color: #ea662d;
  height: 15px;
  border-radius: 6px;
  max-width: 100%;
}

③CSSは以上の通りです。
 進捗率が100%以上になっても突き抜けてしまわないように
 max-width: 100%;を設定しています。

【jQuery】

project_detail.js
$(function() {
  let percent = $( ".percent" ).val();
  window.onload = function(){
    $(".contents-pacent__box2__graph2").css({ 'width' : percent } );
    }
});

③viewのinputの値を取得します。
④リロードのタイミングで.contents-pacent_box2_graph2のwidthの値を@percentからとるように設定します。

最後に

詳細画面は以上のようなシンプルな設定でゲージの作成ができました。
しかしトップ画面の並んでいるプロジェクトそれぞれに進捗率を反映させるのには少し手間取りましたので
次回紹介できればと思っています。

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

IoTデータの可視化サービス「Ambient」を試す(Node.js のパッケージを利用)

以前からサービスについて知っていたものの、クラウドサービスでのデータ可視化はあまりやってこなかったのもあり、まだ利用をしたことはなかった IoTデータ可視化サービスの「Ambient」
(これまでは、HTML+JavaScript でグラフ描画ライブラリを使っての可視化をやってた)

今回、よく自分が利用しているデバイス・サービスで利用する場合はどうすれば使えるのかを見てみたり、実際に Node.js のパッケージを使ってデータの送信と可視化の部分を試してみたりしました(送ったデータはランダムに生成したダミーデータを利用)。

Ambient の概要

既に Qiita でもタグがあり、記事もいくつも書かれていたりしますが、自分の理解を深めるためにも以下の公式ページをざっくり見ていって、その内容をメモしてみました。

●Ambient – IoTデータ可視化サービス
 https://ambidata.io/

構成図.jpg

可視化の例として、公式ページのトップに以下のような事例が出ています。
可視化の例.jpg

利用するための方法(情報を見てみる&チャネルを作成してみる)

Ambient を使うためのおおまかな手順は、以下の公式のチュートリアル(その中の1つ目)に書かれています。

●Ambientを使ってみる – Ambient
 https://ambidata.io/docs/gettingstarted/

【利用するための手順】
1. ユーザー登録(無料)
2. チャネル生成
3. マイコン側プログラミング
4. データー送信
5. 可視化(グラフ化)

ユーザ登録とチャネル作成

最初は公式サイト上でユーザ登録を行い、その後は可視化対象となるデータを管理するための「チャネル」を作成するところまでは、公式サイト上で操作を行っていけば良さそうです。

以下は、サイト上で「チャネルを作る」ボタンを押した後の状態です。
Ambient_チャネル一覧.jpg

チャネルが 1つ追加され、データ送信などに使うキーなどが発行されました。

マイコン側プログラミング

記事の途中で書いた手順では「マイコン側プログラミング」という項目で書かれてますが、公式情報の別のページ(以下のページ)を見ると、複数のテキストプログラミング言語・ビジュアルプログラミング環境からデータの送信を行えるようです。

●ライブラリー/リファレンス – Ambient
 https://ambidata.io/refs/

  • 上記のページに掲載されているもの
    • Arduino / C++
    • mbed / C++
    • Python / MicroPython
    • node.js
    • Node-RED

また、M5Stackシリーズをビジュアルプログラミングで扱える UIFlow についても、GitHub で公開された独自ブロックを使うとデータの送信を行えるようです。

●UIFlow(Blockly)でAmbientにデータを送る – Ambient
 https://ambidata.io/samples/m5stack/uiflow/
●GitHub - AmbientDataInc/UIFlow
 https://github.com/AmbientDataInc/UIFlow

自分が個人的によく利用しそうな方向だと「UIFlow・Node-RED・Node.js」あたりになりそうです。

データ送信・可視化

データ送信元として様々な環境が利用できる中で、今回は Node.js を使ってマイコンを利用せずに試してみようと思います。

●node.jsライブラリー ambient-lib – Ambient
 https://ambidata.io/refs/node-js/

Ambient を試す

現時点までで、 Ambient上でチャネルを作成するところまでは試しました。
チャネル作成時に発行されたキー等の情報を使い、Node.js から可視化用のデータを送ってみようと思います。

Node.js で簡単なお試しまで

公式の情報を見つつ、パッケージのインストールから始めていきます。

パッケージのインストール

$ npm install ambient-lib

プログラムからデータを送る

ライブラリの読み込みや接続・データ送信を行う処理は、以下のように書くようです。

// ライブラリの読み込み
var ambient = require('ambient-lib');
// Ambientへの接続
ambient.connect(チャネルId, ライトキー[, リードキー[, ユーザーキー]]);
// Ambientへのデータ送信
ambient.send(data, callback(err, res, body));

データ送信に関しては、例えば上記の data の部分に var data = {d1: 1.1, d2: 2.2}; という形などで指定するようです。2つのデータを送る場合のサンプルとして、以下の内容が公式に書かれていました。

ambient.send({d1: 1.1, d2: 2.2}, function(err, res, body) {
  if (err) {
    console.log(err);
  }
  console.log(res.statusCode);
});

上記の情報を活用しつつ、とりあえず d1 のみに 1回だけ数値を送る、ということをやってみます。
プログラムは以下のとおりで、 ambient.connect では「チャネルId・ライトキー」の 2つのみを指定しました。 d1 に送る数値は 1.1 を指定しています。

const ambient = require("ambient-lib");

ambient.connect(ご自身のチャネルId, "【ご自身のライトキー】");

ambient.send({ d1: 1.1 }, function (err, res, body) {
  if (err) {
    console.log(err);
  }
  console.log(res.statusCode);
});

上記を実行してみた後に Ambient のチャネルを見てみると、以下の画像のような表示がされていました。うまく数値を送信することができていそうです。
値を1つだけ送ってみる.jpg

Node.js でのお試し(続き)

Node.js でプログラムを書く

無事にデータを送ることができているようなので、次に連続したデータ送信を行ってみます。何かセンサー付きのデバイスから値を取得する方法もありますが、今回は簡単に試せるように Node.js上でセンサーの値に見立てた乱数を生成して、それを送信するという形にしてみています。

先ほどと別のチャネルを作り、以下の Node.js のプログラムを使ってデータを送ってみました。今回のプログラムでは、 d1d2 の 2種類のデータを送るようにしてみました(※ 送信する値は乱数で生成)。

const ambient = require("ambient-lib");

ambient.connect(ご自身のチャネルId, "【ご自身のライトキー】");

const intervalFunc = function () {
  ambient.send({ d1: Math.random() * 100, d2: Math.random() * 70 }, function (err, res, body) {
    if (err) {
      console.log(err);
    }
    console.log(res.statusCode);
  });
};

setInterval(intervalFunc, 5500);

データの送信間隔についてですが、以下の公式情報を見ると 「送信から次の送信まではチャネルごとに最低5秒空ける必要があります。それより短い間隔で送信したものは無視されます」 とあるため、5秒より少しだけ長い間隔を設定しました。

●諸元/制限 – Ambient
 https://ambidata.io/refs/spec/

また、1日に登録可能なデータ数の制限について 「1チャネルあたり1日3,000件までデーターを登録できます。平均すると28.8秒に1回のペースです。」 と書かれているため、1日じゅうデータを送る場合はデータの送信間隔をさらに長くする必要があるようです。

チャネルで 2つのデータを同一のグラフ内に表示させてみる

以下の公式情報の「チャートのカスタマイズ」の部分を参照しながら、グラフの表示方法を 1つのグラフ内で 2つのデータを同時に表示する形にしてみました。

●チャネルとチャートのカスタマイズ – Ambient
 https://ambidata.io/docs/customize/

表示方法の設定変更を行った後に、先ほどのプログラムをしばらくの間実行し続けると、以下のグラフが描画されました。
2種類のデータをグラフ化.jpg

まとめ

今回、IoTデータ可視化サービスの「Ambient」を使った数値データのグラフ化を行ってみました。

記事内でふれていたように、UIFlow・Node-RED や Node.js以外の他のプログラミング言語でも利用できるようなので、今後は何らか特定のデバイスを使ってセンサーで取得した値をグラフ化するようなこともやってみようと思います。

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

【React Native】簡単なAPI連携のアプリケーションを作成する ④天気情報表示の実装

はじめに

本記事は下記の記事の続編になります。
【React Native】簡単なAPI連携のアプリケーションを作成する ①準備編
【React Native】簡単なAPI連携のアプリケーションを作成する ②住所検索の実装
【React Native】簡単なAPI連携のアプリケーションを作成する ③ESLint+Prettierの導入(寄り道)
実践的なアプリを作成するというよりは、ハンズオン的にReact Nativeを触ってみるといった趣旨になっています。
今回で最終回の予定です。それではやっていきましょう。

本記事で説明すること

  • axiosの利用(②の記事から引き続き利用します)
  • APIへのリクエスト(天気情報の取得)

今回使用するAPI

githubリポジトリ

https://github.com/pbyoshida/postal-app

本編

まずは上記APIを使用するために、会員登録とAPIキーの発行を行います。

会員登録・APIキーの発行

Sign Upページで必要事項を入力し、会員登録を行います。
会員登録後、確認メールが届きますので、emailを有効化します。
その後、マイページ内の「API keys」のページにAPIキーがありますので、そちらを使います。
image.png

APIの仕様

詳細はドキュメントを参照ください。
今回は郵便番号で検索し、現在の天気を表示しますので「By ZIP code」の項目を参照します。
URIは以下です。

api.openweathermap.org/data/2.5/weather?zip={zip code},{country code}&lang=ja&appid={API key}

なお、天気の詳細は日本語で表示したいので、&lang=jaのパラメータを付与しています。

APIキー準備編

早速今回の修正版を載せていきます。
まずは、APIキーはハードコーディングしたくないので、env.jsというファイル内に記載します。

env.js
export const weatherAPIKey = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

githubのリポジトリには上記ファイルはアップしていませんが、env.js.sampleという名前で
それっぽいファイルを作成しています。試したい場合には自身でAPIキーを発行後、env.js内に記載してください。

アプリ本体の修正

続いてアプリの本体です。

App.jsx
import React, { useState } from 'react';
import { StyleSheet, Text, View, TextInput, Button, Alert } from 'react-native';
import axios from 'axios';

import { weatherAPIKey } from './env';

// 郵便番号検索APIのURL
const zipcloudURL = 'https://zipcloud.ibsnet.co.jp/api/search';

// OpenWeatherAPIのURL
const weatherURL = 'http://api.openweathermap.org/data/2.5/weather';

export default function App() {
  // 各種値を保存しておくstate
  const [postalCode, setPostalCode] = useState('');
  const [address, setAddress] = useState('');
  const [weather, setWeather] = useState('');

  // axiosのGETメソッドを使った住所検索
  const fetchAddress = async () => {
    try {
      const response = await axios.get(`${zipcloudURL}?zipcode=${postalCode}`);
      const { data } = response;
      if (!data.results) {
        return '該当する住所はありませんでした。';
      }
      switch (data.status) {
        case 200:
          // 今回はテストなので、同じ郵便番号で2件以上存在する場合は除きます
          return `${data.results[0].address1}${data.results[0].address2}${data.results[0].address3}`;
        case 400:
          return data.message;
        case 500:
          return data.message;
        default:
          return '予期しない動作です';
      }
    } catch (error) {
      return '検索失敗';
    }
  };

  // axiosのGETメソッドを使った天気情報取得
  const fetchWeather = async () => {
    try {
      const postalCode1 = postalCode.substr(0, 3);
      const postalCode2 = postalCode.substr(3, 4);
      const response = await axios.get(
        `${weatherURL}?zip=${postalCode1}-${postalCode2},JP&lang=ja&appid=${weatherAPIKey}`
      );
      const { data } = response;
      return data.weather[0].description;
    } catch (error) {
      return '天気情報の取得に失敗しました。';
    }
  };

  // 送信ボタンを押した時に実行される関数
  async function handlePress() {
    // 7桁の数字を正規表現で置きます
    const pattern = /^[0-9]{7}$/;
    if (pattern.test(postalCode)) {
      const searchedAddress = await fetchAddress();
      setAddress(searchedAddress);
      const currentWeather = await fetchWeather();
      setWeather(currentWeather);
    } else {
      // 想定していない文字列の場合
      Alert.alert('正しい郵便番号ではありません', 'もう一度入力してください');
    }
  }

  // アプリに描画する内容
  return (
    <View style={styles.container}>
      <Text style={styles.description}>
        郵便番号を入力してください
        {'\n'}
        (ハイフンなし7桁)
      </Text>
      <TextInput
        value={postalCode}
        style={styles.inputPostalCode}
        onChangeText={(text) => {
          setPostalCode(text);
        }}
        maxLength={7}
        keyboardType="numeric"
        placeholder="郵便番号"
      />
      <Button title="送信" color="#AAAAAA" onPress={handlePress} />
      {address.length > 0 && (
        <View style={styles.addressContainer}>
          <View style={styles.addressLabel}>
            <Text style={styles.addressLabelText}>住所</Text>
          </View>
          <View style={styles.address}>
            <Text style={styles.addressText}>{address}</Text>
          </View>
        </View>
      )}
      {weather.length > 0 && (
        <View style={styles.weatherContainer}>
          <View style={styles.weatherLabel}>
            <Text style={styles.weatherLabelText}>現在の天気</Text>
          </View>
          <View style={styles.weather}>
            <Text style={styles.weatherText}>{weather}</Text>
          </View>
        </View>
      )}
    </View>
  );
}

// 各要素のスタイル
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  description: {
    fontSize: 18,
    color: '#666666',
    paddingBottom: 10,
  },
  inputPostalCode: {
    textAlignVertical: 'center',
    width: 120,
    fontSize: 24,
    marginBottom: 10,
    paddingLeft: 5,
    borderWidth: 1,
    borderColor: 'rgba(0, 0, 0, 0.2)',
  },
  addressContainer: {
    paddingVertical: 10,
    width: 280,
  },
  addressLabel: {
    paddingBottom: 10,
  },
  addressLabelText: {
    fontSize: 18,
    color: '#666666',
  },
  address: {
    paddingLeft: 5,
  },
  addressText: {
    fontSize: 16,
    color: '#000000',
  },
  weatherContainer: {
    paddingVertical: 10,
    width: 280,
  },
  weatherLabel: {
    paddingBottom: 10,
  },
  weatherLabelText: {
    fontSize: 18,
    color: '#666666',
  },
  weather: {
    paddingLeft: 5,
  },
  weatherText: {
    fontSize: 16,
    color: '#000000',
  },
});

追加したポイントは以下になります。

  • APIキーのインポート
  • OpenWeatherAPIのURL
  • 天気の情報を格納しておくuseState
  • APIにリクエストし天気情報を取得するfetchWeather関数
  • 送信ボタンを押した後にfetchWeatherを呼び出す処理
  • 取得した後の現在の天気を表示する処理

前回の郵便番号検索と比較するとエラー処理とかは結構手抜きです。。
また、data.weather[0].descriptionの箇所で天気の詳細のみ取得していますが、レスポンス自体には気温(K)、湿度、気圧など今回使用した以外の項目も多彩に含まれておりますので、気になった方はそちらの表示も試してみてください。
また、今回のAPIでは郵便番号はハイフン有りで指定しなければいけないので、そこだけ文字列をゴニョゴニョしています。
前回のAPIも実はハイフン有りの郵便番号に対応していたので、最初からTextInputを2つにしても良いかと思いました。

動作確認

さて、実機で動作を確認していきます。
1送信ボタン押下前.png
      ↓送信ボタン押下
2結果表示後.png

はい、上記のように天気も表示することができました。

問題点

上記のAPIなんですが、実はそんなに日本の郵便番号が網羅されているわけではなさそうです。
生まれた県である秋田県秋田市内の郵便番号を指定してみます。
3取得失敗時.png
住所の検索は行えていますが、天気情報は取得できていません。
上記の解決法ですが、OpenWeatherMapで郵便番号が使えないときのアプローチの記事で紹介されているように、他のAPIを使って緯度経度を調べてから、緯度経度の指定で天気情報を取得する方法があるようです。
尻すぼみですが、ハンズオン的な内容なので今回は色々と目をつぶろうと思います。

最後に

今回で一旦React Nativeのハンズオンとしては終了になります。
シリーズを通じてReact Nativeでの開発環境の構築と、APIを通じた情報取得といった部分を中心に解説しました。
ただReactとかの仕様やビルド周りはあんまり解説できていないので、その辺りは機会があれば記事にしようと思います。
本記事ではネット上で自由に使えるAPIを使用しましたが、実際に開発する際には自分でバックエンドのAPIを用意したり、Firebaseを使ってサクッとバックエンド周りを実装するなどあると思います。
次は勉強としてRuby on RailsとかでバックエンドのAPIを作成して、スマホアプリと繋いでみるということもしてみたいですね。

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

JavaScriptで画像プレビュー機能を実装するシンプルなコード

Ruby on RailsのアプリでJavaScriptを使ってプレビュー機能を実装するコードを解説します。

こんな感じで画像ファイルを選択すると、プレビューが表示される機能を目指します。
Image from Gyazo

実行環境

Rails 6.0.3.1
macOS Catalina バージョン10.15.7

ビューのコード

ビューはきわめてシンプルです。

app/views/messages/_form.html.erb
<%= form_with model: @message, id: 'new_message', local: true do |f| %>
  <%= f.text_field :content, placeholder: 'type a message' %>
  <%= f.file_field :image %>
  <%= f.submit '送信' %>
  <div id="image-list"></div><%# プレビューを表示する部分 %>
<% end %>
<%# ↓投稿編集ページ用の画像表示箇所↓%>
<%= image_tag @message.image, id: 'image' if @message.image.present? %>

JavaScriptのコード

コードは以下の通りです。流れをシンプルにして説明すると、

  • 画像ファイルが選択されるとその画像ファイルに対してURLが生成
  • 生成したimg要素のsrc属性にそのURLをセット
  • プレビュー表示用のdiv要素の中に子要素のdiv要素とimg要素を追加する

この流れでプレビュー画像を表示します。

app/javascript/packs/preview.js
// プレビュー表示機能は新規投稿("/new/")か投稿編集("/edit/")ページでのみ有効にする
if (document.URL.match( /new/ ) || document.URL.match( /edit/ )) {
  document.addEventListener('DOMContentLoaded', function(){
    // プレビューを表示するための要素を取得
    const ImageList = document.getElementById('image-list');

    const createImageHTML = (blob) => {
      // 画像を表示するためのdiv要素を生成
      const imageElement = document.createElement('div');
      // 表示する画像を生成
      const blobImage = document.createElement('img');
      // img要素のsrc属性の値をセット
      blobImage.setAttribute('src', blob);

      // 生成したHTMLの要素をブラウザに表示させる
      imageElement.appendChild(blobImage);
      ImageList.appendChild(imageElement);
    };

    document.getElementById('message_image').addEventListener('change', function(e){
      // 画像が表示されている場合のみ、すでに存在している画像を削除する(編集ページ用)
      const imageContent = document.querySelector('img');
      if (imageContent){
        imageContent.remove();
      }

      // 発火したイベントeの中の、targetの中の、filesという配列に格納された画像を変数に代入
      const file = e.target.files[0];
      // 画像のURLを生成
      const blob = window.URL.createObjectURL(file);

      createImageHTML(blob);
    });
  });
}

ビューファイルとJavaScriptのファイルを見比べやすいように横並びにした画像を用意しました。
Image from Gyazo

最後に、application.jsに以下の記述を追記して、turbolinksはコメントアウト(または削除)し、preview.jsを読み込むことを忘れないようにしましょう。

app/javascript/packs/application.js
require("@rails/ujs").start()
// require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("./preview") //このコードを追記
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js】WindowsでNode.jsの環境構築をする

プログラミング勉強日記

2021年2月21日
友人にNode.jsの環境構築を教えるために一度調べたので、そのやり方をまとめる。

Node.jsをインストールする

 Node.jsの公式サイトからNode.jsのインストーラーをダウンロードする。(今回は推奨版をダウンロードした。)

image.png

 インストーラーをダウンロードしたら、開いて実行する。その後指示にしたがtt進めていく。基本的にはそのままNextをクリックして進めていく。最後にFinishを押して終了する。

Node.jsのバージョンを確認する

 それぞれコマンドで下記を実行すると、バージョンを確認することができる。バージョンが表示されない場合は、インストールができていない。

$ node ^v
$ npm -v
バージョンが表示されている場合
C:\>node -v
v12.18.4

C:\>npm -v
6.14.6
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【技術書まとめ】『JavaScript Primer』を読んだまとめ

第一部: 基本文法

データ型とリテラル

  • typeof nullobject となる
  • ""''は全く同じ
  • 複数行の文字を入れるなら`
  • undefinedはただのグローバル変数で、undefinedという値を持っているだけ
    • リテラルではない

演算子

  • JSでは10 + 0.510.5となる
    • > 数値は内部的にIEEE 754方式の浮動小数点数として表現されています
  • NaN === NaNfalseになる
  • ==は使うべきではない
    • 暗黙的な型変換をするから
      • 1 + true; // => 2
      • 使ってよい例外: value == nullでnullとundefindを両方比較するとき
        • しかし意図が読めないときもある
  • indexOfは見つからないとき-1を返す
    • if (~str.indexOf("木"))で 0 となる
      • 0 は false なので実行されない
  • includes("木")で見つけられる
  • ""(空文字列)はfalse

暗黙的な型変換

  • 1 + "2"; // => "12"
  • Number.parseInt("1", 10);で1をパースして10進数で取り出す
    • 文字列とundefindはNaNとなる
      • Number.isNaN(NaN);でNaNとわかる
  • sumでundefinedが入ると"NaN"になる
    • if (typeof value !== "number")で防いだりする
  • 空文字列はtypeof str === "string" && str.length === 0;で判定する

関数と宣言

  • 仮引数より呼び出し時の引数が少ない時、余ったものはundefinedになる
    • 溢れたときは無視される
  • prefix || 'デフォルト';だと空文字列の時にもデフォルトが入ってしまう
    • デフォルト引数にする
    • prefix ?? 'デフォルト';にする
  • まとめて入れたり、残りをまとめたり、まとめて出したりは...arrayでできる
    • Rest parameters
  • 関数の中でのみ使える arguments[0]
    • function() { console.log(arguments[0]); }のように引数が定義されてなくても使える
      • Rest parameters が使えるならこれは使わない方がいい
  • function printUserId({ id }) { ... }user.idのidだけとれる
    • const { id } = user;でもとれる
      • 分割代入
  • 関数は関数オブジェクト
  • アロー関数
    • 常に名前がない
    • thisが決まっている
    • 短く書ける
    • newできない
    • argument変数は使えない
  • 同じ名前の関数宣言は上書きされる
    • functionvarを使ったときだけ起こる
  • 引数となる関数はコールバック関数とよぶ
    • コールバック関数を使う関数やメソッドを高階関数とよぶ
  • JSでは関数とメソッドの違いはあまりない

文と式

    • 値を生成し、変数に代入できるもの
    • if文など
  • ブロックで終わる文にはセミコロンは不要
    • 匿名関数は式だからセミコロンは必要

条件分岐

  • falsyな値
    • false
    • undefined
    • null
    • 0
    • 0n
    • NaN
    • ""(空文字列)
  • falsyな値以外は真偽値に変換するとtrueとなる

ループと反復処理

  • someは一度でもtrueが返ってくると反復処理を終了する
    • numbers.some(isEven);
  • filtertrueになった値だけを集められる
    • array.filter(isEven);
  • reduceで反復処理ができる
    • array.reduce((前回の値, 現在の値) => { return 次の値; }, 初期値);
      • numbers.reduce((total, num) => { return total + num }, 0);

オブジェクト

  • オブジェクトとはプロパティの集合
    • プロバティはキーとバリューの対
  • プロパティ名と変数名が同じだと{ name }と省略して書ける
    • importの時などに使う
  • プロパティ名に変数を使うときは[]を使う
    • languages[myLang]
  • オブジェクトのプロパティを変数として使うときは分割代入する
    • const { ja, en } = languages;
  • プロパティの削除はdelete obj.key1
  • JSのconstは再代入を防ぐだけ
    • 値の変更はできてしまう
      • const obj = { key: "value" };でもobj.key = "Hi!";できる
    • 変更防止はObject.freezeを使う
  • 存在しないプロパティにアクセスするとundefinedとなる
    • 例外は発生しない
    • プロパティを持つか確認するには?
      • undefinedと比較する
        • obj.key !== undefined
          • 値がundefinedのときと区別できない
            • 最終的に取得したいのが値の時に使う
      • in演算子
        • if ("key" in obj) { ... };
      • hasOwnPropertyメソッド
        • if (obj.hasOwnProperty("key")) { ... }
  • nullの可能性があるときにはobj?.a?.b
    • widget?.window?.title ?? "未定義"
    • []でも使える
      • languages?.[ja]?.[messageKey]
  • toString()String()は同じ
  • オブジェクトのプロパティ名は暗黙的に文字列に変換される
  • オブジェクトを配列にする
    • Object.keys(obj)でkeyの配列にする
    • Object.values(obj)でvalueの配列にする
    • Object.entries(obj)でkeyとvalueの配列にする
  • Object.assignで複製やマージができる
    • const merged = Object.assign({}, objectA, objectB);
      • プロパティが重複すると後ろで上書きされる
    • const merged = { ...objectA, ...objectB };でもマージできる
    • const cloneObj = Object.assign({}, obj);で浅い複製できる

プロトタイプオブジェクト

  • 空オブジェクトでも.toString()を呼び出せる
    • Object.prototypeのメソッドを継承しているから
      • プロトタイプメソッド
        • 同名のインスタンスメソッドがあればそれが優先
  • hasOwnPropertyinの違い
    • inはプロトタイプまで遡る
  • Object.create(null)で本当に空のオブジェクトを作れる
    • 昔はMapの代わりに使われていた
      • const obj = {};でもobj["toString"]でアクセスできてしまうから

配列

  • 最後の要素へのアクセスはarray[array.length - 1]でできる
  • 存在しないインデックスへのアクセスはundefinedになる
    • オブジェクトでも同じ
  • 配列は常にlengthの数だけ要素を持っているとは限らない
    • 疎な配列
      • const sparseArray = [1,, 3];
        • sparseArray.length3となる
          • sparseArray[1]undefined
  • 配列かどうか確認するにはisArray
    • typeof arrayでは"object"となる
  • 配列も分割代入できる
  • 疎な配列があるから本当に未定義なものと区別できない
    • hasOwnPropertyで区別する
      • sparseArray.hasOwnProperty(1)falseとなる
  • どの位置にあるか知りたい
    • indexOflastIndexOf
      • ary.indexOf("JS");
        • なければ-1が返ってくる
    • オブジェクトにはfindIndexを使う
      • colors.findIndex((obj) => { return obj.color === "blue" });
  • 条件に一致する要素を取得する
    • colors.find((obj) => { return obj.color === "white" });
  • 指定範囲の要素を取得する
    • slice(1, 4)
  • 目当てのものが含まれているか確認する
    • inclues
      • オブジェクトには使えない
        • some
          • colors.some((obj) => return obj.color === "blue"; });
  • 追加と削除
    • pushpop
    • unshiftshift
  • 結合したい
    • array.concat(["D", "E"]);
  • flattenにしたい
    • newArray = ["X", "Y", "Z", ...array];
      • ["X", ...array, "Z"]もできる
    • newArray = ["X", "Y", "Z"].concat(array);
    • ES2019ならflat(Infinity)も使える
      • これ以上フラット化できなくてもそのまま返す
  • 任意のインデックス要素を削除する
    • array.splice(インデックス, 削除する要素数)
      • 自動的に詰められるから疎にはならない
  • すべての要素を削除
    • array.length = 0;とすると配列が空になる
      • その要素数に切り詰められるから
  • 注意する破壊的メソッド
    • sort
  • 非破壊でコピーする
    • array.slice()
    • array.concat()
      • 引数なしで呼び出すとコピーを返す
        • コピーしてから破壊的メソッドを使う
  • 指定の要素だけ集める
    • array.filter((currentValue, index, array) => { return currentValue % 2 == 1; });
  • reduce
    • array.reduce((累積値, 要素, インデックス, 配列) => { return 処理;}, 初期値)

文字列

  • 分解したり繋げたりする
    • const strings = "赤・青・黄".split("・").join("、");
  • 正規表現を使って抜き出す
    • const strings = str.split(/\s+/);
  • "?"以降を抜き出す
    • const indexOfQuery = url.indexOf("?"); const queryString = url.slice(indexOfQuery):
      • slicesubstringはほとんど同じ
  • 文字列の検索
    • str.startsWith()
    • str.endsWith()
    • str.includes()
  • 正規表現
    • const patternA = /パターン/フラグ;
    • const patternB = new RedExp("パターン文字列", "フラグ");
      • 関数として呼び出されるまで評価されない
        • 動的に変更できる
    • str.search()
    • "文字列".match(/パターン/);
      • マッチしない時はnullを返す
      • /[a-zA-Z]+/gで見つかっても最後までやる
      • /バターン1(パターン2)/でカッコを取り出せる
    • そのパターンにマッチするものがあるか調べる
      • /^にわ/.test(str)
        • 繰り返しや文字の集合なども検索できる
    • 基本はStringメソッドでやる
      • 柔軟性や曖昧検索のときは正規表現+コメントする
  • 置換
    • str.replace("文字", "");
    • str.replace(/文字/, "");
      • コールバックもできる
        • dateString.replace(/(\d{4})-(\d{2})-(\d{2})/, (all, year, month, day) => { ... };
  • URLはgetResource()
    • schemehostpathname
      • 最後の/は削除してから使う
        • Node.jsのPathモジュールを使う
  • タグ付きテンプレート関数
    • tagtemplate ${0} literal ${1};
    • function tag(strings, ...values) { ... };
      • valuesが取れる
    • String.raw()

文字列とUnicode

  • JSでは「文字列はCode Unitが順番に並んだもの」として扱われる
    • リンゴの絵文字の length は 2 になる
      • サロゲートペア
    • const codePoints = Array.from("リンゴ?"); // => ["リ", "ン", "ゴ", "?"]
      • 完璧ではない
        • ビルトインだけでは難しい
  • 正規表現のときはuをつける
    • const [all, fish] = "?のひらき".match(/(.)のひらき/);
      • 文字化けする
    • const [all, fish] = "?のひらき".match(/(.)のひらき/u);
      • 基本的にはuを付ける

ラッパーオブジェクト

  • なぜ型がメソッド呼び出しできるのか
    • プリミティブ型は自動的にラッパーオブジェクトに変換されるからメソッド呼び出しできる
      • JSはすべてがオブジェクトのように「見える」
        • すべてがオブジェクトではない

関数とスコープ

  • 関数を定義する
    • 新しいスコープを作るということ
  • スコープとは
    • 参照できる範囲を決めるもの
  • 関数スコープ
    • function fn() { 関数スコープ };
  • ブロックスコープ
    • if() { ブロックスコープ };
  • スコープチェーン
    • { { } }
      • 内側から外側を順番に参照できる
  • グローバルスコープ
    • グローバル変数
    • ビルトインオブジェクト
      • undefinedisNaN
      • ArrayRegExp
      • documentmodule
  • むやみにグローバルスコープへ変数定義しない
    • 外側の変数が隠蔽されるから
      • shadowing
      • 関数を使って小さなスコープにして書く
  • varは巻き上げする
    • ブロックスコープを無視してしまう
  • クロージャー
    • 「外側のスコープにある変数への参照を保持できる」
      • グローバル変数を減らせる
      • 高階関数を作る
    • 静的スコープ
      • 変数の中身は静的に決まる
        • 内側にいないなら一つ外側のスコープを確認する
    • メモリ管理の仕組み
      • 解放はあくまでそのデータが参照されているかどうかで決まる

関数とthis

  • 実行コンテキスト
    • Script
      • thiswindowオブジェクト
    • Module
      • thisundefined
        • ES2020ではglobalThis
  • アロー関数以外の関数のthis
    • ベースオブジェクト
      • selfのようなもの
        • 実行時に決定される
      • なければundefined
    • callapplybindで明示的に指定できる
  • アロー関数のthis
    • 外側で最も近い関数のthisとなる
      • 静的に決まる

非同期処理:コールバック/Promise/Async Function

  • 同期的なブロック処理
    • ブラウザでは大問題になる
      • スクロールが効かなくなる
  • 非同期処理はメインスレッドで実行される
    • 非同期処理も同期処理の影響を受ける
    • 非同期なタイミングで実行される処理
  • 普通にtry...catch書くと非同期処理のエラーは処理できない
  • エラーファーストコールバック
    • 共通ルールの一つ
      • 処理が失敗したらerrorにエラーオブジェクトを渡す
      • 処理が成功なら2番目以降の引数に結果を渡す
        • fs.readfile("./example.txt", (error, data) => { if(error) { ...} else { ... } });
  • Promise
    • 成功したらresolve
    • 失敗したらreject
      • const executor = (resolve, reject) => {};
    • thenメソッドで成功時と失敗時の処理を渡す
    • try...catchを使わなくても例外がキャッチされる
      • catchthenのシンタックスシュガー
  • Promiseの状態
    • Fullfilled
      • resolve成功したとき
    • Rejected
      • reject失敗したとき
    • Pending
      • FullfilledまたはRejectedではないとき
      • new Promiseでインスタンスを作成したとき
        • 最初はPendingで、一度でも変化したらそこからは変わらない
          • resolveした後のrejectは呼び出されない
          • resolveした後にもう一度resolveしても呼び出されない
  • Promise.resolve
    • 最初からFullfilledなPromiseインスタンスを作る
      • Promise.rejectもある
        • テストコードで使われる
          • 短く書けるから
  • Promiseチェーン
    • 失敗時は一番近い失敗処理が呼び出される
      • 途中のthenは無視される
        • 失敗を一度キャッチするとまたチェーンに戻る
          • catchはFullfilled状態のPromiseインスタンスを作成するから
          • return Promise.reject(new Error("失敗"));
            • これはRejected状態となる
              • キャッチしてもRejected状態を継続できる
      • 例外時も同じ
    • returnで値を返すと次のthenへ引数として渡せる
    • Promise#finallyは必ず呼び出される
      • isLoadingfalseにするなど
    • thenごとに配列に値をpushしていく使い方もできる
      • Promise.allで複数のPromiseもまとめられる
        • どれを先に取得しても問題ないとき
          • 一つでもRejectedとなったら失敗処理が呼び出される
    • Promise.race
      • 1つでもSettled状態になれば次の処理をする
        • 非同期処理のタイムアウトが作れる
          • 一定時間経過しても処理が終わってないならエラーとする
            • timeoutとやりたい処理をPromise.raceの引数にする
  • Async Function
    • async function doAsync() { return "値"; }
    • function doAsync() { return Promise.resolve("値"); }
      • 同じ
        • 必ずPromiseインスタンスを返す
          • Promiseを返すただの関数と何も変わらない
        • awaitが使える
    • どれにでもつけられる
      • async function fn1() {}
      • const fn2 = async function() {};
      • const fn3 = async() => {};
      • const obj = { async method() {} };
  • await
    • 同期処理のように書ける
    • Promiseのresolveされた値がawaitの返り値になる
      • const value = await Promise.resolve(42);
        • 例外があったらRejectedなPromiseが返る
          • try...catch構文でキャッチできる
            • 同期処理と同じ
    • 非同期でもループ処理ができる
    • Promise.allも使える
      • Promiseを複数作ってからPromise.allに渡す
        • const promises = resources.map(function(resource) { return fetch(resource); });
          • const responses = await Promise.all(promises);
    • Async Funtion の中でのみ使える
      • 外の処理は止まらない
        • UIなどの処理が止まってしまうから
        • コールバック関数の時に注意
          • forforEachに単純に変えられない
          • Promise.allでまとめるか

Map/Set

  • Map
    • 初期値で渡せるのはエントリーの配列
      • new Map([["key1", "value1"], ["key2", "value2"]]);
    • sizeで数がわかる
    • setで追加する
      • 同じキーは上書きされる
    • getで取り出す
    • hasでそのキーがあるか確認できる
    • deleteで削除する
    • clearで全て削除する
    • keys,valuesが返すのは配列ではない
      • Array.from(map.keys());
        • 配列に変換して反復処理をする
    • マップとしてのObjectとの違い
      • デメリット
        • Objectはprototypeメソッドで意図しない動きをすることがある
          • 昔はObject.create(null)のようにして使っていた
            • Mapが導入された
        • キーはSymbolのみ
      • メリット
        • リテラル表現で作成しやすい
        • JSON.stringifyで変換できる
        • 多くの場所で使われている
  • WeakMap
    • キーを弱い参照で持つ
      • ガベージコレクションを妨げない
    • iterableではない
      • keyssizeがない
    • 使い方
      • イベントリスナーを管理する
        • 使われなくなったら消える
      • キャッシュとして一時的に計算結果を保存する
  • Set
    • 同じ値を入れると1つのみ格納される
    • インデックスはない
    • forEachが使える
  • WeakSet
    • データの一意性を確認することに特化したセット

JSON

  • JSON.parse
    • json = [1, 2, 3]なら返り値も配列
    • パースできないと例外が投げられる
      • 基本的にtry...catchされる
  • JSON.stringify
    • 第二引数にreplacerを渡せる
      • keyがnullならundefinedにする処理
      • ホワイトリストとしてkeyの配列を渡せる
        • ["id", "name"]だけを抜き出す
    • 第三引数にフォーマット時のインデントを設定できる
      • JSON.stringify(obj, null, 2)ならスペース2個でインデント
        • "\t"もできる
    • Symbolやundefinedは変換されない
    • オブジェクト内にtoJSONがある場合
      • その返り値のみ使う
        • 特殊な形式でシリアライズできる

Date

  • Date.UTC(2006, 0, 2, 15, 4, 5, 999)
    • UTCなのでローカルのタイムゾーンに影響されない
  • getMonthで取得すると+1する必要がある
    • const mm = String(date.getMonth() + 1).padStart(2, "0");
  • ほとんどライブラリを使う
    • moment()

Math

  • 乱数をつくる
    • Math.random()

ECMAScriptモジュール

  • なぜモジュールを使うのか
    • 保守性
      • 依存性の高いコードをまとめて、他への依存性を減らせる
    • 名前空間
      • グローバルを汚染しない
    • 再利用性
      • コピペせずに再利用できる
  • 名前つきエクスポート/インポート
    • export { foo };
    • export function bar() {};
    • import { foo, bar } from "./my-module.js";
    • エイリアス
      • export { internalFoo as foo };
      • import { foo as myFoo } from "./named-export-alias.js";
  • デフォルトエクスポート/インポート
    • デフォルトエクスポートに名前をつけてインポートする

ECMAScript

  • その機能がどのような経緯で入ったのかを調べる手段を持っておく
    • その機能が何を解決するために導入されたのかを知る
      • 調べたいと思ったときに調べることができるように、調べ方を知っておくことが重要
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでHTMLを操る最低限の知識

エンジニア経験約1年半のTKです。
今までは業務でJavaばっかり書いていてフロント知識を捨てていました。

最近、初めてフロントの開発せざるを得ない展開になってしまいました。
嫌だなぁと思いましたが、これを機にちゃんと勉強しようと思いました。

今回はJavaScriptを実装した時に思った事を素直に書きつつ、どう対応したかを今後の自分の為に書き記します。
今までサーバーサイドの開発ばっかりやっていてフロントは嫌い!って人向けです。

ループ文やif文などの構文についてではありません。(そこはサーバー開発言語の知識があればググってすぐに理解できます)

HTMLとの関係について

僕がJavaScriptを書く際に最初にめんどくさいと思ったのはここです。
全くわからない、どうやってHTMLを操作するの?と純粋に思いました。

例えば

そうですね、画面表示する時に処理したいって時。

シンプル.HTML
<!DOCTYPE html>
<html lang="en">
<script type="text/javascript" src="test.js"></script>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <!--サーバーから渡ってきた日付-->
    <div id="ymd">21000909</div>
</body>

</html>

実際の業務ではこんな画面あり得ないですが、ご了承ください。

サーバーから日付が8桁で渡ってきて表示する時はスラッシュを補完したい!という要件があるとします。

フロント嫌いの僕からしたらどうやるねん、、、って話です。

JavaScript内で日付文字列にスラッシュを補完するという処理はすぐに思いつきます。が、フロント開発初心者の僕はまずこの値どうやって受け取るんだという疑問から始まります。
しかも、画面表示時に値を受け取るというタイミングもおまけでついてきます。きついですね、、

実際にこう書けば仕様通りに動きます。

test.js
// ページ読み込み完了後に実行
window.onload = function () { 

    // 対象の要素を取得する
    const target = document.getElementById("ymd");
    // スラッシュを補完し、要素のtextに設定する
    target.textContent = target.textContent.substr(0, 4) + "/" + target.textContent.substr(4, 2) + "/" + target.textContent.substr(6, 2);

}

※値の存在チェック等はここでは割愛しております。

やることを箇条書きすると、
・画面表示の際にJavaScriptを呼ぶ
・HTMLのtextを取得し、スラッシュを補完
・HTMLのtextに編集後の値を設定する

書いてみるとかなり簡単ですが、僕の勝手な妄想ですがフロント嫌いはここまで辿り着くのに結構時間を要します。

イベント発動→JavaScript実行

JavaScriptの実行タイミングはこれだけと言っても過言ではありません!
この流れがわかってしまえば応用でなんでも出来そうな気がしてきました。
イベントなどは別途また投稿します。(ここでのイベントはざっくり言うと画面表示です。)

そういえば、window.onloadは使うべきではないという噂も聞いたことがあります。
同ページで複数のwindow.onloadがあると処理が上書きされてしまう事があるみたいです。

updateTest.js
// ページ読み込み完了後に実行
window.addEventListener('load', function() {

    // 対象の要素を取得する
    const target = document.getElementById("ymd");
    // スラッシュを補完し、要素のtextに設定する
    target.textContent = target.textContent.substr(0, 4) + "/" + target.textContent.substr(4, 2) + "/" + target.textContent.substr(6, 2);

}

こっちのaddEventListenerを使用すればイベントを追加する為、上書きされないです!
target.textContentも変数に入れるべきだったかな。。。まあいいか。

HTMLを操作するための最低限

上記の例のtest.jsに習って記載します。

// 対象の要素を取得する
const target = document.getElementById("ymd");

これは、HTMLのid属性が"ymd"の要素を取得し、targetという変数に格納しています。
documentとは何でしょうか?
console.log(document)で表示してみましょう。

スクリーンショット 2021-02-12 17.09.24.png

なるほど!HTMLが表示されています。
つまり、document.何かしらで実際に要素が取得出来る事がわかりました。

これがわかれば大体ググれば何とかなります!
よく使うものは下記です。

指定するもの 関数名
id getElementById document.getElementById("id名")
class getElementsByClassName document.getElementsByClassName("class名")
id、class querySelector document.querySelector("idまたはclass名")

次は実際にHTMLに値を設定するところです。

// スラッシュを補完し、要素のtextに設定する
target.textContent = target.textContent.substr(0, 4) + "/" + target.textContent.substr(4, 2) + "/" + target.textContent.substr(6, 2);

targetには取得した要素が入っております。

target.関数名=設定する値で設定できます。
設定先の属性によって関数名を使い分ければ良いと言う事です。

よく使うものは下記です。

属性 関数名
id id target.id=設定する値
class className target.className=設定する値
text textContent target.textContent=設定する値
value value target.value=設定する値

まとめ

とりあえず、JavaScriptが実行できれば後は値を取得する、設定するの知識で結構何とかなる気がします!

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