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

非同期通信(バックエンド)

非同期通信

リクエスト後にブラウザが再読み込みされず一部分のみが更新される通信方法。
待ち時間や画面の切り替わりがなくストレスが少なく操作できます。
SNSの既読機能だったり、いいね機能だったり多用されています。

Ajax

JavaScriptを使用して非同期通信を行う処理のこと
("Asynchronous JavaScript + XML"の略)

メモアプリを実装しながら投稿機能を非同期通信で、また既読機能をつけてみましょう。

新規アプリケーションを作成

console
% rails _6.0.0_ new memo_app -d mysql

% rails db:create

ルーティングを設定

config/routes.rb
Rails.application.routes.draw do
 root to: 'posts#index'
post 'posts', to: 'posts#create'
end

postモデルを作成

% rails g model post

マイグレーションファイル作成

db/migrate/....
class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.text :content
      t.boolean :checked
      t.timestamps
    end
  end
end

boolean型

trueまたはfalseの真理値を判断する型です。

既読機能実装時に「既読か未読か」をboolean型で管理します。

% rails db:migrate

postsコントローラーを作成

% rails g controller posts index

アクションを記述

class PostsController < ApplicationController

  def index
    @posts = Post.all.order(id: "DESC")
  end

 def create
   Post.create(content: params[:content])
   redirect_to action: :index

end

ビューを編集

views/posts/index.html.erb
<h1>MemoApp</h1>
<%= form_with url:  "/posts", method: :post,id: "form" do |form| %>
  <%= form.text_field :content %>
  <%= form.submit '投稿する' , id: "submit" %>
<% end %>

<% @posts.each do |post| %>
<div class="post">
  <div class="post-date">
    投稿日時:<%= post.created_at %>
  </div>
  <div class="post-content">
    <%= post.content %>
  </div>
</div>
<% end %>

memo_appのappディレクトリにあるjavascriptディレクトリに
memo.jsとchecked.jsというファイルを作成

app/javascript/packs/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require("../checked")
require("../memo")

非同期通信の実装方法

非同期通信時のレスポンス内容はHTMLではなくデータを返却します。
それをJavaScriptで受け取り、すでに表示されているHTMLを部分的にレンダリングするような仕組みです。

動きの図

エンドポイント

Ajaxでやり取りする際の、データ返却のアクションを実行するためのURLのことです。

Ajaxを実現するためには、コントローラーでのレスポンスを、HTMLではなくjsonなどのデータ形式で返却する必要がありデータを取得する時にアクセスするためのURLを、エンドポイントといいます。

既読機能の実装の際に、エンドポイントには「どのメモを既読にしたか」を判別するため「メモのid」というパラメーターを渡す必要があります。

URLパラメーター

サーバーに情報を送るために記載するURL末尾の文字列のことで、送信した情報は、今までparams[:id]などで取得し、使用してきました。
非同期通信では、このURLパラメーターを活用し、サーバーへデータを送ります。

queryパラメーター

queryパラメーターとは、

http://sample.jp/?fruit=orange

のように、「?」以降に情報をかくURLパラメーターです。
「?」以降の構造は、?<変数名>=<値>となっています。

このURLでparams[:fruit]とすると、orangeが戻り値となります。

エンドポイントを、queryパラメーターで記述

既読機能に必要なパラメーターは、「どのメモを既読したか」を判別するためのメモのidです。
メモのidを取得できるようにルーティングに設定します。
下記を追記しましょう。

config/routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  post 'posts', to: 'posts#create'
  get 'posts', to: 'posts#checked'
end

queryパラメーターを使用した場合、/posts/?id=1とリクエストを行うと、params[:id]にてパラメーターを取得することができます。

pathパラメーター

http://tweets.jp/tweets/1

のように指定するURLパラメーターです。
queryパラメーターとの使い分けは、pathパラメーターで指定するのは「リソースを識別する場合」です。

既読機能のエンドポイントは、queryパラメーターで設定しました。しかし、今回のように渡す情報が一意の情報であればpathパラメーターの方が適しています。

routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  post 'posts', to: 'posts#create'
  get 'posts/:id', to: 'posts#checked'
end

今回のように、postのidであれば'posts/:id'のように記載するpathパラメーターの方が認識もしやすく、記述も単純です。

データ形式

コンピューター上でデータをやり取りする際の形式のことです。プログラミングではJSONが多く使用されます。

XML

JSONと同じくデータをやり取りする際に使用する形式の1つです。

JSONはRubyのハッシュに似ていますが、XMLはHTMLに似ています。
JSONはJavaScriptObjectNotationの略であり、JavaScriptにおけるオブジェクトの表記です。そのため、データをJavaScriptのオブジェクト指向で取り扱う場合に相性が良いです。

text.json
{
  "name": "json表記",
  "format": {
    "json": "JavaScriptにおけるオブジェクトの表記",
    "xml": "HTMLに似ています"
  }
}
text.xml
<name>xml表記</name>
<format>
    <json>JavaScriptにおけるオブジェクトの表記</json>
    <xml>HTMLに似ています</xml>
</format>

Ruby on Railsは、デフォルトではHTMLをレスポンスとして返却する仕組みになっているので
JSONでデータを返却するにはコントローラーの記述を工夫する必要があります。

その際にrenderメソッドを使用します。

既読機能のcheckedというアクションを定義

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.all.order(id: "DESC")
  end

  def create
    Post.create(content: params[:content])
    redirect_to action: :index
  end

  def checked
    post = Post.find(params[:id])
    if post.checked 
      post.update(checked: false)
    else
      post.update(checked: true)
    end

    item = Post.find(params[:id])
    render json: { post: item }
  end
end

①先ほど設定したURLパラメーターから、既読したメモのidが渡されるように設定するので、そのidを使用して該当するレコードを取得しています。

②if文で、post.checkedという既読であるか否かを判定するプロパティを指定し、既読であれば「既読を解除するためにfalseへ変更」し、既読でなければ「既読にするためtrueへ変更」します。
ActiveRecordのupdateというメソッドを使用して更新しています。

最後に、更新したレコードをitem = Post.find(params[:id])で取得し直し
render json:{ post: item }でJSON形式(データ)としてchecked.jsに返却しています。

バックエンド側の処理はいったんここまで次はフロントにいきます。

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

iPhoneの計算機アプリをHTML/scss+JavaScriptでパクってみる①

最近、HTML/scssとJavaScriptを習ってきたので、ちょっとずつアウトプットしていきたいと思い、iPhoneの計算機をパクろうとしてみました。

今回実装したところ

HTML/scss部分を実装しました。

計算機
(スクショなので、デベロッパーツールの部分が少し含まれています)

実装内容

calculator.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>電卓</title>
    <link rel="stylesheet" href="breakpoints/base.css">
</head>

<body>
    <div class="calc">

        <div class="display">
            <input type="text" class='calcArea'>
        </div>
        <div class="buttons">
            <div class="row">
                <button class='btn'>AC</button>
                <button class='btn'>+/-</button>
                <button class='btn'>%</button>
                <button class='btn symbol'>÷</button>
            </div>
            <div class="row">
                <button class='btn number'>7</button>
                <button class='btn number'>8</button>
                <button class='btn number'>9</button>
                <button class='btn symbol'>×</button>
            </div>
            <div class="row">
                <button class='btn number'>4</button>
                <button class='btn number'>5</button>
                <button class='btn number'>6</button>
                <button class='btn symbol'>-</button>
            </div>
            <div class="row">
                <button class='btn number'>1</button>
                <button class='btn number'>2</button>
                <button class='btn number'>3</button>
                <button class='btn symbol'>+</button>
            </div>
            <div class="row">
                <button class='btn-0 number'>0</button>
                <button class='btn number'>.</button>
                <button class='btn symbol'>=</button>
            </div>
        </div>
    </div>
</body>

</html>
base.scss
$bRadius: 70px;
$bMargin: 3px;
$calcWidth: ($bRadius + $bMargin * 2) * 4;
$displayHeight: 120px;
$calcAreaColor: black;
$calcTextColor: white;

body {
    margin: 0;
    position: relative;
    background-color: $calcAreaColor;
        width: 100vw;
    height: 100vh;
}

.calc {
    position: absolute;
    bottom: 0;

}

.display {
  height: $displayHeight;
  position: relative;

  & .calcArea {
    position: absolute;
    bottom: 5px;
    background-color: $calcAreaColor;
    color: $calcTextColor;
    text-align: right;
    font-size: 20px;
    padding: 0;
    border-width: 0;
    width: $calcWidth;
    height: $displayHeight / 2;

    &:focus {
      outline: none;
      caret-color: $calcAreaColor;
    }
  }
}

.buttons {
  font-size: 0;
    margin: 0 auto;

  & .row {
    display: block;

    & .btn {
      margin: $bMargin;
      font-size: 20px;
      width: $bRadius;
      height: $bRadius;
      border-radius: $bRadius / 2;
      border: none;

      &:focus {
        outline: none;
      }

      &.symbol {
        background-color: rgb(255, 145, 0);
        color: white;
      }

      &.number {
        background-color: gray;
        color: white;
      }
    }

    & .btn-0 {
      @extend .btn;
      width: ($bRadius + $bMargin) * 2;
    }
  }
}

大したことはやっていませんが、通常だとボタンとボタンに謎の隙間がありました。
そこで、インライン要素で謎の隙間が出てくる問題を参考にしました。

  • bottonsクラス(親クラス)にfont-size: 0;を設定
  • btnクラスに(子クラス)にfont-size: 20px;を設定

そうすることで、その謎の隙間をなくすことに成功し、自分でmarginを設定することができました。

課題

  • Javascriptの追加
  • 横幅と縦幅が謎にスクロールできてしまう問題の解消
  • 画面を横にした時の見た目を整える

etc...

今後の展望

とりあえず、JavaScriptの部分を実装して、計算機能を持たせたいと思います!

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

【発展】Active Storageで複数の画像を投稿しよう!(プレビュー機能)

Active Storage利用して複数の画像を投稿できる機能とJavaScriptを用いてプレビューできる機能を実装します。
今回も初心者向けにレシピ投稿アプリを例に作成していきます。

完成イメージ

e76d454dd2a3321f60786f9d0954754e.gif

※なお今回は前回の記事の発展的な内容になっております。
Active Storageの導入方法についてこちら

プレビュー機能についてはこちら

今回は上記の機能が実装されている前提で話を進めていきます。

Active Storage関連ファイルの修正

まずはActive Storage周りのファイルから修正していきます。

アソシエーションの修正

レコードとファイルを1対1の関係で紐づけるためにhas_one_attachedメソッドを使用していましたが、今回は1対多の関係に変更するのでhas_many_attachedメソッドに修正します。Railsガイド

また、imageを複数形のimagesに変更します。

app/models/recipe.rb
class Recipe < ApplicationRecord
  has_many_attached :images #ここを修正
end

投稿フォームの修正

file_fieldのimageをimagesに修正します。
また、name属性を追加し送信する際に必要な画像の配列を設定します。

app/views/recipes/new.html.erb
<%= form_with model: @recipe, local: true do |f| %>

#中略
    <div class="form-group">
      <label class="text-secondary">画像</label><br>
      <%= f.file_field :images, name: 'recipe[images][]' %> #ここを修正
    </div>

#以下略

<% end %>

コントローラーの修正

画像の配列を受け取れるようにストロングパラメーターを修正します。

app/controllers/recipes_controller.rb
class RecipesController < ApplicationController

#中略

  private
  def recipe_params
    params.require(:recipe).permit(:title, :text, :category_id, :time_required_id, images: []) #images: []に修正
  end
end

バリデーションの設定

今回、投稿できる画像を3枚までにしたいので、独自のバリデーションメソッドを作成していきます。Railsガイド

app/models/recipe.rb
class Recipe < ApplicationRecord
  has_many_attached :images 

#ここから追加
  validate :image_length #カスタムメソッドなので"validate"

  private

  def image_length
    if images.length >= 4
      errors.add(:images, "は3枚以内にしてください")
    end
  end
#ここまで追加
end

以上でActive Storage関連の修正は完了です。

preview.jsの修正

今回は以下のGIFのようにひとつ画像を選択すると新しく画像選択フォームが出現する仕様にします。
90c396247ee860b014d53f48d1c5f087.gif

まずは、前回作成したpreview.jsを確認してみましょう。

app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    const createImageHTML = (blob) => {
      const imageElement = document.getElementById('new-image');
      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      imageElement.appendChild(blobImage);
    };

    document.getElementById('recipe_image').addEventListener('change', (e) => {
      const imageContent = document.querySelector('img');
      if (imageContent){
        imageContent.remove();
      }

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}

こちらを修正していきます。

今回は、以下のようなHTMLを作成することを前提にpreview.jsを編集していきます。

/recipes/new
<div id="new-image">
<!-- ここから -->
  <div class="image-element">
    <img class="new-img" src="xxxxxxx...">
    <input id="recipe_image_n" class="recipe-images" name="recipe[images][]" type="file">
  </div>
<!-- ここまでを複製していくイメージ -->
</div>

前回まではid="new-image"のdiv要素に直接プレビュー画像を挿入していましたが、今回は新たなdiv要素を作成しその中に画像を挿入していきます。

そして、classにimage-elementを設定しquerySelectorAllメソッドとlengthメソッドで作成された要素の数を定数imageElementNumに格納します。

app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {

    const ImageList = document.getElementById('new-image'); //追記

    const createImageHTML = (blob) => {

      const imageElement = document.createElement('div'); //new-imageからdivに変更

//ここから
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length
//ここまで追記

      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      imageElement.appendChild(blobImage);
      ImageList.appendChild(imageElement); //追記
      });
    };

    document.getElementById('recipe-images').addEventListener('change', (e) => { //recipe-imagesに修正

//ここから
     //const imageContent = document.querySelector('img');
      //if (imageContent){
        //imageContent.remove();
      //}
//ここまで削除

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}

次に新しく画像選択フォームを作成する記述をしていきます。
inputのidに先ほど宣言したimageElementNumを使いinput要素が何番目の要素かを判別します。

app/javascript/packs/preview.js
//中略
document.addEventListener('DOMContentLoaded', () => {
    const ImageList = document.getElementById('new-image');
    const createImageHTML = (blob) => {
      const imageElement = document.createElement('div');
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length

      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

//ここから
      const inputHTML = document.createElement('input');
      inputHTML.setAttribute('id', `recipe_image_${imageElementNum}`);
      inputHTML.setAttribute('class', 'recipe-images');
      inputHTML.setAttribute('name', 'recipe[images][]');
      inputHTML.setAttribute('type', 'file');
//ここまで追記

      imageElement.appendChild(blobImage);
      imageElement.appendChild(inputHTML); //追記
      ImageList.appendChild(imageElement);

//以下略

これで、画像を選択すると新しく画像選択フォームが出現するようになりました。
2枚目以降にもイベント発火するよう処理を記述していきます。
1度目に発火するイベントとほとんど同じ記述になります。

app/javascript/packs/preview.js
//中略

    const createImageHTML = (blob) => {
      const imageElement = document.createElement('div');
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length

      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      const inputHTML = document.createElement('input');
      inputHTML.setAttribute('id', `recipe_image_${imageElementNum}`);
      inputHTML.setAttribute('class', 'recipe-images');
      inputHTML.setAttribute('name', 'recipe[images][]');
      inputHTML.setAttribute('type', 'file');

      imageElement.appendChild(blobImage);
      imageElement.appendChild(inputHTML);
      ImageList.appendChild(imageElement);

//ここから
      inputHTML.addEventListener('change', (e) => {
        const file = e.target.files[0];
        const blob = window.URL.createObjectURL(file);
        createImageHTML(blob);
      });
//ここまで追記
    };

//以下略

ここまでで複数の画像投稿に対応したプレビュー機能の実装が完了しました。
しかし、これでは1枚画像を選択するたびに新しく画像選択フォームが出現してしまうので何枚でも画像選択が可能になってしまいます。
9e0000ca5ed8bee348ff2752f8962bef.gif
モデルにバリデーションをかけているので投稿は保存されません。
61635f5521f4d56c6380ed39a77dea34.png

条件分岐で制限をかけよう

input要素の数える定数imageElementNumを使い、3枚選択すると新しく画像選択フォームが出現しないように条件分岐していきましょう。

app/javascript/packs/preview.js
//中略

      const inputHTML = document.createElement('input');
      inputHTML.setAttribute('id', `recipe_image_${imageElementNum}`);
      inputHTML.setAttribute('class', 'recipe-images');
      inputHTML.setAttribute('name', 'recipe[images][]');
      inputHTML.setAttribute('type', 'file');

      imageElement.appendChild(blobImage);
      if (imageElementNum < 2) {            //追記
        imageElement.appendChild(inputHTML);
      }                                     //追記
      ImageList.appendChild(imageElement);

      inputHTML.addEventListener('change', (e) => {
        const file = e.target.files[0];
        const blob = window.URL.createObjectURL(file);
        createImageHTML(blob);
      });
//以下略

以上で完成です。
32e67abef3b9fedea1327e930a9556a2.gif
以下、完成形のコードです。

app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    const ImageList = document.getElementById('new-image');
    const createImageHTML = (blob) => {
      const imageElement = document.createElement('div');
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length

      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      const inputHTML = document.createElement('input');
      inputHTML.setAttribute('id', `recipe_image_${imageElementNum}`);
      inputHTML.setAttribute('class', 'recipe-images');
      inputHTML.setAttribute('name', 'recipe[images][]');
      inputHTML.setAttribute('type', 'file');

      imageElement.appendChild(blobImage);
      if (imageElementNum < 2) {
        imageElement.appendChild(inputHTML);
      } 
      ImageList.appendChild(imageElement);

      inputHTML.addEventListener('change', (e) => {
        const file = e.target.files[0];
        const blob = window.URL.createObjectURL(file);
        createImageHTML(blob);
      });
    };

    document.getElementById('recipe-images').addEventListener('change', (e) => {

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}

次回は投稿された複数の画像をスライド形式で表示する実装を行っていきます。

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

【発展】Active Storageで複数の画像を投稿しよう! (プレビュー機能も実装)

Active Storage利用して複数の画像を投稿できる機能とJavaScriptを用いてプレビューできる機能を実装します。
今回も初心者向けにレシピ投稿アプリを例に作成していきます。

完成イメージ

e76d454dd2a3321f60786f9d0954754e.gif

※なお今回は前回の記事の発展的な内容になっております。
Active Storageの導入方法についてこちら

プレビュー機能についてはこちら

今回は上記の機能が実装されている前提で話を進めていきます。

Active Storage関連ファイルの修正

まずはActive Storage周りのファイルから修正していきます。

アソシエーションの修正

レコードとファイルを1対1の関係で紐づけるためにhas_one_attachedメソッドを使用していましたが、今回は1対多の関係に変更するのでhas_many_attachedメソッドに修正します。Railsガイド

また、imageを複数形のimagesに変更します。

app/models/recipe.rb
class Recipe < ApplicationRecord
  has_many_attached :images #ここを修正
end

投稿フォームの修正

file_fieldのimageをimagesに修正します。
また、name属性を追加し送信する際に必要な画像の配列を設定します。

app/views/recipes/new.html.erb
<%= form_with model: @recipe, local: true do |f| %>

#中略
    <div class="form-group">
      <label class="text-secondary">画像</label><br>
      <%= f.file_field :images, name: 'recipe[images][]' %> #ここを修正
    </div>

#以下略

<% end %>

コントローラーの修正

画像の配列を受け取れるようにストロングパラメーターを修正します。

app/controllers/recipes_controller.rb
class RecipesController < ApplicationController

#中略

  private
  def recipe_params
    params.require(:recipe).permit(:title, :text, :category_id, :time_required_id, images: []) #images: []に修正
  end
end

バリデーションの設定

今回、投稿できる画像を3枚までにしたいので、独自のバリデーションメソッドを作成していきます。Railsガイド

app/models/recipe.rb
class Recipe < ApplicationRecord
  has_many_attached :images 

#ここから追加
  validate :image_length #カスタムメソッドなので"validate"

  private

  def image_length
    if images.length >= 4
      errors.add(:images, "は3枚以内にしてください")
    end
  end
#ここまで追加
end

以上でActive Storage関連の修正は完了です。

preview.jsの修正

今回は以下のGIFのようにひとつ画像を選択すると新しく画像選択フォームが出現する仕様にします。
90c396247ee860b014d53f48d1c5f087.gif

まずは、前回作成したpreview.jsを確認してみましょう。

app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    const createImageHTML = (blob) => {
      const imageElement = document.getElementById('new-image');
      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      imageElement.appendChild(blobImage);
    };

    document.getElementById('recipe_image').addEventListener('change', (e) => {
      const imageContent = document.querySelector('img');
      if (imageContent){
        imageContent.remove();
      }

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}

こちらを修正していきます。

今回は、以下のようなHTMLを生成することを前提にpreview.jsを編集していきます。

/recipes/new
<div id="new-image">
<!-- ここから -->
  <div class="image-element">
    <img class="new-img" src="xxxxxxx...">
    <input id="recipe_image_n" class="recipe-images" name="recipe[images][]" type="file">
  </div>
<!-- ここまでを複製していくイメージ -->
</div>

前回まではid="new-image"のdiv要素に直接プレビュー画像を挿入していましたが、今回は新たなdiv要素を作成しその中に画像を挿入していきます。

そして、classにimage-elementを設定しquerySelectorAllメソッドとlengthメソッドで作成された要素の数を定数imageElementNumに格納します。

app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {

    const ImageList = document.getElementById('new-image'); //追記

    const createImageHTML = (blob) => {

      const imageElement = document.createElement('div'); //new-imageからdivに変更

//ここから
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length
//ここまで追記

      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      imageElement.appendChild(blobImage);
      ImageList.appendChild(imageElement); //追記
      });
    };

    document.getElementById('recipe-images').addEventListener('change', (e) => { //recipe-imagesに修正

//ここから
     //const imageContent = document.querySelector('img');
      //if (imageContent){
        //imageContent.remove();
      //}
//ここまで削除

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}

次に新しく画像選択フォームを作成する記述をしていきます。

createElementメソッドでinput要素を生成し、setAttributeで属性を追加していきます。
そして、inputのidに先ほど宣言したimageElementNumを使いinput要素が何番目の要素かを判別します。

app/javascript/packs/preview.js
//中略
document.addEventListener('DOMContentLoaded', () => {
    const ImageList = document.getElementById('new-image');
    const createImageHTML = (blob) => {
      const imageElement = document.createElement('div');
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length

      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

//ここから
      const inputHTML = document.createElement('input');
      inputHTML.setAttribute('id', `recipe_image_${imageElementNum}`);
      inputHTML.setAttribute('class', 'recipe-images');
      inputHTML.setAttribute('name', 'recipe[images][]');
      inputHTML.setAttribute('type', 'file');
//ここまで追記

      imageElement.appendChild(blobImage);
      imageElement.appendChild(inputHTML); //追記
      ImageList.appendChild(imageElement);

//以下略

これで、画像を選択すると新しく画像選択フォームが出現するようになりました。
2枚目以降にもイベント発火するよう処理を記述していきます。
1度目に発火するイベントとほとんど同じ記述になります。

app/javascript/packs/preview.js
//中略

    const createImageHTML = (blob) => {
      const imageElement = document.createElement('div');
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length

      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      const inputHTML = document.createElement('input');
      inputHTML.setAttribute('id', `recipe_image_${imageElementNum}`);
      inputHTML.setAttribute('class', 'recipe-images');
      inputHTML.setAttribute('name', 'recipe[images][]');
      inputHTML.setAttribute('type', 'file');

      imageElement.appendChild(blobImage);
      imageElement.appendChild(inputHTML);
      ImageList.appendChild(imageElement);

//ここから
      inputHTML.addEventListener('change', (e) => {
        const file = e.target.files[0];
        const blob = window.URL.createObjectURL(file);
        createImageHTML(blob);
      });
//ここまで追記
    };

//以下略

ここまでで複数の画像投稿に対応したプレビュー機能の実装が完了しました。

しかし、これでは1枚画像を選択するたびに新しく画像選択フォームが出現してしまうので何枚でも画像選択が可能になってしまいます。
9e0000ca5ed8bee348ff2752f8962bef.gif
モデルにバリデーションをかけているので投稿は保存されません。
61635f5521f4d56c6380ed39a77dea34.png

条件分岐で制限をかけよう

input要素の数える定数imageElementNumを使い、3枚選択すると新しく画像選択フォームが出現しないように条件分岐していきましょう。

app/javascript/packs/preview.js
//中略

      const inputHTML = document.createElement('input');
      inputHTML.setAttribute('id', `recipe_image_${imageElementNum}`);
      inputHTML.setAttribute('class', 'recipe-images');
      inputHTML.setAttribute('name', 'recipe[images][]');
      inputHTML.setAttribute('type', 'file');

      imageElement.appendChild(blobImage);
      if (imageElementNum < 2) {            //追記
        imageElement.appendChild(inputHTML);
      }                                     //追記
      ImageList.appendChild(imageElement);

      inputHTML.addEventListener('change', (e) => {
        const file = e.target.files[0];
        const blob = window.URL.createObjectURL(file);
        createImageHTML(blob);
      });
//以下略

以上で完成です。
32e67abef3b9fedea1327e930a9556a2.gif
以下、完成形のコードです。

app/javascript/packs/preview.js
if (document.URL.match(/new/)){
  document.addEventListener('DOMContentLoaded', () => {
    const ImageList = document.getElementById('new-image');
    const createImageHTML = (blob) => {
      const imageElement = document.createElement('div');
      imageElement.setAttribute('class', "image-element")
      let imageElementNum = document.querySelectorAll('.image-element').length

      const blobImage = document.createElement('img');
      blobImage.setAttribute('class', 'new-img')
      blobImage.setAttribute('src', blob);

      const inputHTML = document.createElement('input');
      inputHTML.setAttribute('id', `recipe_image_${imageElementNum}`);
      inputHTML.setAttribute('class', 'recipe-images');
      inputHTML.setAttribute('name', 'recipe[images][]');
      inputHTML.setAttribute('type', 'file');

      imageElement.appendChild(blobImage);
      if (imageElementNum < 2) {
        imageElement.appendChild(inputHTML);
      } 
      ImageList.appendChild(imageElement);

      inputHTML.addEventListener('change', (e) => {
        const file = e.target.files[0];
        const blob = window.URL.createObjectURL(file);
        createImageHTML(blob);
      });
    };

    document.getElementById('recipe-images').addEventListener('change', (e) => {

      const file = e.target.files[0];
      const blob = window.URL.createObjectURL(file);
      createImageHTML(blob);
    });
  });
}

次回は投稿された複数の画像をスライド形式で表示する実装を行っていきます。

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

Javascriptで端末判定

結論

<script type="text/javascript"><!--
function getPlatform() {
    // IE か
    var isIe = (String.prototype.hasOwnProperty('startsWith') === false);
    // Winか否か
    var isWinPc = navigator.platform.startsWith('Win') || isIe;
    // Androidか否か
    var isAndroid = navigator.userAgent.indexOf('Android') > 0;
    // Macか否か
    var isApple = navigator.platform.startsWith('Mac') || navigator.platform.startsWith('iPhone') || navigator.platform.startsWith('iPad');
    // AppleMobileか否か
    var isAppleMoblile = isApple && (typeof navigator.standalone !== 'undefind' && navigator.standalone === false);
    // AppleDesctopか否か
    var isAppleDesctop = isApple && !isAppleMoblile;
    if (isWinPc) {
        return 'win';
    }
    if (isAndroid) {
        return 'android';
    }
    if (isAppleMoblile) {
        return 'apple_mobile';
    }
    if (isAppleDesctop) {
        return 'mac';
    }
    return false;
}
--></script>

参考: navigator
https://developer.mozilla.org/ja/docs/Web/API/Window/navigator

参考: navigator.standalone
https://developer.mozilla.org/ja/docs/Web/API/Navigator

論理コード

VAR IEである = 'String' に'startWith'がない。

VAR Windowsである = 'navigator.platform' が 'Win' で始まる

VAR Androidである = 'navigator.userAgent' に 'Android' がある。

VAR Appleの端末である = 
    ('navigator.platform' が 'Mac' で始まる) または 
    ('navigator.platform' が 'iPhone' で始まる) または
    ('navigator.platform' が 'iPad' で始まる)

VAR Appleのモバイル端末である = 
    ('navigator.standalone' が 'undefind' ではない) かつ
    ('navigator.standalone' が 'false' である)
 
VAR Appleのデスクトップ端末である = 
    (Appleの端末である)かつ
    (Appleのモバイル端末である の否定)

解説1VAR IEである = 'String' に'startWith'がない。

IEのみ startWith をサポートしていないので利用する。
参考:
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith

解説2 VAR Windowsである = 'navigator.platform' が 'Win' で始まる

Windowの場合は簡単で、navigator.platform の返り値に Win が含まれている。
これを利用しない手はない。

解説3VAR Androidである = 'navigator.userAgent' に 'Android' がある。

今回、Androidのみ、UserAgentを利用している。理由は2つ。

1: navigator.platform の返り値が カーネルになっているため。
2: 他にスマートな方法が思いつかなかった。

の2点。
...UserAgentが廃止になったらどうすべきか。

解説4 アップル端末について判定

Appleの端末だけ段階的に判定を行います。理由は2つ。

1: navigator.platform の返り値が端末の種類やブラウザ設定によって切り替わるため
2: navigator.userAgentが端末のバージョンごとに違うため(またはデスクトップとモバイルで同じになるため)

  • ステップ1

    • navigator.platform で Mac iPhone iPad を網羅してApppleの端末であることを切り分ける
    • ※ iPhone、iPadでも 返り値が Macになることがあるためこれだけでは判定できない。
  • ステップ2

    • navigator.standaloneでiOSサファリであることを確認する。
    • このプロパティはMDSによるとiOSサファリにしか実装されておらず、
    • 端末がスタンドアロンで起動している場合は TRUE を返すそう。
    • 画面分割は試してない。けどしらん!
  • ステップ3

    • ステップ1とステップ2を基に Mac か iOS かを判定する。

以上。

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

メモ化とは?かるーくまとめてみた!

概要

Reactを使ってよく耳にするのがメモ化。
正直今まで避けて通ってました、、。
しかし「今こそ!」しっかりと理解することでより良いアプリを作ることができると思うので、
学びつつ共有して行こうと思います〜!

この記事は、

  • メモ化ってよく聞くけどざっくりとしかわからない
  • メモ化ってどんな時に使うの?
  • 実際どんな動きをするの?

っていう方に有益な記事かもです!

メモ化とは?

まず、メモ化って言われても何かよくわかってなかったりしますよね。

そんな方に説明しますと、
「計算結果を保持することで、もう一度計算を行うときに変更がなければ再利用する」
というものです。

簡単に言えば、キャッシュ的なことです。

計算結果を保持していることで、
もう一度同じ結果を返す場合に
その都度計算し直すことがなくなるので
パフォーマンスが向上するよってことです。

また具体的には、
React.memouseCallbackuseMemoという手段があります。

メモ化は再度同様の計算をする必要がない分パフォーマンスが上がる反面、
これらの手段を使うためのコストの方が高いという場合もあり、
むしろ使った方が余計にメモリを食ってしまう。
なんてこともあるのでここでしっかりと理解して上手く使えるようになりましょう!

React.memo

React.memoはどんなことができるのでしょうか。
主に、コンポーネントをメモ化し、余計な再レンダリングをスキップできるのです。

つまり、

  • レンダリングにコストがかかるコンポーネントに対して余計なレンダリングをしないようにする
  • カウントアップに関するコンポーネントのように、 何度も頻繁にレンダリングがされるコンポーネントに対して使う

このような場合に使用するとパフォーマンスをあげることができます。

では、どのように使うのかという話をしていきます。
⬇︎例

import React, { useState } from "react";

const Count2Component = React.memo(props => {
  return <p>count2: {props.count}</p>;
});

export const App = () => {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const countUp1 = () => setCount1(c => c + 1);
  const countUp2 = () => setCount2(c => c + 1);


  return (
    <>
      <button onClick={countUp1}>count1</button>
      <button onClick={countUp2}>count2</button>
      <p>count1: {count1}</p>
      <Count2Component count={count2} />
    </>
  );
}

通常、count1ボタンを押すと、
Appコンポーネント自体が再レンダリングされるので、
stateの変化があった<p>count1: {count1}</p>に加え、<Count2Component count={count2} />も再レンダリングされるはずです。

しかし、実際上記のコードを動かすと<p>count1: {count1}</p>しか再レンダリングされません。

これがどういったことを意味するのかというと、
React.memo(コンポーネント)の形で生成されたコンポーネントは、
「親の再レンダリングに関係なく、
そのコンポーネントに依存する値(ここではcount2)に変更があった時のみ再レンダリングが発生する」
ということになります。

この場合、
count1ボタンが押され、Appコンポーネントが再レンダリングされてもcount2自体に変更がないので<Count2Component count={count2} />は再レンダリングされないということになります。

useCallback

次に、useCallbackはどう使うのかという話をしていこうと思います。

まずuseCallbackができることは、
useCallbackはメモ化されたコールバックを返すことができるhookです。

つまりはどういうことかというと、
例えば親コンポーネントから渡されるpropsが変化する場合に、通常はこのコンポーネントは全部再レンダリングされます。

しかし、もしこのpropsが自身のコンポーネントにとって関係のないpropsである場合、メモ化されたコールバックを返すことでパフォーマンス向上が期待できるといったものです。

まあとりあえず例ですね。
⬇︎例

import React, { useState, useCallback } from "react";

const Count2Component = React.memo(props => {
  return <p>count2: {props.count}</p>;
});

export const App = () => {
  console.log("render App");
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
  const countUp1 = () => setCount1(c => c + 1);
  const countUp2 = useCallback(() => setCount2(c => c + 1), [count2]);


  return (
    <>
      <button onClick={countUp1}>count1</button>
      <button onClick={countUp2}>count2</button>
      <p>count1: {count1}</p>
      <Count2Component count={count2} />
    </>
  );
}

上記のコードでは、
count1ボタンが押され、Appコンポーネントが再レンダリングされてもcount2自体に変更がないので<Count2Component count={count2} />は再レンダリングされません。

これは、useCallback
useCallback(コールバック関数, 依存配列)という構文における依存配列(ここではcount2)の値に変化がないからです。

そして上記の例にて一番重要なのは、
Count2Componentにて、React.memoが使用されている点です。

これがないとuseCallbackが上手く機能しないんですよね。

なぜかというと、useCallbackはcountUp2がメモリ内の、つまりメモ化された値と同じかどうかでCount2Componentを再レンダリングするか判断するものです。

何が言いたいかというと、React.memoを使わず、親のコンポーネントが再レンダリングされたらuseCallbackがあろうとなかろうと意味がないのです。

つまりはuseCallbackは親コンポーネントが再レンダリングされないからこそ初めて意味を持つんですよね。

ここは絶対注意が必要なので抑えておきましょう!

useMemo

最後に、useMemoについて話そうと思います!

useMemoとは、簡単にいうと値を計算するロジックなどで余計な計算をスキップしてパフォーマンスを向上させよう!といったものです。

⬇︎例

import React { useState, useMemo } from "react";

export const App = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const countUp = c => {
    let i = 0;
    while (i < 1000000000) i++;
    return c += 1;
  };

  const count = useMemo(() => countUp(count2), [count]);
  return (
    <>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>countUp</button>
      <p>
        Counter: {count2} | {countUp}
      </p>
      <button onClick={() => setCount2(count2 + 1)}>countUp</button>
    </>
  );
}

通常、count1はcount1に+1するだけの処理だが、useMemoがない状態で実行するとcountUpが実行されて再レンダリングに時間がかかってしまいます。

しかし上記のコードでは、countUpをメモ化することでcount1が再レンダリングされてもcountUpは実行されず、高速にコンポーネントを再レンダリングできます。

useMemoはレンダリング結果もメモ化できるらしいのでコンポーネントの再レンダリングをスキップすることができるらしいです。
詳しくはこの参考記事を参照ください!(てかほとんどここを参照してますw)
https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2

まとめ

これらははじめにいったようにどこで使うかが重要で、どこかしらにもポンポン使うべきではありません。

特に、関数宣言はコストの安い処理なのでuseCallbackを使うべきではないです。

これらはレンダリングの度に動きますし、最適化が必要なときは思った以上にないっぽいので見極めは大切だと思います。

終わりに

メモ化は、正直使わなくとも開発自体にはあまり影響がないかもです。

しかし、使うことでアプリの使いやすさ、パフォーマンスが向上するので適材適所、上手く分析して使うといいと思います。

まあ結局は使う必要性がでたらって感じで問題ないと思いますね!
なので日頃からパフォーマンスチューニングを心がけるといいのでは!

参考資料

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

ReactでPaginationを実装する(material-ui)

はじめに

10件のアイテムを1ページあたり3件、計4ページに渡って表示するサンプルです。

実装

サンプルアイテム

const items = [
  {
    id: '1',
    name: 'abe',
    from: 'nagoya'
  },
  {
    id: '2',
    name: 'ito',
    from: 'iwate'
  },
  {
    id: '3',
    name: 'uno',
    from: 'fukuoka'
  },
  {
    id: '4',
    name: 'kato',
    from: 'tokyo'
  },
  {
    id: '5',
    name: 'kurata',
    from: 'tottori'
  },
  {
    id: '6',
    name: 'sato',
    from: 'kagoshima'
  },
  {
    id: '7',
    name: 'takahashi',
    from: 'saitama'
  },
  {
    id: '8',
    name: 'nishida',
    from: 'miyagi'
  },
  {
    id: '9',
    name: 'hasegawa',
    from: 'gifu'
  },
  {
    id: '10',
    name: 'yamada',
    from: 'aomori'
  }
]

本体

import React, { useState, useEffect } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import {
  List,
  ListItem,
  ListItemText,
  ListItemAvatar,
  Avatar,
  Typography
} from '@material-ui/core';
import Pagination from '@material-ui/lab/Pagination';


const useStyles = makeStyles((theme) => ({
  list: {
    width: '100%',
    maxWidth: 360,
    backgroundColor: theme.palette.background.paper,
  },
  pagenation: {
    '& > *': {
      marginTop: theme.spacing(2),
      display: 'inline-block',
    }
  },
  }));

const ListView = () => {

  const classes = useStyles();

  const [page, setPage] = useState(1); //ページ番号
  const [pageCount, setPageCount] = useState(); //ページ数
  const [allItems, setAllItems] = useState([]); //全データ
  const [displayedItems, setDisplayedItems] = useState([]); //表示データ
  const displayNum = 3; //1ページあたりの項目数

  const items = [...]


  useEffect(() => {
    setAllItems(items);
    //ページカウントの計算(今回は3項目/ページなので4ページ)
    setPageCount(Math.ceil(items.length/displayNum));
    //表示データを抽出
    setDisplayedItems(items.slice(((page - 1) * displayNum), page * displayNum))
  }, [])

  const handleChange = (event, index) => {
    //ページ移動時にページ番号を更新
    setPage(index);
    //ページ移動時に表示データを書き換える
    setDisplayedItems(allItems.slice(((index - 1) * displayNum), index * displayNum))
  }

  return (
    <>
      <List dense className={classes.list}>
        {displayedItems.map((item, index) => {
          const labelId = `label-${item.id}`;
          return (
            <ListItem key={index} button>
              <ListItemAvatar>
                <Avatar alt={item.name} src="/static/images/avatar/xxx.jpg" />
              </ListItemAvatar>
              <ListItemText
                id={labelId}
                primary={item.name}
                secondary={
                  <>
                    <Typography
                      component="span"
                      variant="caption"
                      display="block"
                      color="textPrimary"
                    >
                      {item.from}
                    </Typography>
                  </>
                } />
            </ListItem>
          );
        })}
      </List>
      <div className={classes.pagenation} style={{textAlign:'center'}}>
        <Pagination count={pageCount} page={page} variant="outlined" color="primary" size="small" onChange={handleChange} />
      </div>
    </>
  )
}

export default ListView;

実行結果

スクリーンショット 2021-03-06 20.58.50.png

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

【TypeScriptハンズオン③】男もすなるTypeScriptといふものを、女もしてみむとてするなり ~webアプリとして動かそう!~

この記はなに

TypeScriptを実際に触りながらTypeScriptを説く記の第3弾です。
TypeScriptが しょしんなる 方や、TypeScriptを いとをかし と思っているひとが対象です。

この記で、こころよしと思った方は、LGTM✨を何卒よしなに願い申し上げ候

過去と未来?
1. 【TypeScriptハンズオン①】男もすなるTypeScriptといふものを、女もしてみむとてするなり
2. 【TypeScriptハンズオン②】 自分だけの型を作ろう!
3. 【TypeScriptハンズオン③】 webアプリとして動かそう! この記事

@ TypeScriptをインストールしていないひとはこちら?
  【画像で説明】シンプルにTypeScriptを導入して使う方法
@ TypeScriptをいとをかしと思ったひとはこちら?
 JavaScriptを知っている方がTypeScriptをなんとなく理解するための記事

ハンズオン

✅ これ以降、古語は出てきません。ご安心ください
✅ VSCodeを使います

TypeScriptは、変数や関数に型を明記することで、開発者がハッピー?になります。
今回は、このハッピー?になれるTypeScriptを使って、書いたコードを実際にブラウザ上で表示できるようにしてみましょう?

プロジェクトのセットアップ

まずは、プロジェクトをセットアップする必要があるので、以下の記事にてセットアップをお願いします。
【画像で説明】シンプルにTypeScriptを導入して使う方法

この記事では、以下のようなコマンドを使って環境を構築しています。

$ mkdir new-typescript               // プロジェクト用のディレクトリを作成する
$ cd new-typescript                  // プロジェクト用のディレクトリに移動する
$ npm init                           // package.json(プロジェクト管理用のファイル)を作成する
$ npm install typescript --save-dev  // typescriptをインストールする

この結果以下のようなディレクトリ構成になります。

image.png

ここで、ソースコードを入れるsrcディレクトリを用意しましょう。

$ pwd                // 現在のディレクトリが/new-typescriptであることを確認
/new-typescript
$ mkdir src          // "src"ディレクトリを作成する

image.png

次に、src内に.html.tsファイルを作成します。

image.png

最後に、index.htmlファイルに以下の内容をコピペしましょう。

<!DOCTYPE html>
<html>
  <head>
    <title>土佐日記を読もう</title>
  </head>
  <body>
    <h1>土佐日記を読もう</h1>
    <p>次のフォームに、「紀貫之」と入力してね</p>
    <input type="text" id="text" />
    <button onclick="getBookInfo()">確定</button>
    <p id="details"></p>
    <script src="./book.js"></script>
  </body>
</html>

このhtmlは以下のような画面を表示します。
image.png

最終的には、inputフォームに紀貫之を入力して、確定ボタンを押したら、土佐日記の内容が表示されるようにしていきます。

image.png

.tsに記述してアプリを完成する

では、実際に.tsファイルにスクリプトを記述していきます。
何を記述すればいいのかを確認するために、一旦htmlを見てみましょう。

<!DOCTYPE html>
<html>
  <head>
    <title>土佐日記を読もう</title>
  </head>
  <body>
    <h1>土佐日記を読もう</h1>
    <p>次のフォームに、「紀貫之」と入力してね</p>
    <input type="text" id="text" />
    <button onclick="getBookInfo()">確定</button>
    <p id="details"></p>
    <script src="./book.js"></script>
  </body>
</html>
  1. <input type="text" id="text" />は、紀貫之と入力するフォームです。ここから、入力した文字を取得します。
  2. <button onclick="getBookInfo()">確定</button>はクリックすると、getBookInfo()関数を呼び出します。
  3. getBookInfo()関数では、<input />で入力したテキストを取得し、「紀貫之」というテキストが入力されたら、<p id="details"></p>に土佐日記の内容を表示します。

それを実現したのが、以下のコードです。(ここでは、素のJavaScriptです)

const getBookInfo = () => {
    const inputText = document.getElementById("text").value;
    if (inputText === "紀貫之") {
        document.getElementById("details").innerText =
            "男もすなる日記といふものを、女もしてみむとてするなり。それの年(承平四年)のしはすの二十日あまり一日の、戌の時に門出す。そのよしいさゝかものにかきつく。ある人縣の四年五年はてゝ例のことゞも皆しをへて、解由など取りて住むたちより出でゝ船に乘るべき所へわたる。かれこれ知る知らぬおくりす。年ごろよく具しつる人々(共イ)なむわかれ難く思ひてその日頻にとかくしつゝのゝしるうちに夜更けぬ。廿二日(にイ有)、和泉の國までとたひらかにねがひたつ。藤原の言實船路なれど馬の餞す。上中下ながら醉ひ過ぎていと怪しくしほ海のほとりにてあざれあへり。廿三日、八木の康教といふ人あり。この人國に必ずしもいひつかふ者にもあらざる(二字ずイ)なり。これぞ正しきやうにて馬の餞したる。かみがらにやあらむ、國人の心の常として今はとて見えざなるを心あるものは恥ぢずき(ぞイ)なむきける。これは物によりて譽むるにしもあらず。";
    }
    else {
        document.getElementById("details").innerText = "?????";
    }
};

「確定」ボタンを押した時にdocument.getElementById("text").valueにて、フォームで入力した値を取得し、inputTextに格納します。
その後、if文にてinputText紀貫之が入っていたら、document.getElementById("details").innerTextに、土佐日記の内容を代入します。

紀貫之でなかったら、"?????"を入れます。

以上のJavaScriptのコードは問題ないように見えますが、.tsファイルに入れるとエラーが発生します。
image.png

これは、document.getElementById("text").valueのように記述するとき、.valueが使えるのは<input />要素を利用するときです。
そのため、document.getElementById("text")自体は<Input />要素であると教えてあげる必要があります。
つまり、 <Input />要素でないとdocument.getElementById("text").valueのように記述出来ないことを示しています。

そこで、型を設定してあげます。

- const inputText = document.getElementById("text").value;
+ const inputText = (document.getElementById("text") as HTMLInputElement).value;

ここでは、as HTMLInputElementというのを付け加えることで、document.getElementById("text")はInputの要素によるものだよーというのを明示的に表すことができます。

そのため、最終的には次のようなコードに書き換えます。

const getBookInfo = () => {
  const inputText = (document.getElementById("text") as HTMLInputElement).value;
  if (inputText === "紀貫之") {
    document.getElementById("details").innerText =
      "男もすなる日記といふものを、女もしてみむとてするなり。それの年(承平四年)のしはすの二十日あまり一日の、戌の時に門出す。そのよしいさゝかものにかきつく。ある人縣の四年五年はてゝ例のことゞも皆しをへて、解由など取りて住むたちより出でゝ船に乘るべき所へわたる。かれこれ知る知らぬおくりす。年ごろよく具しつる人々(共イ)なむわかれ難く思ひてその日頻にとかくしつゝのゝしるうちに夜更けぬ。廿二日(にイ有)、和泉の國までとたひらかにねがひたつ。藤原の言實船路なれど馬の餞す。上中下ながら醉ひ過ぎていと怪しくしほ海のほとりにてあざれあへり。廿三日、八木の康教といふ人あり。この人國に必ずしもいひつかふ者にもあらざる(二字ずイ)なり。これぞ正しきやうにて馬の餞したる。かみがらにやあらむ、國人の心の常として今はとて見えざなるを心あるものは恥ぢずき(ぞイ)なむきける。これは物によりて譽むるにしもあらず。";
  } else {
    document.getElementById("details").innerText = "?????";
  }
};

このコードをbook.tsに書けたら、以下のコマンドを実行して、.tsファイルを.jsファイルに変換します(コンパイル)。

npx tsc book.ts

これは、htmlファイルではあくまで.jsファイルでないとを読み込めないので、TypeScriptのファイルはJavaScriptのファイルを変換する必要があるからです。そのため、このコマンドを実行すると、以下のようにファイルが生成されます。

image.png

これで、動かせるようになったので、ブラウザで表示してみましょう!

Pathをコピーして、ブラウザのURLに貼ります。
image.png

問題なく動きました!!!
image.png

おわりに

TypeScriptを使ったアプリの作成方法について紹介しました。
今回は、一部しか型定義を使っていないので、コードを大きくして様々な箇所で活用できるようにしておきましょう!

参考文献

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

スクロールで動的に追加される要素をユーザスクリプトで加工する

<div id="recommendations">
  <div><!-- ページ表示時のHTMLに静的に存在している要素 -->
    <a class="recommendation">...</a>
    <a class="recommendation">...</a>
    <a class="recommendation">...</a>
  </div>
  <div><!-- スクロール時に動的に追加された要素 -->
    <a class="recommendation">...</a>
    <a class="recommendation">...</a>
    <a class="recommendation">...</a>
  </div>
</div>

上記のようにスクロールに応じて、一定個数ずつ動的に読み込まれ表示されていくサイトの場合、ユーザスクリプトでページ表示時にHTMLの加工を実施すると、スクロールによって動的に追加される部分が処理されません。

不要なものを削除する系ならsetIntervalで定期実行しても十分ですが、DOMの追加に応じて丁寧に処理を挟みたい時には MutationObserverで要素の追加を監視します。

(function() {
    'use strict';
    const observer = new MutationObserver((mutations) => {
        //console.log(mutations)
        let m = mutations.filter(m => m.target.id == 'recommendations');
        if (m[0]) process(m[0].addedNodes[0]);
    });
    observer.observe(document.querySelector('#recommendations'), {
        childList: true,
        attributes: false,
        characterData: false,
        subtree: true,
    });
    function process(wrapEl) {
        Array.from(wrapEl.querySelectorAll('.recommendation')).forEach(function(el){
            let a = document.createElement('a');
            a.href = '#';
            a.innerHTML = '何か便利なリンク';
            el.querySelector('.title').appendChild(a);
        });
    }
    process(document.querySelector('#recommendations > div'));

})();

注意が必要な点として、ユーザスクリプト自身で行ったappendChild()の操作でも検知するため監視の条件によっては無限再起でハングアップします。
それを防ぐために変化の内容を確認してフィルタします。
監視の起点とする要素や監視の条件をうまく調整すれば省けるのですが、サイトによって作りが異なるのでそこまでは付き合わず、毎回フィルタするコードを書く方がやりやすいです。

応用例として、Qiitaでのタグから最新記事でのページネーションのようにJSで記事一覧を再構築している場合にも同じやり方が適用できます。

// ==UserScript==
// @name         nabeatsu
// @match        https://qiita.com/tags/*
// ==/UserScript==

(function() {
    'use strict';
    const observer = new MutationObserver((mutations) => {
        //console.log(mutations)
        let m = mutations
            .filter(m => m.addedNodes[0])
            .filter(m => m.addedNodes[0].tagName == 'DIV')
            .filter(m => !/Dummy/.test(m.addedNodes[0].className));
        if (m[0]) process(m[0].addedNodes[0]);
    });
    observer.observe(document.querySelector('[id^="TagNewestItemList-"]'), {
        childList: true,
        attributes: false,
        characterData: false,
        subtree: true,
    });
    function process(wrapEl) {
        Array.from(wrapEl.querySelectorAll('article')).forEach(function(el){
            let lgtm = el.querySelector('svg').nextSibling.textContent;
            if (/3/.test(lgtm) || (lgtm > 0 && lgtm % 3 == 0)) {
                let a = el.querySelector('h2 a');
                a.textContent = a.textContent.replace(/[a-zA-Z0-9]/g, m => String.fromCharCode(m[0].charCodeAt(0) + 0xFEE0));
            }
        });
    }
})();

LGTMの数字が3の倍数か3が含まれる時にタイトルの英数が全角になります。

Qiitaの場合、HTMLを読み込んだ初期状態で記事一覧は存在しないことと、
ローディング表示のダミー要素が一度出るためそれを無視するようなケアを行いましたが、
サイトが異なってもほぼ同じやり方が流用できそうでした。

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

JSのProxyで無量空処する

ProxyはES6から使えるようになった新しい術式です。
この術式は非常に強力で、Proxyで包み込んだ人や呪霊の行う様々なアクション(術式の発動など)をトラップして、特定の処理を術式発動までの間に挟み込む事ができます。

一種の結界術のようなものだと思っていただければ良いかと思います。

この記事ではこの強力な術式を使って、現代最強の呪術師である五条悟の使う領域展開「無量空処」を再現してみたいと思います。

五条悟も最初から最強だったわけではない

まずはじめに、無下限呪術を覚えていない頃の五条悟に登場してもらいます。敵役はもちろん漏瑚です。

index.js
const 五條悟 = {
  name: "五條悟",
};

const 漏瑚 = {
  name: "漏瑚",
  "極ノ番「隕」"(target) {
    console.log("極ノ番「隕」発動!!");
    console.log(target.name + "は死ぬ");
    target.name = "死体";
  },
  蓋棺鉄囲山(target) {
    console.log("蓋棺鉄囲山発動!!");
    console.log(target.name + "は死ぬ");
  },
};

// 蓋棺鉄囲山で五条悟を攻撃する
漏瑚.蓋棺鉄囲山(五条悟);

これを実行してみると、当たり前ですが、五条悟は死にます。

$ node index.js 
蓋棺鉄囲山発動!!
五条悟は死ぬ

そして五条悟は最強と成った

無量空処は、無下限の内側を展開し、相手の知覚、伝達、生きるという行為に無限回の作業を強制する術式です。つまり無量空処の内側では、術式を発動するまでに無限回の作業が必要になってしまい、結果術式を発動できなくなるわけですね。

これをProxyを使って再現すると、以下のようになります。

inde.js
const 無量空処の術式2 = {
  apply(target, thisArg, argumentsList) {
    // 無下限呪術を適用した関数(術式の発動)に無限回の施行を要求する
    while (true) {
      console.log(target.name + "するぞ!");
    }
    return target(...argumentsList);
  },
};

const 無量空処の術式 = {
  get(target, prop) {
    if (typeof target[prop] === "function") {
      return new Proxy(target[prop], 無量空処の術式2);
    }
  },
};

const 五条悟 = {
  name: "五条悟",
  /**
   * 領域展開「無量空処」
   * 領域に引きずり込んだ相手は術式発動のために無限回の施行が要求されるようになる
   * @param {*} target 領域に引きずり込む相手
   */
  無量空処(target) {
    return new Proxy(target, 無量空処の術式);
  },
};

「無量空処」の術式は2段階の術式です。1段階目の術式(無量空処の術式)で、敵の術式発動に干渉し、敵の術式発動までの間に2段目の術式を挟み込みます

const 無量空処の術式 = {
  get(target, prop) {
    // 敵が術式を発動する場合(functionを実行する場合)はその術式をProxy術式でラップする
    if (typeof target[prop] === "function") {
      return new Proxy(target[prop], 無量空処の術式2);
    }
  },
};

2段階目の術式では、術式発動に無限回の作業を強制します。ある意味これが無下限呪術の本体ですね

const 無量空処の術式2 = {
  apply(target, thisArg, argumentsList) {
    // 無下限呪術を適用した関数(術式の発動)に無限回の作業を要求する
    while (true) {
      console.log(target.name + "するぞ!");
    }
    // オリジナルの術式の発動(ここにたどり着くことはない)
    return target(...argumentsList);
  },
};

無量空処を覚えた五条悟であれば、漏瑚など敵ではありません。以下を実行してみましょう


const 無量空処の術式2 = {
  apply(target, thisArg, argumentsList) {
    // 無下限呪術を適用した関数(術式の発動)に無限回の施行を要求する
    while (true) {
      console.log(target.name + "するぞ!");
    }
    return target(...argumentsList);
  },
};

const 無量空処の術式 = {
  get(target, prop) {
    if (typeof target[prop] === "function") {
      return new Proxy(target[prop], 無量空処の術式2);
    }
  },
};

const 五条悟 = {
  name: "五条悟",
  /**
   * 領域展開「無量空処」
   * 領域に引きずり込んだ相手は術式発動のために無限回の施行が要求されるようになる
   * @param {*} target 領域に引きずり込む相手
   */
  無量空処(target) {
    return new Proxy(target, 無量空処の術式);
  },
};

const 漏瑚 = {
  name: "漏瑚",
  "極ノ番「隕」"(target) {
    console.log("極ノ番「隕」発動!!");
    console.log(target.name + "は死ぬ");
    target.name = "死体";
  },
  蓋棺鉄囲山(target) {
    console.log("蓋棺鉄囲山発動!!");
    console.log(target.name + "は死ぬ");
  },
};

const 無量空処を食らった漏瑚 = 五条悟.無量空処(漏瑚);

無量空処を食らった漏瑚.蓋棺鉄囲山(五条悟);

実行すると以下のようになります。

無量空処.gif

蓋棺鉄囲山の発動に無限回の作業が必要になってしまっていますね。

この術式はとても強力なので、相手が誰だろうが、どんな術式を持っていようが関係なく発動します。例えば真人相手でも

const 真人 = {
  name: "真人",
  無為転変(target) {
    console.log("無為転変発動!!");
    console.log(target.name + "は死ぬ");
  },
  自閉円頓裹(target) {
    console.log("自閉円頓裹発動!!");
    console.log(target.name + "は死ぬ");
  },
};

const 無量空処を食らった真人 = 五条悟.無量空処(真人);

無量空処を食らった真人.無為転変(五条悟);

こうなります
無量空処2.gif

もうあの人1人で良くないですか?

今回作った五条悟は、無量空処の領域内に取り込んだ相手からの術式を受けなくなりますが、領域外にいる相手からの攻撃は依然受けてしまいます。

以下を実行してみましょう

index.js
const 五条悟 = {
  name: "五条悟",
  /**
   * 領域展開「無量空処」
   * 領域に引きずり込んだ相手は術式発動のために無限回の施行が要求されるようになる
   * @param {*} target 領域に引きずり込む相手
   */
  無量空処(target) {
    return new Proxy(target, 無量空処の術式);
  },
};

const 漏瑚 = {
  name: "漏瑚",
  "極ノ番「隕」"(target) {
    console.log("極ノ番「隕」発動!!");
    console.log(target.name + "は死ぬ");
    target.name = "死体";
  },
  蓋棺鉄囲山(target) {
    console.log("蓋棺鉄囲山発動!!");
    console.log(target.name + "は死ぬ");
  },
};

// 無量空処を発動しない
漏瑚.蓋棺鉄囲山(五条悟);

無量空処3.gif

しかし五条悟には無下限呪術があります。これを自身の周囲に展開することで、攻撃が自身に到達するまでに無限回の作業を強制することができます。

この無下限呪術もProxyを使って再現することができます。

const 無下限呪術 = {
  get(target, prop) {
    while (true) {
      console.log("五条悟に触れることはできない・・・");
    }
  },
  set(target, prop) {
    while (true) {
      console.log("五条悟に触れることはできない・・・");
    }
  },
};

const 五条悟 = new Proxy(
  {
    name: "五条悟",
    /**
     * 領域展開「無量空処」
     * 領域に引きずり込んだ相手は術式発動のために無限回の施行が要求されるようになる
     * @param {*} target 領域に引きずり込む相手
     */
    無量空処(target) {
      return new Proxy(target, 無量空処の術式);
    },
  },
  // 無下限呪術を自身の周囲に展開する
  無下限呪術
);

先程までは相手に無下限呪術を展開していたわけですが、今回は自身の周囲に展開します。

この状態でもう一度、蓋棺鉄囲山で五条悟を攻撃してみましょう

const 無下限呪術 = {
  get(target, prop) {
    while (true) {
      console.log("五条悟に触れることはできない・・・");
    }
  },
  set(target, prop) {
    while (true) {
      console.log("五条悟に触れることはできない・・・");
    }
  },
};

const 五条悟 = new Proxy(
  {
    name: "五条悟",
    /**
     * 領域展開「無量空処」
     * 領域に引きずり込んだ相手は術式発動のために無限回の施行が要求されるようになる
     * @param {*} target 領域に引きずり込む相手
     */
    無量空処(target) {
      return new Proxy(target, 無量空処の術式);
    },
  },
  // 無下限呪術を自身の周囲に展開する
  無下限呪術
);

const 漏瑚 = {
  name: "漏瑚",
  "極ノ番「隕」"(target) {
    console.log("極ノ番「隕」発動!!");
    console.log(target.name + "は死ぬ");
    target.name = "死体";
  },
  蓋棺鉄囲山(target) {
    console.log("蓋棺鉄囲山発動!!");
    console.log(target.name + "は死ぬ");
  },
};

// 無量空処されていない漏瑚で攻撃
漏瑚.蓋棺鉄囲山(五条悟);

すると以下のようになります。漏瑚の攻撃は五条悟に無事届かなくなりました。無量空処なんて使わなくても五条悟は最強ってことですね

無下限呪術.gif

ちなみにこの無下限呪術は非常に強力で、極ノ番「隕」のような代入処理にも干渉できます。すごい

  "極ノ番「隕」"(target) {
    console.log("極ノ番「隕」発動!!");
    // 相手は死体になる
    target.name = "死体"; // targetが無下限呪術を発動している場合はこの処理が終わることはない
  },

まとめ

今回紹介したのは、Proxy呪術のごく一部です。今回は、変数の取得 / 代入 / 関数の実行に対してトラップを仕掛けましたが、他にも new演算子へのトラップ / in演算子へのトラップ / ownKeysへのトラップなどオブジェクトに対する様々な操作をトラップできます。詳しくは以下を見てみてください。
Proxy - JavaScript - MDN Web Docs -

現実世界で今回作ったような無下限呪術を使ってしまうと、動作しているシステムが五条悟という情報を処理しきれず止まってしまいますし、Proxy呪術自体もメタプロチックでわかりにくいコードを生む原因になりがちなので、採用には慎重になったほうが良いと思いますが、とてもおもしろい技術なので、もしよければみなさんもオリジナルの術式を作ってみてください

最後に、芥見下々先生、本当にすいませんでした

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

mdファイルの中に書き込んだJSが自動成型されなかったとき

大量のmdファイルを修正していたのですが1ファイルだけvscodeのprettierが効きませんでした。

そのためvscodeの設定なども見直したのですが解決しませんでした。

最終手段として各ファイルで修正してからコピペしようとし、mdファイルからjsファイルにコピペしたときに気づきました。

{}の数がズレてエラーが起きている!?

これに気づいてmdファイルを修正すると無事にprettierが効くようになりました!

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

Vue.jsのテーブルで動的にモーダルを表示させる

状況

Vue.jsでテーブルを作成し、テーブルのデータごとに動的にモーダルを表示させようとするも、何かおかしい。

前提

$ vue --version
@vue/cli 4.5.11

なお、本記事では Vue Materialのテーブルコンポーネント を使用しています。

百聞は一見にしかずということで

状況をアニメーションで貼っておきます。

sample.gif

見ての通り、どのモーダルボタンをクリックしても3番目の要素しか表示されない。
しかも背景がかなり暗い。

出来れば親でモーダルを発火させるボタンを押せて、モーダルの状態を親で管理できるようにしたい。

該当コード

Parent.vue
<template>
  <div>
    <md-table v-model="products">
      <md-table-row slot="md-table-row" slot-scope="{ item }">
        <md-table-cell md-label="ID">{{ item.id }}</md-table-cell>
        <md-table-cell md-label="ユーザー名">{{ item.user }}</md-table-cell>
        <md-table-cell md-label="アクション">
          <button @click="modalState = true">モーダル表示</button>
          <md-dialog :md-active.sync="modalState">
            <Child :user="item.user" :item-id="item.id" />
          </md-dialog>
        </md-table-cell>
      </md-table-row>
    </md-table>
  </div>
</template>

<script lang="ts">
import Child from './Child.vue'
import Vue from 'vue'

export default Vue.extend({
  name: 'TableContent',
  components: {
    Child,
  },
  data() {
    return {
      products: [
        { id: 1, user: '伊藤' },
        { id: 2, user: '田中' },
        { id: 3, user: '佐久間' },
      ],
      modalState: false,
    }
  },
})
</script>
Child.vue
<template>
  <div style="text-align: center">{{ itemId }} : {{ user }}</div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  name: 'Vue:Child',
  props: {
    itemId: {
      type: Number,
      required: true,
    },
    user: {
      type: String,
      default: '',
    },
  },
})
</script>

問題の原因

初歩的なミスをしていました。
modalStateにテーブルごとのユニークな値を与えていなかった為、ボタンを押すと全てのモーダルがtrueとなり、全て重なって表示されており、背景も暗くなっているということでした。

コード修正

Parent.vue
<template>
  <div>
    <md-table v-model="products">
      <md-table-row slot="md-table-row" slot-scope="{ item }">
        <md-table-cell md-label="ID">{{ item.id }}</md-table-cell>
        <md-table-cell md-label="ユーザー名">{{ item.user }}</md-table-cell>
        <md-table-cell md-label="アクション">
          <button @click="$set(modalState, item.id, true)">モーダル表示</button> <!-- テーブルごとのmodalStateを制御 -->
          <md-dialog :md-active.sync="modalState[item.id]"> <!-- ユニークな値を追加 -->
            <Child :user="item.user" :item-id="item.id" />
          </md-dialog>
        </md-table-cell>
      </md-table-row>
    </md-table>
  </div>
</template>

<script lang="ts">
import Child from './Child.vue'
import Vue from 'vue'

export default Vue.extend({
  name: 'TableContent',
  components: {
    Child,
  },
  data() {
    return {
      products: [
        { id: 1, user: '伊藤' },
        { id: 2, user: '田中' },
        { id: 3, user: '佐久間' },
      ],
      modalState: {}, // 定義をオブジェクト形式に変更
    }
  },
})
</script>

sample2.gif

無事動的にモーダルを表示することができました!

参考

https://stackoverflow.com/questions/49580396/vuetify-how-to-open-and-close-dialogs-within-a-data-table
https://vuematerial.io/components/table

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

1年間でpythonで収益化してみた

今日からpythonの勉強を始める
目標としてはこの1年で自分で案件を獲得し収益を生み出すこと

多くのサイトを閲覧しこの1年の大まかな計画を立てる
その中でこのサイトの手順が最も有効だと考えた

【初心者必見】プログラミング未経験から3年間のpython学習ロードマップ

基本的にこの手順に乗っ取って頑張ってみようと思う

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

1年間でpython収益化してみた

今日からpythonの勉強を始める
目標としてはこの1年で自分で案件を獲得し収益を生み出すこと

多くのサイトを閲覧しこの1年の大まかな計画を立てる
その中でこのサイトの手順が最も有効だと考えた

【初心者必見】プログラミング未経験から3年間のpython学習ロードマップ

基本的にこの手順に乗っ取って頑張ってみようと思う

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

Chrome拡張で通知を表示させる

Chrome拡張で通知を表示させる方法を調べたら、日本語の情報が少なかったので書いときます。

環境

OS:Windows10 Home 64bit 20H2
Chrome:88.0.4324.190

下準備

フォルダ構造
フォルダ
┣ manifest.json
┗ script.js

manifest.json
{
    "name": "notification",
    "version": "1.0",
    "manifest_version": 2,
    "icons": {
        "48": "icon.png"
    },
    "background": {
        "scripts": [
            "script.js"
        ],
        "persistent": false
    },
    "permissions": [
        "notifications"
    ]
}

chrome://extensions/を開いて「デベロッパー モード」をオン
「パッケージ化されていない拡張機能を読み込む」でフォルダを選択。これで準備はできました。

通知を表示させる

通知を表示させるには、chrome.notifications.createを使います。とりあえず簡単な通知を表示させてみます。

script.js
chrome.runtime.onInstalled.addListener(() => {
    // 通知を表示
    chrome.notifications.create("1", {
        iconUrl: "./icon.png",
        message: "メッセージ",
        title: "タイトル",
        type: "basic"
    }, noticeid);
});

function noticeid(id) {
    console.log(id);
}

chrome://extensionsで再読み込みすると、通知が表示されます。(アイコン手抜きでごめんなさい)
img_0.png

"1"は通知id(notificationId)です。数字じゃなくてもいいです。
これを使うと通知内容を後から変更できたり、通知を削除したりできます。指定しない場合は勝手に生成されます。
(Chrome42以前は指定が必要みたいです)

関数名を指定しておくと、その関数に通知idが返されます。Chrome42以前はこれを指定しないとダメみたいです

NotificationOptions

通知の内容をカスタマイズできます。非推奨になっているappIconMaskUrl,imageUrl,isClickableは書いてません。

buttons

iconUrl
現在非推奨(OSXでは表示されない)。通知に表示されるボタンのアイコンURL
title
ボタンのテキスト

buttons: [{iconUrl:"./btnIcon.png", title: "ボタンです"}]

両方設定するとこうなります。ボタンをクリックすると通知は消えます。
img_2.png
(アイコンが小さい...)
ボタンは2つまで追加できます。

contextMessage

messageよりも小さく表示されます。

contextMessage: "コンテキスト"

img_1.png

eventTime

img_3.png
赤で囲んだところをいじれます。

eventTime: Date.parse('February 28, 2021 00:00:00')

img_4.png

iconUrl

通知のアイコンを設定します。

iconUrl: "./icon.png",

items

複数のアイテムを指定できます。typeに"list"を指定しないとエラーが出ます。

type: "list",
items: [{title:"タイトル", message: "メッセージ"},{title:"title", message:"message"}]

img_5.png

message

message: "メッセージ"

通知のメッセージを指定します。

priority

通知の優先度。-2,-1,0,1,2で指定できますが、Windows,Mac,Linuxだと0,1,2しか使えないようです。

priority: 2

progress

0~100の間で指定します。typeには"progress"を指定してください。

type: "progress",
progress: 64

img_6.png

requireInteraction

Chrome50以降
trueにすると、ユーザーが通知をクリックするまで画面に表示され続けます。
デフォルトはfalseです

requireInteraction: true

silent

Chrome70以降
trueにすると、通知音がなりません。デフォルトはfalseです

silent: true

title

通知のタイトルを指定します。

title: "タイトル"

type

通知のタイプを"basic","image","list","progress"から指定します。

type: "basic"

参考

公式リファレンス

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

フォームのバリデーションをユーザーの入力が完了してから行う(JavaScript)

やりたいこと

フォームの入力に対するバリデーションをユーザーの入力が終わるまで待機して,入力が完了(入力欄からフォーカスが外れる)してからバリデーションをする

使用ライブラリ

  • Bootstrap 4
  • JavaScript

Bootstrapでバリデーション

Bootstrapではwas-validatedというクラスをinputタグを囲っているdivタグに追加することでバリデーションの結果を入力欄の枠の色で分かりやすく表示してくます.

実際に例としてメールを入力する欄を作成します. (必須の入力としています)

<body>
  <form action="" method="POST" enctype="multipart/form-data">
    <div class="form-group was-validated" id="mail_div">
      <label for="mail" class="font-weight-bold">E-mail</label>
      <input type="email" id="mail" required class="form-control" name="user_email" placeholder="Ex) sample@sample.com" autocomplete="email">
    </div>
    <div class="input">
      <button type="submit" class="btn btn-primary">Submit</button>
    </div>
  </form>
</body>

以下に実際の例を表示します.

See the Pen email valid always by yutake27 (@yutake27) on CodePen.

ここではメールの入力が必須となっているので何も入力されていない状態は不正な状態です.
そのため最初から赤い枠で入力欄が囲まれています.

しかしこれから入力を行うのに最初から赤で囲まれているのはあまり良い気分ではありません.

さらにこの状態では入力をしている最中にも文字を1文字打つごとにバリデーションの結果が更新されます.

上記入力欄で実際に試してもらうと分かりますが,sample@sample.comを打つ際にsample@sまで打ったところで一度入力欄の枠の色が緑になります.
スクリーンショット 2021-03-06 15.40.24.png
その後sample@sample.まで打ったところでもう一度枠が赤になります.
スクリーンショット 2021-03-06 15.41.39.png
sample@sample.cまで打つとようやく緑になります.

このように入力中にコロコロ枠の色が変わるのは好ましくありません.

色々な記事で入力中のバリデーションはやめた方がいいと指摘されていました.
入力フォームをリアルタイムチェックを実装する際に気を付けるべきポイント
ユーザーをイラつかせているかも?確実に避けたいフォームヴァリデーション(入力チェック機能)7つの禁じ手

そこでJavaScriptを用いて入力が完了してからバリデーションを行うようにしました.

ユーザーの入力が完了してからバリデーション

JavaScriptのコードは次のようになります.

const mail = document.getElementById('mail');
const mail_div = document.getElementById('mail_div');
mail.addEventListener('focus', TurnOffValid);
function TurnOffValid() {
  mail_div.classList.remove('was-validated');
}
mail.addEventListener('blur', validMail);
function validMail() {
  if (mail.value != "") {
    mail_div.classList.add('was-validated');
  }
}

ユーザーがmail入力欄にフォーカスを当てた際にwas-validatedクラスを削除して,入力欄からフォーカスが外れた際にwas-validatedクラスを追加しています.

さらに入力欄に文字が何も入力されていない時はユーザーは入力を開始していないと判断してwas-validatedクラスを付与しないようにしました.

実際の実行例が以下になります.


See the Pen
email valid after finish typing
by yutake27 (@yutake27)
on CodePen.


入力している最中は枠の色が青になり,入力が完了したら(入力欄からフォーカスを外したら)バリデーションの結果に応じて枠の色が変化すると思います.

感想

色々なところでこのような処理が行われていると思うのですが意外と参考になる実装があまりなくて(自分のサーチ力のせい?)JavaScript初心者の自分は苦労しました.

参考

イラっとしない超ミニマルなフォームバリデーションを素のJSで実装しよう
addEventListener type一覧と各ブラウザ対応
とほほのBootstrap 4入門

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

フロントサイトからCMSの編集画面に飛ぶブックマークレットを作ったら反響良かった話

はじめに

こんにちは。母親が誕生日なので、花キューピットで珍しく花送ってみた筆者です。
あれすごいですね、当日配送可能というほとばしる情熱:thumbsup: 皆さんもぜひ!

さて、自社インバウンドメディアの運営を行っているのですが、編集者の方々が、CMSで記事を執筆してくださっているのですが、そのなかで低コストのしょうもないブックマークレットを作ったのですが、結構喜ばれたので、他にもこれで幸せになる人いるかもと、本記事を執筆しております。

参考になれば幸いです。

背景

編集者の方々は、CMSとメディアサイトの往来を行う頻度が多いです。
その往来には、2パターン存在してます。

  1. CMSの記事編集ページ → メディアサイトの記事ページ
  2. メディアサイトの記事ページ → CMSの記事編集ページ

1については、記事のプレビューページの導線や公開済みの記事ページの導線がCMS上に表示されているため、スムーズに遷移可能です。

ただ、2については、記事タイトルや記事IDをコピーして、CMSを開いて記事検索して記事編集が画面にたどり着くという、地味にめんどくさい作業が発生してました。
(よくあるdebug用のheaderバー等も表示されていればいいんですがね... :thinking:)

また、弊社の場合、多言語のため、探すのも少し一苦労なのです...:sob:

よしブックマークレットでも作るか

そんなわけで、上記背景の2を1と同じワンクリックで遷移できるブックマークレットを作成しました。

実際のコードは公開できませんが、イメージこんな感じです。
記事ページ: http://example.com/1でブックマークをクリックすると、CMSの記事編集ページ: http://cms.example.com/edit/1 に別タブで表示させます。

javascript: (() => {
  const url_article = 'http://example.com/1'
  const url_cms_base = 'http://cms.example.com/edit/'
  const article_id = url_article.pathname.split`/`[1]
  window.open(`${url_cms_base}${article_id}`, "blank")
})();

おわりに

そもそもURL構成とかわかっていたので、自身が背景2のケースをわざわざ記事検索してたどり着くようなフローを取っていなかったですが、それでもこういったものがあるとポチるだけで飛べるのでとてもいいなと思いました。

書くコード量も少ないのに、結構喜びの声をいただきました:smile:

それでは!

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

【Javascript】const、let、varの違い <基本>

Javascriptで変数を宣言するときに、const、let、varという宣言のためのものがある。
この違いを簡単にまとめておく。

const

constは再代入をすることができない。

const title = "ゴリラ";
title = "おさるさん";

このようにすると、エラーとなってしまう。

let

letは最代入をすることができる変数の宣言方法。

let title = "さる";
title = "ゴリラ”;
title = "さかな";
title = "やまだくん":

このように何度でも最代入ができる。

var

varは同じ名前の変数を定義し、上書きすることができる.

let title = "ゴリラ";

let title = "たろう”;

上書きされる。

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

[Vue.js]main.jsのimport { createApp } from 'vue'の'vue'って何者??

(当たり前として捉えられているのかは知らないが)意外とこれ解説してる記事がどこにもなかったので、自分なりの見解を記載。

環境:
PC:mac book pro
バージョン:Vue.js 3
エディタ:VSCode

前提:
・Vue.js環境周りを全て設定完了
・(設定直後で)ファイルは何もいじっていない
・localhost:8080でサーバ起動できる

上記前提を元に以下ファイルを見ていったときに疑問が1つ。

main.js

import { createApp } from 'vue' ⇦こいつ
import App from './App.vue'
createApp(App).mount('#app')

2行目はまぁ、App.vueをmain.jsにインポートしてるってだけの話だからいいとして、
1行目のこいつ。
createAppはただの関数名(予約語という訳でもなく)と思われるのでここは問題ない。
createAppは関数であるものの、'vue'って何??
そんなファイルどこにもないぞ??

何者だこいつ???

(18:47 更新 解釈が全然違っていたため。すでに閲覧された方、ご迷惑をおかけしました。)
以下のコマンドを発行時に、my-projectディレクトリが生成され、その下に色々とファイルが生成される。

vue create my-project
※my-projectの部分は可変

その際に、「package.json」というjsonファイルが作成されるのだが、
どうやらその中に「dependencies」という情報が設定されているようで、(中身は環境によって異なる(バージョンやデータなど))その中のvueを使用するためのインポート文として、「import { createApp } from 'vue'」が記載されているらしい。

"dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0"
  }

以上。

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

ブラウザのタブ間でのリアルタイム通信を行う 4つの方法【概要編】

はじめに

過去、ネットワーク経由でのリアルタイム通信を行いたい時、よく MQTT や WebSocket を使って行っていました(それに関し、Qiita の記事も書いたりしてました)。

今回の記事の内容は、PC内の異なるページ間でリアルタイム通信を行おうとしたとき「サーバを経由しない方法で何か良さそうなやり方があるかな?」と思って調べた内容です。具体的には、調査して出てきた以下の記事の内容を一部抜粋したり翻訳したり、というのが主な内容です。

●4 Ways to Communicate Across Browser Tabs in Realtime | by Dilantha Prasanjith | Bits and Pieces
 https://blog.bitsrc.io/4-ways-to-communicate-across-browser-tabs-in-realtime-e4f5f6cbedca
ブログのトップ.jpg

余談

上記の記事にたどり着く前、p5.js のライブラリをいろいろ見ていた時に、以下のローカルでのメッセージ通信というものを見かけました。その仕組みを見てみると Service worker を使ったもののようでした。

●bmoren/p5.localmessage: p5.localmessage provides an interface to send messages locally from one sketch to another with a service worker for multi-window sketching!
 https://github.com/bmoren/p5.localmessage
p5jsのライブラリ.jpg

それで、Service worker と他の関連しそうなキーワードで検索していて、冒頭の記事にたどり着いたというのが今回の話のはじまりです。

ブラウザのタブ間でのリアルタイム通信を行う 4つの方法

4つの方法の概要

まずは、冒頭の記事に書かれた 4つのキーワードを列挙してみます。

  1. Local Storage Events
  2. Broadcast Channel API
  3. Service Worker Post Message
  4. Window Post Message

MDN のサイトで、上記に関するページもひっぱってきて、以下に補足付で列挙してみました。

  1. storage イベント: ストレージエリアが変更された時に発生、 Web Storage API に関連
  2. Broadcast Channel API: 閲覧コンテキスト(ウィンドウ、タブ、フレーム、iframe)間で、同じオリジンを使用して簡単に通信可能
  3. Client.postMessage(): サービスワーカーがクライアント (Window, Worker, SharedWorker) にメッセージを送信
  4. window.postMessage: Window オブジェクト間で安全にクロスドメイン通信を可能にするためのメソッド

ここから冒頭の記事の中のソースコードの部分を見ていきます。
それぞれの方法に注意書きがあったりもしますが、そのあたりは一部のみ抜粋して記載してみます。

【具体的なソース】 Local Storage Events

ここでは、Local Storage を使った仕組みの部分を見ていきます。

2つのタブがあるうち一方のタブで以下を実行して、Local Storage にデータを保存します。
JavaScript
window.localStorage.setItem("loggedIn", "true");

そして、もう一方のタブでは以下のイベントリスナーでイベントを検出し、上記で保存されたデータの読み出しも行う流れのようです。

window.addEventListener('storage', (event) => {
 if (event.storageArea != localStorage) return;
 if (event.key === 'loggedIn') {
   // Do something with event.newValue
 }
});

【具体的なソース】 Broadcast Channel API

ここでは、Broadcast Channel API を使った仕組みの部分を見ていきます。

2つのタブがあるうち一方のタブで、チャンネルの作成とメッセージ送信を行います。

const channel = new BroadcastChannel('app-data');
channel.postMessage(data);

そして、もう一方のタブでは以下のチャンネル作成・イベントリスナーでのイベントを検出を行って、メッセージを受信する流れのようです。

const channel = new BroadcastChannel('app-data');
channel.addEventListener ('message', (event) => {
 console.log(event.data);
});

【具体的なソース】 Service Worker Post Message

ここでは、Service Worker を使った仕組みの部分を見ていきます。

Service Worker によるメッセージ送信の処理は、以下となるようです。

navigator.serviceWorker.controller.postMessage({
 broadcast: data
});

そして、受信側の処理は以下のようになります。

addEventListener('message', async (event) => {
 if ('boadcast' in event.data ) {
  const allClients = await clients.matchAll();
  for (const client of allClients) {
   client.postMessage(event.broadcast);
  }
 }
});

元のサイトでは、この方法については「Service Worker API の知識が必要になる」という学習コストの面についてコメントされています。他の方法がうまく活用できるようであれば、その他の方法を用いるほうがシンプルに実行できる、という感じのようです。

【具体的なソース】 Window Post Message

ここでは、Window Post Message を使った仕組みの部分を見ていきます。

「One of the traditional ways」と書かれていて、以前からある王道という感じです。
メッセージの送信側の処理は以下となります。

targetWindow.postMessage(message, targetOrigin)

受信側の処理は以下のとおりです。

window.addEventListener("message", (event) => {
  if (event.origin !== "http://localhost:8080")
    return;
  // Do something
}, false);

この方法について「クロスオリジンでの通信が可能であるものの、今回のタブ間通信の用途では制限をかけている」という記載があります。
上記のソースコードで言うと if (event.origin !== "http://localhost:8080") の部分で、異なるオリジン同士での通信に制限をかけている、というものです。

おわりに

今回は概要を記載しただけになりましたが、今後は特定のものを試してみて、できればそれの記事化もしてみようと思います。

まずは試すとしたら、自分は以下を選んでみようかと思っています。

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

JsでUnixタイムスタンプを取得する

JavascriptでUnixタイムスタンプを取得する

firebaseを使うにあたってタイムスタンプ変換を学習したので備忘録的に残すことにしました。

Unixタイムスタンプとは

UTCで1970年1月1日 00:00 からの経過時間のこと
単位は秒数

タイムスタンプを取得する

現在時刻のタイムスタンプを取得

console.log(new Date().getTime()); 

指定した時間のタイムスタンプを取得

let d = new Date(2000,3,1,10,30).getTime()); 
//getTime()ではミリ秒で取得されるので、1000で割ります
console.log(Math.floor( d / 1000 ));
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

5 Simple Tricks to Boost Your Website Performance

Hi, Jerfareza here.
This time I would like to share a collection of tricks that I'm implementing in my websites to improve their performance.

1. Use Smaller Font File

If you self-host your fonts, try to use modern file extensions woff or woff2 format instead of ttf. ttf (True Type Font) fonts are considered the 'old' type, developed as far as the 1980s. They typically have larger sizes than their counterparts woff (Web Open Font Format), a rather new type developed as recently as 2009 woff.

The size difference is evident here.

01.png

There are numerous online converters out there to help you convert ttf to woff/woff2 easily.

2. Avoid Adding Unnecessary Package

Every npm package that you add to your bundle is a cost. That's the undeniable truth. Do you really need that package that you may replace with a few lines of code or that package that you only use once on your page?

One of the popular utility packages for time and date is moment.js. It's handy, yes, but if not used correctly, it may bloat the size of your client bundle. I stopped using it a while ago since I don't really show many dates on my website.

Always think before adding a new package to your code. The default is not to use. But if you have to add, use Bundlephobia to find out how big is the package you want, or if it has a better, slimmer alternative.

3. Defer Not Critical Third-Party Scripts

The easiest example of this is Google Analytics, which is an indispensable tool for all website owners. But it's also blocking the main thread and make Lighthouse complains every single time.

02.png

To deal with this, try to load the scripts last in your code. Typically you can put it in the last position before the closing tag of HTML.

In React I do something like:

        {/* Global Site Tag (gtag.js) - Google Analytics */}
        <script
            defer
            src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
        />
    </Html>

This will result in the script being called last:

03.png

4. Utilize Code Splitting

Code splitting is the splitting of code into various bundles or components, which can then be loaded on-demand or in parallel. (from Mozilla)

Bundlers like Webpack or Rollup make it easy for us to separate some parts of our website page to load independently beside the main thread. With React it's even easier: dynamic-import.

import("./expensiveComponent").then(component => {
  <>YOUR HEAVY DUTY COMPONENT HERE</>
});

Even much easier in Next.Js:

import dynamic from 'next/dynamic';
const DynamicProfile = dynamic(() => import('components/Profile'));
...
return (
    <>
        <DynamicProfile />
    </>

5. Pay Attention to Above & Below the Fold

Ok, this last trick might not be a simple one. This may involve changing your website design and may apply more to mobile users compared to desktop users. But since people use smartphones most of the time, optimizing for mobile is more paramount than desktop.

Let's think of it this way. In mobile, you have minimal space, although screen size depends on the device type. And to make use of that precious space, your browser does not load everything all at once. It loads your website part-by-part.

The part of your website shown on your screen right after page load is called 'Above the fold'. Everything else is not shown immediately, such as the footer or the main content below a beautiful hero image, is called 'Below the fold'. To boost the performance, all you have to do is optimize those visible parts. Simple. Anything else you can load later, it's not important to show in the initial.

In my photography website, I purposely made a huge hero section covering the whole screen. Aside from a marketing point of view, I did this to improve the performance. All Google sees for the first time on my home page is that hero, and thus I need to maximize the optimization of that hero.

This gets me a score of 90 for mobile in Page Speed Insight!

04.jpg

Closing

That's it. I hope these tricks are useful.

One trick that I initially wanted to include in the article is optimizing images. But then again, image optimization is a huge and complex topic and not really a 'trick'. Plus, there are already so many great articles out there that delve deep into the matter.

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

単一ファイルコンポーネントの各オプションについて

はじめに

単一ファイルコンポーネントの scriptタグ 内で用いられるオプションについてまとめました。

data

data(){
  return{
     val:1,
     dog:{
        name:"ぽち”
        feature:"プードル"
     }
  }
},

ここでdata関数内で定義したプロパティだけがリアクティブになる。(Vuexのstateもだが今回は割愛)

リアクティブとは?

vue.jsが変更を検知する状態のこと。

※dataに定義されたobjectに新しくプロパティを追加した場合、その新しいプロパティはリアクティブにならない。上記の例だとdogオブジェクトにweight:"20kg"を追加してもweightはリアクティブにならない。$setを使えば追加できる。
参考:https://jp.vuejs.org/v2/guide/reactivity.html#ad

変更を検知するとその度にbeforeupdate=>updatedの処理が行われる。

ここでライフサイクルフック

mounted前の↓のライフサイクルは1回のみ実行される。
beforeCreate
created
beforeMount
mounted

データの更新(リアクティブデータの変更の検知時)時はrerenderされるため、この前後で以下のフックが実行される。

beforeUpdate

データの更新があった時に、rerenderされる前に実行される。
更新前の既存のDOMに対してアクセスすることができる。
また、beforeUpdateはSSRの場合は使えないので注意が必要。

updated

データの更新があった時に、rerenderされた後に実行される。
無限ループ防止のため、状態変化を行うような処理は書いてはいけない。

子コンポーネントを全て再描画するためにはmounted同様$nextTickを使う。
また、updatedはSSRの場合は使えないので注意が必要。

参考:https://jp.vuejs.org/v2/guide/computed.html#ウォッチャ

computed

基本的には値を返すもの。基本的には変数を取らない。returnが必須。処理の中にdataを含めるとリアクティブにもなる。非同期処理ができない。

computed:{
  test2(){
    return Math.random()*this.val;
  },
}

一方で例外的に変数をとる場合がある。
その場合、以下のようにgetter関数とsetter関数に分けてかける。
getter関数は引数を取らない。値を返すもの。本来のcomputedの役割の処理をかく。
setter関数は引数を取る。値を返すのではなく値を更新する。後述するmethodsの役割に近い。例では仮引数のnewValueの値を更新している。

computed: {
  fullName: {
    // getter 関数
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter 関数
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

参考:https://jp.vuejs.org/v2/guide/computed.html#算出-Setter-関数

setter関数の処理はmethodsで書けばいいじゃないか!と思っている方へ。
なぜmethodsの役割に近いsetter関数が必要なのかはcomputedとmethodsの違いのところでかく。

methods

基本的には値を更新するもの。変数を取れる。returnはどちらでもいい。

methods: {
  countUp(size) {
    this.val = this.val + size;
  },
}

computedとmethodsの違い

1.役割の違い
前述したようにcomputedは値を返すことが基本的な役割(変数を取らず、returnする)
一方でmethodsは値を更新することが基本的な役割(変数を取れる。returnもできる)
2.キャッシュされるかされないか
なぜsetter関数が必要なのかというところの答えになる。
computedでは処理の結果がキャッシュされるが、methodsではキャッシュされない。
どういうこと...?

<template>
  <div>
    <h1>methods</h1>
      <span>{{test()}}</span>
      <span>{{test()}}</span>
      <span>{{test()}}</span>
    <h1>computed</h1>
      <span>{{test2}}</span>
      <span>{{test2}}</span>
      <span>{{test2}}</span>
  </div>
</template>

<script>
  export default {
    data(){
      return{
        val:1,
      }
    },
  methods: {
    test(){
      return Math.random();
    },
  },
  computed:{
    test2(){
      return Math.random();
    },
  }
}
</script>

上記コードの結果が以下である。

methods
0.85313097662468860.097562584449483620.43239994914384416
computed
0.51703190122513630.51703190122513630.5170319012251363

computedは結果がキャッシュされるため3回とも同じ結果が表示される。(2回目3回目は処理を実行せず1回目の結果を流用している。
一方でmethodsは結果がキャッシュされないため3回とも処理が実行されている。そのため結果がことなる。

setter関数をなぜ使うのかの話に戻るが、変数を取りたい&キャッシュの仕組みを使ってエコな処理にしたいという時に使える機能。

またこのキャッシュ機能がvue.jsの強みである。通常のjsにはfunctionしかないので、毎回methodsを実行してるイメージ。

watch

watchは一部のプロパティの変更を検知できる。また第一引数に変更後の値、第二引数に変更前の値を取ることができる。
例えば、以下のような場合に使える。

data(){
  return{
    question: '',
  }
}
watch: {
    // この関数は question が変わるごとに実行されます。
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.debouncedGetAnswer()
    }
},

//questionは名前統一する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue】単一ファイルコンポーネントの各オプションについて

はじめに

単一ファイルコンポーネントの scriptタグ 内で用いられるオプションについてまとめました。

data

data(){
  return{
     val:1,
     dog:{
        name:"ぽち"
        feature:"プードル"
     }
  }
},

ここでdata関数内で定義したプロパティだけがリアクティブになる。(Vuexのstateもだが今回は割愛)

リアクティブとは?

vue.jsが変更を検知する状態のこと。

※dataに定義されたobjectに新しくプロパティを追加した場合、その新しいプロパティはリアクティブにならない。上記の例だとdogオブジェクトにweight:"20kg"を追加してもweightはリアクティブにならない。$setを使えば追加できる。
参考:https://jp.vuejs.org/v2/guide/reactivity.html#ad

変更を検知するとその度にbeforeupdate=>updatedの処理が行われる。

ここでライフサイクルフック

mounted前の↓のライフサイクルは1回のみ実行される。
beforeCreate
created
beforeMount
mounted

データの更新(リアクティブデータの変更の検知時)時はrerenderされるため、この前後で以下のフックが実行される。

beforeUpdate

データの更新があった時に、rerenderされる前に実行される。
更新前の既存のDOMに対してアクセスすることができる。
また、beforeUpdateはSSRの場合は使えないので注意が必要。

updated

データの更新があった時に、rerenderされた後に実行される。
無限ループ防止のため、状態変化を行うような処理は書いてはいけない。

子コンポーネントを全て再描画するためにはmounted同様$nextTickを使う。
また、updatedはSSRの場合は使えないので注意が必要。

参考:https://jp.vuejs.org/v2/guide/computed.html#ウォッチャ

computed

基本的には値を返すもの。基本的には変数を取らない。returnが必須。処理の中にdataを含めるとリアクティブにもなる。非同期処理ができない。

computed:{
  test2(){
    return Math.random()*this.val;
  },
}

一方で例外的に変数をとる場合がある。
その場合、以下のようにgetter関数とsetter関数に分けてかける。
getter関数は引数を取らない。値を返すもの。本来のcomputedの役割の処理をかく。
setter関数は引数を取る。値を返すのではなく値を更新する。後述するmethodsの役割に近い。例では仮引数のnewValueの値を更新している。

computed: {
  fullName: {
    // getter 関数
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter 関数
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

参考:https://jp.vuejs.org/v2/guide/computed.html#算出-Setter-関数

setter関数の処理はmethodsで書けばいいじゃないか!と思っている方へ。
なぜmethodsの役割に近いsetter関数が必要なのかはcomputedとmethodsの違いのところでかく。

methods

基本的には値を更新するもの。変数を取れる。returnはどちらでもいい。

methods: {
  countUp(size) {
    this.val = this.val + size;
  },
}

computedとmethodsの違い

1.役割の違い
前述したようにcomputedは値を返すことが基本的な役割(変数を取らず、returnする)
一方でmethodsは値を更新することが基本的な役割(変数を取れる。returnもできる)
2.キャッシュされるかされないか
なぜsetter関数が必要なのかというところの答えになる。
computedでは処理の結果がキャッシュされるが、methodsではキャッシュされない。
どういうこと...?

<template>
  <div>
    <h1>methods</h1>
      <span>{{test()}}</span>
      <span>{{test()}}</span>
      <span>{{test()}}</span>
    <h1>computed</h1>
      <span>{{test2}}</span>
      <span>{{test2}}</span>
      <span>{{test2}}</span>
  </div>
</template>

<script>
  export default {
    data(){
      return{
        val:1,
      }
    },
  methods: {
    test(){
      return Math.random();
    },
  },
  computed:{
    test2(){
      return Math.random();
    },
  }
}
</script>

上記コードの結果が以下である。

methods
0.85313097662468860.097562584449483620.43239994914384416
computed
0.51703190122513630.51703190122513630.5170319012251363

computedは結果がキャッシュされるため3回とも同じ結果が表示される。(2回目3回目は処理を実行せず1回目の結果を流用している。
一方でmethodsは結果がキャッシュされないため3回とも処理が実行されている。そのため結果がことなる。

setter関数をなぜ使うのかの話に戻るが、変数を取りたい&キャッシュの仕組みを使ってエコな処理にしたいという時に使える機能。

またこのキャッシュ機能がvue.jsの強みである。通常のjsにはfunctionしかないので、毎回methodsを実行してるイメージ。

watch

watchは一部のプロパティの変更を検知できる。また第一引数に変更後の値、第二引数に変更前の値を取ることができる。
例えば、以下のような場合に使える。

data(){
  return{
    question: '',
  }
}
watch: {
    // この関数は question が変わるごとに実行されます。
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.debouncedGetAnswer()
    }
},

//questionは名前統一する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CORSのエラーで困った

CORSのエラーで困った

Jqueryのajaxを使ってhttp通信しています。gasを使ってスプレッドシートに書き込みをしています。

単純リクエストで良いはずのリクエスト内容なのにoptionメソッドが呼ばれてしまっていた。

原因は
余計なことを書きすぎていた。

とにかく書いて良いのは、
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
この公式ドキュメントの「単純リクエスト」のところに書いてあるものだけ。

一つでも余計なものを書いてしまうとプリフライトリクエストが送られて、実行したいpostメソッドやgetメソッドのまえにOptionメソッドが呼ばれてしまいます。
下の写真で言うと、コメントアウトされているものは単純リクエストの際には書いてはいけないものです。

image.png

参考記事
https://qiita.com/im36-123/items/775aa7fd4ba3b171768c
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

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

CORSのエラーで困った。

CORSのエラーで困った

Jqueryのajaxを使ってhttp通信しています。gasを使ってスプレッドシートに書き込みをしています。
CORSエラーに関してはたくさんの記事があったので参考の一つになれば嬉しいです。
まだわかっていないとこも多いですがとりあえず解決できたところに注目して書いてみました。

原因は
単純リクエストで良いはずのリクエスト内容なのにoptionメソッドが呼ばれてしまっていた。

改善策
書きすぎていた余計なことを消す。

いろいろな記事を参考にしながら手探りでやってしまったせいで余計なものをたくさん書いてしまっていました。

http通信には2種類あるようです。「単純リクエスト」と「プリライトリクエスト」です。
二つの違いは公式サイトを見てください!

単純リクエストを実装したい場合にとにかく書いて良いのは、
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
この公式ドキュメントの「単純リクエスト」のところに書いてあるものだけ!!!

一つでも余計なものを書いてしまうとプリフライトリクエストが送られて、実行したいpostメソッドやgetメソッドのまえにOptionメソッドが呼ばれてしまいます。
下の写真で言うと、コメントアウトされているものは単純リクエストの際には書いてはいけないものです。

image.png

参考記事
https://qiita.com/im36-123/items/775aa7fd4ba3b171768c
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

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

tensorflowjs+GANで誰でもギャルになれるWebアプリを作ってみた

BeGyaruTranslate.png

1. はじめに

こんにちは、大学生のNdです。私は機械学習に興味があり、いろいろな人に遊んでもらえるツールが作れないものかと考えていました。そこで今回、だれでもギャルになれるBegyaruというサイトを作ってみました。本記事ではこのサイトの遊び方と使った技術の簡単な紹介をしていきます。

目次

1.はじめに
2.なにができるの?・どんな問題が解決できるの?
3.使い方
4.サービスの仕組み
5.どういった仕組みで変換しているのか
6.tensorflowjsを使う際のポイント
7.おわりに

2. なにができるの?・どんな問題が解決できるの?

みなさんはガングロギャルという単語をご存知でしょうか。ガングロギャルとは90年代から2000年代に流行った、真っ黒に日焼けして髪の色を金髪やピンクにするギャルたちのことです。
ganguro_gal.png
このガングロメイクですが、メイクに非常に手間がかかったり、紫外線で焼く場合は肌に大きな負担がかかる問題があります。そこで、今回作ったサイトでは、肌を焼いたりせずとも顔の画像をアップロードするだけでブラウザ上で顔検知および変換を行い、ガングロギャルになった姿を見ることができます。
また、今回作ったサイトでは個人情報の流出を防ぐことができます。

kojinjouhou_pc_lock.png
これはTensorflowjsというライブラリを使用してユーザのPCおよびスマホ上で画像の変換を行っているからです。言い換えると、アップロードした画像は外部に送信されないので、安心して自分の画像を使い遊ぶことができます。さらに、開発者にとっても、個人情報の流出という問題と推論を行うのに必要なサーバがいらなくなるという大きなメリットがあります。

3. 使い方

Begyaruにアクセスすると以下のページが開かれると思うので、"ファイルを選択"のボタンをおして、正面を向いた一人だけ映っている画像をアップロードしてください。変換が始まり5~20秒程度で変換できます。
BegyaruFileButton.png

※Chrome, Firefox, Edgeでは動作確認しています。
※Windows, Android, Macでは動作確認できていますが、iPhoneだと動かない場合があります。(iPhoneを持っていないのでデバッグができません...)

4. サービスの仕組み

サービスの仕組みについて以下の図を使いながら紹介します。
BegyaruArchitecture.png

まず図の左上の水色の範囲をご覧ください。これはサイトのWebサーバを表しています。サイトのWebサーバには主に
・機械学習モデル
・推論用のjavascriptコード
が配置されています。まず、1つ目の機械学習モデルはこのサイトの核となるギャル変換を実現するためのモデルです。こちらは自分でデータを集めて学習させたモデルで、顔写真を入力すると出力がギャルとなって出てきます。2つ目の推論用のjavascriptコードは、ユーザのブラウザ上で機械学習モデルをロードし、推論するために必要となるソースコードです。主に画像の加工を行ったり、tensorflowjsの関数を呼び出したりします。
続いて左下はCDNサービスを表しており、このネットワークからTensorflowjsのライブラリやCSSフレームワークのBootstrapが提供されます。
そして最後に、右側のユーザ側では端末上のブラウザが先ほどのWebサーバとCDNから必要なファイルをロードし、サービスを利用できる状態となります。

5. どういった仕組みで変換しているのか

変換には敵対的生成ネットワーク(GAN)という技術を使いました。以下の図を使って簡単な説明をします。
GAN_shikumi.jpg

まずGANでは二人の主要キャラクターがいます。一人は左側のイラストレーターである生成器、もう一人は右側の鑑定士である識別器です。まずイラストレーターは普通の顔写真をもらってガングロギャルの絵に書き換えます。そして書き換えられた偽物の写真は鑑定士に渡されます。鑑定士は偽物と本物のガングロギャル写真がランダムに与えられ見抜けるように学習します。最初、イラストレーターは超初心者であるため、ガングロギャルとは思えない下手な画像を描いてきます。したがって鑑定士側は容易に本物か見抜けるわけですが、しばらくしているとイラストレーターは経験を積み鑑定士を騙すような本物そっくりの画像を送ってきます。そこで鑑定士側も負けじと以前より細かい部分まで特徴をチェックするようになります。このようにイラストレーターと鑑定士がお互いに敵対しあって学習を進めることで、最終的にイラストレーターは人間ですら本物と見間違うような本物そっくりのガングロギャルを描いてくれるまでに成長します。これが敵対的生成ネットワーク(GAN)の仕組みです。(さらに詳しい技術については「CycleGAN」で調べてみてください。)

6. tensorflowjsを使う際のポイント

続いて、tensorflowjsを利用して気付いた点を紹介します。
・学習させたモデルをtensorflowjsで扱えるモデルに変換する。
学習済みのモデルはそのままではtensorflowjsで利用できないためtensorflowjs_converterを使って変換を行う必要があります。
kerasを利用した場合、モデルは以下のようにh5形式で保存します。

model.save('my_model.h5')

この際、モデル全体を保存するように気を付けてください。つまり

model.save_weights('my_model_weights.h5')

は使わないようにしてください。これはsave_weights()を使うとモデルの構造が失われてしまい、tensorflowjsモデルに変換できなくなるためです。
続いて、実際にtensorflowjsモデルに変換していきます。まずtensorflowjsのインストールをします。

pip install tensorflowjs
#成功すると"Successfully installed tensorflowjs-x.x.x"と表示される。

こちらが成功したらいよいよ変換です。以下のコマンドを実行してみましょう。

tensorflowjs_converter --input_format=keras --output_format=tfjs_layers_model
 h5_model_file_path tfjs_model_output_path

コマンドオプションについて、--input_formatはkerasを指定、--output_formatはtfjs_layers_modelを指定します。続いて、h5ファイルのパスと、出力先のパスを指定してあげましょう。なお、この設定のままコマンドを実行すると32bitFloat型でモデルが保存されるのですが、サイズを小さくしたい場合は--quantize_uint16--quantize_uint8オプションを利用してみましょう。多少推論精度が落ちてしまいますが、半分や1/4の大きさまで圧縮することができます。ほかにもいろいろなオプションがあるので、

tensorflowjs_converter --help

で適宜自分の使いたい機能を利用していきましょう。

・バックエンドの指定
tensorflowjsでは使用できるバックエンドがcpu,wasm,webglの中から選択できますが、極力webglを利用するようにしましょう。

tf.setBackend('webgl');

WebGLを利用することでGPUを利用することができ、cpuバックエンドと比較して10倍近く速い推論が実現できます。もしWebGL Backendが無理な場合はWebAssembly(wasm)を利用しましょう。

・Tensorflowjsでは自分でメモリ開放をする必要がある
pythonでtensorflowやnumpyを利用したとき、あまりメモリ開放について考えることはないでしょう。ですがtensorflowjsでは既に使った配列(テンソル)はメモリ開放してあげないといけません。
例えば以下のような場合

//まずモデルをロードする
const model = await tf.loadLayersModel(model_path);
//tensorを入力し推論結果をpredictionに代入
const prediction = await model.predict(tensor);

//なんらかの処理
//predictionは利用済み
tf.dispose(prediction); //predictionを破棄し、メモリ開放する。

最後の処理のようにtf.dispose()関数を利用してメモリ開放してあげてください。

7. おわりに

ブログを書くのは初めてだったのですが、作ったツールについて紹介することができたので良かったです。
ブログの内容に関する指摘や実際にツールを使ってみた感想等はTwitterや以下のコメント欄にお寄せください。

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

【TypeScriptハンズオン②】男もすなるTypeScriptといふものを、女もしてみむとてするなり ~自分だけの型を作ろう!~

この記はなに

TypeScriptを実際に触りながらTypeScriptを説く記の第2弾です。
TypeScriptが しょしんなる 方や、TypeScriptを いとをかし と思っているひとが対象です。

この記で、こころよしと思った方は、LGTM✨を何卒よしなに願い申し上げ候

過去と未来?
1. 【TypeScriptハンズオン①】男もすなるTypeScriptといふものを、女もしてみむとてするなり
2. 【TypeScriptハンズオン②】男もすなるTypeScriptといふものを、女もしてみむとてするなり ~自分だけの型を作ろう!~ ←この記
3. 【TypeScriptハンズオン③】 ~webアプリとして動かそう!~

@ TypeScriptをインストールしていないひとはこちら?
  【画像で説明】シンプルにTypeScriptを導入して使う方法
@ TypeScriptをいとをかしと思ったひとはこちら?
 JavaScriptを知っている方がTypeScriptをなんとなく理解するための記事

ハンズオン

✅ これ以降古語は出てきません。ご安心ください
✅ VSCodeを使います

型の作成(Interface)

TypeScriptは、変数や関数に型を明記することで、開発者がハッピー?になります。
今回は自作の型を作って?、ハッピー?になっていきます。

自分だけの型を作るには、interfaceを使います。
自分だけの型とはどういうものなのでしょうか?

例として関数の引数に、以下のようなオブジェクトを受け取りたいとします。

const kinotsurayuki = {
  bookName: "土佐日記",
  author: "紀貫之",
  year: 866,
}

このオブジェクトは、bookNameがstring型, authorがstring型, yearがnumber型であることがわかります。
これを明示的に型を付けたい場合は、interfaceを使ってBook型を作ります。

interface Book {
  bookName: string;
  author: string;
  year: number;
}

interfaceは以下のように、作られています。

interface 型名 {
  : <>;
  : <>;
  : <>;
}

このBook型は以下のように使います。型が一致しているので、TypeScriptのエラーはありません。

const kinotsurayuki: Book = {
  bookName: "土佐日記",
  author: "紀貫之",
  year: 866,
}

一方で、Book型の構造に違反すると、以下のように怒られます。
この場合は、authorではなく、headOfficeと記述してしまいました。

const kinokuniya: Book = {
  bookName: "紀伊國屋書店",
  headOffice: "新宿", // タイプ '{bookName: string; headOffice: string; year: number; } 'はタイプ' Book 'に割り当てることはできません。
  year: 1927
}

実際にVSCodeで見ると、怒られていることがわかります。

image.png

このBook型は引数にも利用できます。

const bookDetails = (details: Book) => {
  console.log(`${details.bookName}の著者は${details.author}で、生まれは${details.year}です。`);
}

予め型を定義しておくことで、使い回しが出来ます。
そのため、頻繁に使う型については用意しておくといいかもしれませんね!

クラスの型の作成

クラスでも型定義を利用することが出来ます。

class Book {
  bookInfo: string;
  constructor(public bookName: string, public author: string) {
    this.bookInfo = `${bookName}の作者は${author}です。`;
  }
}

このように予め、型を宣言しておくことで、型の違うものが入った時に気づくことが出来ます。
問題ない例は以下です。

const introduce = new Book("土佐日記", "紀貫之");
console.log(introduce.bookInfo); // 土佐日記の作者は紀貫之です。

以下は、違反しているので、エラーが出てしまいます。

const bookPreview = new Book(897, "紀貫之"); // タイプ 'number'の引数は、タイプ 'string'のパラメーターに割り当てることができません。

VSCodeでも見てみましょう!エラーが出ていることがわかります。
image.png
このようにClassでも問題なく型が使えます!
Classで型安全にコードを書きたい方は是非ご利用ください!

おわりに

今回は、自作の型について紹介しました。
開発する上で自作の型を作ることは頻繁にあるので、作れるようにしておくと良いとも思います。

Next: 【TypeScriptハンズオン③】 ~webアプリとして動かそう!~

参考文献

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

slickでスライダーを作ってみた

slickを使ってスライダーを作ってみた。

slickの利用

slickはスライダーのプラグインで、公式サイトからダウンロードできます。
以下のファイルが必須

  • slick.css
  • slick-theme.css
  • slick.min.js
  • fontsフォルダ

今回はslickの練習としていろいろやってみた。

仕様

調べた中で、できそうなことはとりあえずやってみよう

HTML & CSS

HTML
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
        <title>SlideBanner</title>
        <link rel="stylesheet" href="slick.css"/>
        <link rel="stylesheet" href="slick-theme.css">
        <link rel="stylesheet" href="test.css">
        <script src="jquery-3.5.1.min.js"></script>
    </head>
    <body>
        <h2 class="slide_title">slickのスライドショー</h2>
        <ul class="slide_show">
            <li><img class="slide_img" src="#"></li>
            <li><img class="slide_img" src="#"></li>
            <li><img class="slide_img" src="#"></li>
            <li><img class="slide_img" src="#"></li>
            <li><img class="slide_img" src="#"></li>
        </ul>
        <ul class="thumb">
            <li><img class="thumb_img" src="#"></li>
            <li><img class="thumb_img" src="#"></li>
            <li><img class="thumb_img" src="#"></li>
            <li><img class="thumb_img" src="#"></li>
            <li><img class="thumb_img" src="#"></li>
        </ul>
        <p class="slide_text">4秒ごとに自動でスライドします。</p>
        <p class="slide_text">左右の矢印を押下でスライドさせることができます。</p>
        <script type="text/javascript" src="slick.min.js"></script>
        <script type="text/javascript" src="test.js" charset="UTF-8"></script>
    </body>
</html>
CSS
body {
    margin: 0px;
    padding: 0 75px;
    background-color: black;
}

h2, p {
    color: white;
}

.slide_show {
    width: 100%;
}

ul {
    padding: 0px;
}

li {
    list-style: none;
}

.slide_img {
    width: 100%;
    height: 45vw;
    object-fit: cover;
}

.thumb_img {
    height: 20vw;
}
.slick-prev::before, .slick-next::before {
    color: #ffff66;
}

.slick-dots li.slick-active button:before, .slick-dots li button:before {
    color: #ffff66;
}

@media screen and (max-width: 400px) {
    body {
        padding:0 30px;
    }
    h2 {
        font-size: 15px;
    }
    p {
        font-size: 10px;
    }
    .thumb img {
        width: 100%;
    }
}

スライダーの矢印の色とか、ドットの色とか変えてみた。
.slick-prev::before, .slick-next::before {color: #ffff66;}
.slick-dots li.slick-active button:before, .slick-dots li button:before {color: #ffff66;}

画像が大きいので、メディアクエリもやってみた(ブレークポイント1箇所だけど)
meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"
@media screen and (max-width: 400px)

JavaScript

JavaScript
$(".slide_show").slick({
    asNavFor: ".thumb",
    arrows: false,
    dots: false,
    centerMode: true,
    centerPadding: '5%',
    infinite: true,
});

$(".thumb").slick({
    autoplay: true,
    autoplaySpeed: 4000,
    asNavFor: ".slide_show",
    focusOnSelect: true,
    infinite: true,
    slidesToShow: 3,
    slidesToScroll: 1,
    dots: true,
})

やってみたこと

初めてのslickだったが、比較的簡単に設定できた。

  • スライダーの矢印、ドットの色を変える。(上述)
  • センターモードで表示
    • centerModeとcenterPaddingを設定
  • サムネイル付き
    • thumbクラスを作成し、asNavForでslide_showと紐付け
    • サムネイルにarrowを付けるため、slide_showのarrowを消す
    • focusOnSelectでサムネイルを操作できるようにする
    • slidesToShowを設定してサムネイルを作成
  • 自動再生
    • autoplayとautoplaySpeedを設定

完成

スクリーンショット 2021-03-06 0.43.47.png

参考記事

レスポンシブ対応するためのCSSの書き方
【jQuery】スライダープラグイン「slick」の使い方を詳しく解説
slickの使い方からカスタマイズまで【スライダープラグイン決定版】
スライドショー(jQueryスライダープラグイン slickを使ってみる)
高さ変更!slickスライダーで画像サイズがバラバラの時の2つの対処法!【jQuery】
使い勝手の良いslick.jsスライダー(レスポンシブ対応)

画像

フリー画像をお借りしました。
雪景色 (lloorraa Pixabay)
オーロラ (Noel_Bauza Pixabay)
朝焼け (DeltaWorks Pixabay)
スタートレイル (Free-Photos Pixabay)
ビーチ (Walkerssk Pixabay)

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

リアルタイムフィルター作ってみた

JavaScriptを使ってリアルタイムフィルターを作ってみました。

仕様

キーワードにマッチする画像のみ表示できるようにする。

HTML & CSS

HTML
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>リアルタイムフィルター</title>
        <link rel="stylesheet" href="main.css">
        <script src="jquery-3.5.1.min.js"></script>
    </head>
    <body>
        <h2>リアルタイムフィルター</h2>
        <p>入力した文字を含まない要素は非表示となります</p>
        <input type="text" class="input_text" placeholder="キーワードを入力してください">
        <div class="exhibition">
            <div class="filter_img">
                <div class="contain_img">
                    <img src="#" alt="airplain">
                </div>
                <p class="keywords">キーワード</p>
                <div class="keyword_list">
                    <p>飛行機</p>
                    <p>ひこうき</p>
                    <p>hikouki</p>
                </div>
            </div>
            <div class="filter_img">
                <div class="contain_img">
                    <img src="#" alt="dolphin">
                </div>
                <p class="keywords">キーワード</p>
                <div class="keyword_list">
                    <p>いるか</p>
                    <p>iruka</p>
                </div>
            </div>
            <div class="filter_img">
                <div class="contain_img">
                    <img src="#" alt="cherry-blossoms">
                </div>
                <p class="keywords">キーワード</p>
                <div class="keyword_list">
                    <p></p>
                    <p>さくら</p>
                    <p>sakura</p>
                </div>
            </div>
            <div class="filter_img">
                <div class="contain_img">
                    <img src="#" alt="train">
                </div>
                <p class="keywords">キーワード</p>
                <div class="keyword_list">
                    <p>蒸気機関車</p>
                    <p>じょうききかんしゃ</p>
                    <p>joukikikansya</p>
                </div>
            </div>
        </div>
        <script type="text/javascript" src="main.js" charset="UTF-8"></script>
    </body>
</html>
  • input typeをtextにして入力を受け付け、placeholderを設定してデフォルト入力を入れておく。
CSS
body {
    padding: 0 30px;
}

.input_text {
    width: 100%;
    font: 16px;
    height: 20px;
    border: 2px solid #0d91e9;
}

.input_text:focus {
    border: 2px solid magenta;
    outline: 0;
}
.exhibition {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    width: 100%;
}

.filter_img {
    border: 1px solid blue;
    width: 45%;
    height: auto;
    margin: 20px 0;
}

.contain_img {
    width: 100%;
    height: 25vw;
    text-align: center;
    margin-bottom: 20px;
}

img {
    width: 95%;
    height: 100%;
    margin-top: 15px;
}

.keywords {
    margin: 15px 0px -10px 15px;
}

.keyword_list {
    display: flex;
    margin-left: 15px;
    margin-top: - 15px;
}

.keyword_list p {
    margin-right: 1em;
}
  • :focusでinputに入力するとき枠線の色が変わるようにしてみました。

JavaScript

JavaScript
function Search(text){
    $(".keyword_list").each(function(index, element){
        let keyword = $(element).text();
        if(keyword.indexOf(text) === -1){
            $(element).parent().css("display", "none");
        }else{
            $(element).parent().css("display", "block");
        }
    });
}

$(".input_text").on("input", function(event){
    let text = $(".input_text").val();
    if(text === ""){
        $(".filter_img").css("display", "block");
    }else{
        Search(text);
    }
});
  • .on("input", function(event){})でキーワードが入力されたときに処理を行う
  • 入力された文字をval()で受け取る。
  • 入力がないときは、全ての画像を表示するようにする。
  • 入力がある時に検索を開始。
  • .eachで全てのkeyword_listクラスを処理。
  • 検索語がキーワードとマッチしない時(indexOF()が-1)、マッチしていない画像を非表示にする。
  • それ以外の時は、画像を表示。

完成

86FFE82C-B036-45D0-BC51-22193545C267.png

51028E7A-4921-410A-B29D-F8CCBDDA5B23.png

参考文献

jQueryで入力中のテキストをリアルタイムで取得する
jQueryのeachメソッド
【Javascript】親要素や祖先要素を取得する方法 parentNode/closest

画像

全てフリーです。
飛行機 (ThePixelman Pixabay)
イルカ (Claudia14 Pixabay)
桜 (Couleur Pixabay)
機関車 (Harald_Landsrath Pixabay)

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