20201119のJavaScriptに関する記事は17件です。

Discord招待リンクをランダムに生成するボット

はじめに

このボットはDiscordの招待リンクをランダムに生成できます。
生成したリンクは全部有効な招待であるわけではなく、大体無効な招待です。

このような感じです。
001.png

これを使えば、誰かのサーバーに入ることができるかもしれません。

*免責事項:これはただ教育のためのサンプルです。

コード

パラメータ 意味
l リンクの長さ
c リンクの字母の構成元素

l は生成するリンクの長さ、今Discordの招待リンクは8桁です(昔は6桁です)。
c はリンクの生成の条件、これを変えると、リンクの生成するモードは変わる。

例えば、

var c1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

var c2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345678901234567890123456789";

c1を使えば生成したリンクは数字がありません。
c2を使えば生成したリンクは数字が含む確率は高くなります。

ですから、もしDisocrdの招待リンクの生成するモードが見つけたら、生成したリンクが有効な招待である確率は高くなります。

//main.js

const Discord = require('discord.js');
const client = new Discord.Client();

client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`);
});

client.on('message', message => {
    const args = message.content.slice('').split(' ');
    if(message.content.startsWith('生成')){
        if(args[1] === undefined){
            message.channel.send("Pleace enter a munber.");
        }
        else{
            for(let i=0;i<args[1];i++){
                var l = 8;
                var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
                var cl = c.length;
                var r = "";
                for(var ii=0;ii<l;ii++){
                    r += c[Math.floor(Math.random()*cl)];
                }
                message.channel.send(`https://discord.gg/${r}`);
            }
            message.reply(`${args[1]} invite link generated.`);
        }
    }
});

client.login('BOT_TOKEN');

使い方

以下のコマンドを入力します。

生成 (数字;生成するリンクの数量)

002.png

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

連休中の新型コロナウイルス拡大防止のためのJavaScript

3連休は自宅にいたほうがいいね

明後日(執筆時から起算)から、3連休です。
普通なら楽しみにする日ですが、第3波も到来して危険な状況になりました。

私の職場は医療機関なので、特に指示されなくても不用意な外出は避けていますが、どこにも行けないのは結構しんどいものです。だけど……、医療機関の状況も大変なものはあり、できるだけ悪化する事態にならないよう行動したいものです。

で、、、プログラミングは家でもできますので、勉強する機会と捉えましょう。エンジニアには勉強大事ですしね…。私は個人的にはC#(.NET)の勉強をしていますが、エンジニアみんなに共通(?)のJavaScriptも勉強しているので、今回はJSにしています。

言語が違えば書き方が違うので、今回は日付とループ処理の話題です。JSの日付処理の仕方を私が知らなかったので、勉強のために、記します。初歩的な内容にはなりますが(JavaScriptの経験は浅めです)。

ちなみに、もともとも着想はこのLinkedinのページからです。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script type="text/javascript">

            /* 3連休を格納する配列 */
            const holidays = [new Date("2020/11/21"),
                              new Date("2020/11/22"),
                              new Date("2020/11/23")]; //勤労感謝の日

            for (today in holidays){
                stayHome();
                wearMasks();
                putSocialDistance();
                enjoyPrograming();
            }

            //外出はできるだけ自粛しましょう
            function stayHome()
            {
                console.log("Stay Home");
            }

            //公の場ではマスクを着用しましょう
            function wearMasks()
            {
                console.log("Wear Masks");
            }

            //ソーシャルディスタンスを保ちましょう
            function putSocialDistance()
            {
                console.log("Put social distance");
            }

            //家で楽しくプログラミングを楽しもうぜ!
            function enjoyPrograming()
            {
                console.log("Enjoy programming!!");
            }
        </script>
    </body>
</html>

JavaScriptは外部ファイルにすることもできます。

簡単に解説を付します。

new Date(日付文字列)

例えば、new Date("2020/11/21")にすると、2020/11/21の日付型のデータができます。つまり、特定の日のデータを持つ日付型のデータが作れます。

ちなみに、配列にする場合は[値,値, ... ]と書きます。補足ですが、{ }で書くのは連想配列ですね。

for in 構文

配列の中身を1つずつ取りだし、dateという変数に入れ、配列の数の分ループします。今回のように特定の日付分だけループするなら、これが分かりやすいでしょうか。

function(関数)

関数(ある処理のまとまりを定義する)は、function 関数名() { ...処理... }として定義ます。
呼び出す時は、関数名();とします。

以上、エンジニアの皆様におかれましては、不用意な外出は避けてぜひご自宅で勉強に励みましょう。

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

連休中の新型コロナ拡大防止をJavaScriptで表現してみる

3連休は自宅にいたほうがいいね

明後日(執筆時から起算)から、3連休です。
普通なら楽しみですが、第3波も到来して危険な状況です。

特に私は医療機関勤務なので、敏感になってしまいますね…。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script type="text/javascript">

            /* 3連休を格納する配列 */
            const holidays = [new Date("2020/11/21"),
                              new Date("2020/11/22"),
                              new Date("2020/11/23")]; //勤労感謝の日

            for (today in holidays){
                stayHome();
                wearMasks();
                putSocialDistance();
                enjoyPrograming();
            }

            //外出はできるだけ自粛しましょう
            function stayHome()
            {
                console.log("Stay Home");
            }

            //公の場ではマスクを着用しましょう
            function wearMasks()
            {
                console.log("Wear Masks");
            }

            //ソーシャルディスタンスを保ちましょう
            function putSocialDistance()
            {
                console.log("Put social distance");
            }

            //家で楽しくプログラミングを楽しもうぜ!
            function enjoyPrograming()
            {
                console.log("Enjoy programming!!");
            }
        </script>
    </body>
</html>

JavaScriptは外部ファイルにすることもできます。

簡単に解説を付します。

new Date(日付文字列)

例えば、new Date("2020/11/21")にすると、2020/11/21の日付型のデータができます。つまり、特定の日のデータを持つ日付型のデータが作れます。

ちなみに、配列にする場合は[値,値, ... ]と書きます。補足ですが、{ }で書くのは連想配列ですね。

for in 構文

配列の中身を1つずつ取りだし、dateという変数に入れ、配列の数の分ループします。今回のように特定の日付分だけループするなら、これが分かりやすいでしょうか。

function(関数)

関数(ある処理のまとまりを定義する)は、function 関数名() { ...処理... }として定義ます。
呼び出す時は、関数名();とします。

以上、エンジニアの皆様におかれましては、不用意な外出は避けてぜひご自宅で勉強に励みましょう。

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

ハンバーガーメニューを開いても背景が透けない!?

ハンバーガーメニューを開いた際に、なぜか背景が透けなかったので原因を分析してみました。

元のコード

index.html
<span class="nav-menu">
        <i class="nav-menu-i"></i>
        <i class="nav-menu-i"></i>
        <i class="nav-menu-i"></i>
      </span>
      <nav class="nav">
        <ul class="header-nav">
          <li class="header-nav-list"><a href="#"></a></li>
          <li class="header-nav-list"><a href=""></a></li>
          <li class="header-nav-list"><a href=""></a></li>
          <li class="header-nav-list"><a href=""></a></li>
          <li class="header-nav-list"><a href=""></a></li>
          <li class="header-nav-list"><a href=""></a></li>
        </ul>
      </nav>
stylesheet.scss
.nav {
  position: relative;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity .5s, visibility .5s
}

.nav.show {
  opacity: 0.9;
  visibility: visible;
  background-color: white;
}

.nav-menu {
  display: block;
  position: relative;
  position: absolute;
  position: fixed;
  width: 1.75rem;
  height: 1.5rem;
  top: 10px;
  right: 10px;
  cursor: pointer;
  z-index: 100;
  .nav-menu-i {
  display: block;
  width: 100%;
  height: 2px;
  background-color: black;
  position: absolute;
  transition: transform .5s, opacity .5s;
  }
  .nav-menu-i:nth-child(1) {
  top: 0;
  }
.nav-menu-i:nth-child(2) {
  top: 0;
  bottom: 0;
  margin: auto;
  }
.nav-menu-i:nth-child(3) {
  bottom: 0;
  }
}

Sassの書き方が汚いのはご愛嬌で…

nav-menu-iはハンバーガーの線を表しています

navを開いても、背景が透けてくれず、困ってたのですが、、

stylesheet.scss
.nav {
  position: relative;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity .5s, visibility .5s;
  z-index: 99;
}

z-indexを指定してあげると解決しました!
positionを使っているので、重なり順を指定してあげると解決するみたいですね?

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

5歳娘「パパ、型ガードって何?」

とある休日

娘(5歳)「ねえパパ」

ワイ「なんや?娘ちゃん」

娘「あのね」
娘「TypeScriptを勉強してたら、型ガードっていう概念が出てきたんだけど」
娘「型ガードって何?」

ワイ「ああ、それはな」
ワイ「片面がクッキーで、もう片面がチョコのやつや」
ワイ「ほんで、チョコの面は船の絵みたいになってんねや」

娘「食べてみた〜い」

よめ太郎「それアルフォートやないかい」

ワイ「Oh...よめ太郎...」

よめ太郎「そうやなくて」
よめ太郎「型ガードの話をしてんねん」
よめ太郎「変われ、わしが説明する」

選手交代

よめ太郎「そんで娘ちゃん」
よめ太郎「具体的にどういうタイミングで型ガードが気になったん?」

娘「ええとね」
娘「実は今、遊園地のオンライン申し込みシステムを作っててね」
娘「その遊園地では、12歳以下の子供は料金が半額になるから」
娘「あるユーザーが12歳以下かどうかを判定する関数を書こうとしてたの」

関数名: isChild

  • ユーザーが子供料金の対象かどうかを判定する関数
  • ユーザーが12歳以下であればtrueを返す
  • ユーザーが13歳以上であればfalseを返す

娘「↑こんな関数なの」

よめ太郎「なるほどな」

娘「それでね」
娘「年齢を登録していないユーザーもいるから」
娘「ユーザーを表す型は↓こんな感じで書いたの」

type User = {
    name: string
    age: number | null
}

よめ太郎「なるほどな」
よめ太郎「ユーザーの年齢nullの場合もあるってことやな」

娘「うん」
娘「それで、さっきの子供判定関数を書こうとしてみたの」

const isChild = (user: User): boolean => {
    return user.age <= 12
    // -> true または false
}

よめ太郎「なるほどな」
よめ太郎「引数としてユーザーを受け取って」
よめ太郎「12歳以下かどうかを真偽値で返すんやな」

娘「うん」
娘「でも、ここでTypeScriptのエラーが出ちゃったの1

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

よめ太郎「なるほどな」
よめ太郎「nullかもしれん値を、数値と比較しようとしたからやな」

娘「調べてみたところ」
娘「こういうときには型ガードが必要みたいなんだけど・・・」

ワイ「ちゃうちゃう」
ワイ「こういうときに必要なのは、型アサーションや」

型アサーションしてみる

ワイ「user.age数値として扱うために」
ワイ「user.age as numberって書けばええんや」

娘「なるほどね!」

user.ageは、ここではnumber型やで!」

娘「↑こんな風に主張するんだね」
娘「アサーションって、主張とか断言って意味だもんね!」
娘「やってみるね!」

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

娘「あっ!ちゃんとエラーが消えた!」
娘「パパ、ありがとう!」

ワイ「ゲヘヘ」

よめ太郎「いや全然あかんで

ワイ「なんでや」

よめ太郎「試しに、今の状態のisChild関数を実行してみい」

ワイ「ぐぬぬ、やってみたるわ」

const ojisan: User = {
    name: 'やめ太郎',
    age: null
}

ワイ「↑まずは、こうやな」
ワイ「試しに1人ユーザーを作ってやるんや」
ワイ「年齢は登録していないパターンにしといたで」
ワイ「ほんで、次は・・・」

console.log(isChild(ojisan))

ワイ「↑こうや!」
ワイ「isChild関数で、ojisanが子供かどうかチェックしたるんや!」
ワイ「結果は・・・!」

true

ワイ「ファッ!?
ワイ「true!?」
ワイ「ojisan子供扱いになってもうた・・・」

よめ太郎「そうやで」
よめ太郎「null12を比較したら」
よめ太郎「nullの方が小さいことになってしまうねん」

ワイ「マジか」

よめ太郎「nullと数値の比較とか、せんほうがええで」
よめ太郎「だからわざわざコンパイラくんが・・・」

オブジェクトは 'null' である可能性があります。

よめ太郎「↑こんなエラーを出してくれてたんや」

ワイ「なるほどな・・・」

よめ太郎「nullの可能性もあるのに」
よめ太郎「無理やり型アサーションでコンパイラに言うことを聞かせて」
よめ太郎「数値扱いなんてしたらあかんねん」

ワイ「ぐぬぬ」

よめ太郎「そこで、型ガードや」

型ガードしてみる

よめ太郎「こうや!」

const isChild = (user: User): boolean => {
    if (user.age === null) return false // <- 追加

    return user.age <= 12 // <- 型アサーションはしない
}

よめ太郎「user.agenull、つまり年齢が未登録の場合は」
よめ太郎「子供料金を適用するわけにはいかんやろ?」

ワイ「せやな」

よめ太郎「せやから」
よめ太郎「関数の始めの部分で・・・」

    if (user.age === null) return false

よめ太郎「↑こうやって、年齢がnullの場合には早期リターンして」
よめ太郎「falseを返す、つまり子供じゃない扱いにしたるんや」

ワイ「ほうほう」

よめ太郎「こうすることで、その下の行のuser.ageは」
よめ太郎「nullである可能性が排除されるから・・・」

   return user.age <= 12

よめ太郎「↑この部分の型エラーは出なくなるんや」
よめ太郎「数値が来て欲しいところにnullが来てしまう可能性を排除できたんや」

ワイ「ほんまや」

オブジェクトは 'null' である可能性があります。

ワイ「↑このエラーが表示されんくなっとるな」
ワイ「型アサーションも消したのにな」
ワイ「user.ageがちゃんと数値として認識されとるみたいやな」

娘「あ、そっか!」
娘「年齢がnullだった場合には早期リターンされちゃうから」
娘「この行までたどり着けないはずだもんね」
娘「この行ではuser.ageは必ずnumber型だ、ってことが保証2されたから」
娘「エラーが出なくなったんだね!」

const isChild = (user: User): boolean => {
    // user.age が null なら、次の行で処理は終了する 
    if (user.age === null) return false

    // 従って、ここでは user.age は null になる可能性はなく
    // number となる
    return user.age <= 12
}

娘「↑こういうことだね!」

よめ太郎「そういうことや」
よめ太郎「TypeScriptのコンパイラくんは」

コンパイラ「user.agenullになる可能性は、関数の1行目で消えたな!」
コンパイラ「ほな、次の行ではuser.ageは確実に数値や!」

よめ太郎「ってことを理解してくれるんや」

ワイ「へえ〜、条件をつけて、先の処理に進める型を絞り込むわけか」
ワイ「でも、何でこれを型ガードって呼ぶんやろ・・・」

なぜに型「ガード」か

よめ太郎「guardって単語は」
よめ太郎「番人、見張り、みたいな意味やから」

型の番人「numberが来るべきところにnullが入り込まないよう、見張っておきます!」
型の番人「きちんとガードします!」

よめ太郎「↑こんなイメージや」

娘「そう言われてみると、まさに型ガードって感じだね」

ワイ「ほんまや」
ワイ「numberとかnullってのはあくまで一例やけど」

型の番人「この型は、通ってヨシ!」

ワイ「↑こんな感じやな」

まとめ

  • 来るべきでないところにnullundefinedが来る可能性が残っていると、TypeScriptのコンパイラはエラーを出して教えてくれる
  • そのとき、型アサーションで無理やりコンパイラに言うことを聞かせるのは、なるべく控えよう
  • 型ガードで型を絞り込んで、あるべき型の値だけが先の処理に進めるようにすれば、ちゃんとエラーは消せる

ワイ「ってことやで!娘ちゃん」

娘「うん、聞いてたから知ってる」

ワイ「それな」

〜おしまい〜

おすすめ文献


  1. TSConfigで、strictまたはstrictNullCheckstrueにしていないとエラーになりません。ぜひtrueにしておきましょう。 

  2. any型を使ってると保証されないから、TSConfigで禁止しましょう。。。 

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

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

この記事の概要

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

目標物

demo


開発環境

・macOS Catalina 10.15.7
・@vue/cli 4.5.6
・npm 6.9.0
・node v10.16.0

前提

・node、npm、vue-cli環境が整っている。
・firebaseのアカウントを作成している。


firebaseのプロジェクト作成。

firebaseに行き
【コンソールへ移動】
→【プロジェクトの作成】または【プロジェクトの追加】

プロジェクト名は「vue-test」としておきます。

スクリーンショット 2020-11-18 11.41.23.png


アナリティクスは無効でOKです。

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


プロジェクトができました。
スクリーンショット 2020-11-18 11.42.52.png


こんなページに来たらOKです。
スクリーンショット 2020-11-18 11.43.19.png

DBの作成

Cloud Firestoreを使用します。
スクリーンショット 2020-11-18 11.45.06.png


テストモードでOKです。
スクリーンショット 2020-11-18 11.45.39.png


ロケーションを「asia-norheast1(東京)」に設定。
スクリーンショット 2020-11-18 11.46.01.png


DBができました。
これでfirebaseの設定は終わりです!
スクリーンショット 2020-11-18 11.47.19.png


プロジェクト作成

ターミナル
$ vue create vue-test
$ cd vue-testcd
vue-test$

axiosをインストール

ターミナル
vue-test$ npm install axios

スクリーンショット 2020-11-18 12.07.30.png


アプリ立ち上げ。

ターミナル
vue-test$ npm run serve   

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

View作成

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

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


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


データを送る

まずaxiosをimportする必要があります。

App.vue
<script>
import axios from 'axios'//         <-
export default {
  deta() {
    return {
      name: '',
      comment: ''
    }
  },
</script>

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

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

今回オプションは取りません。

.thenには通信が成功したときの処理を指定できます。
今回はthis.namethis.commentを空にしています。

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


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

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

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


以下、適宜YOUR_PROJECT_IDを自分のプロジェクトIDに置き換える必要があることに注意してください。

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

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

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


データが入っています!

データの取得

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

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

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

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

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

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

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

データの表示

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

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

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

完成!

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


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

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

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

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

GASでプログラミング入門 Vol.7

GASでプログラミング入門 Vol.7

社内サークルにてエンジニアから非エンジニアの方向けにプログラミングを教えるという活動を行っています。

今回はその教材第7弾です。
前回の記事はこちら

前回の演習問題の解答例

(1). 下記のプログラムを実行すると事前に意図した表示とは異なって表示されるプログラムになってしまっていました。意図した表示になるようにプログラムを修正して下さい。
また修正する際にはなるべくグローバル変数を使用しないように配慮して修正して下さい。

function myFunction(){
    num = 1;
    myFunction2();
    console.log("myFunction内でのnumは" + num + "です。");
}
function myFunction2(){
    num = num + 1;
    console.log("myFunction2内でのnumは" + num + "です。");
}

事前に期待していた実行結果

myFunction2内でのnumは2です。
myFunction内でのnumは1です。

現在のコード実行結果

myFunction2内でのnumは2です。
myFunction内でのnumは2です。

解答例コード

function myFunction(){
    let num = 1;
    myFunction2(num);
    console.log("myFunction内でのnumは" + num + "です。");
}
function myFunction2(num){
    num = num + 1;
    console.log("myFunction2内でのnumは" + num + "です。");
}

(2). 下記のプログラムを実行すると事前に意図した表示とは異なって表示されるプログラムになってしまっていました。意図した表示になるようにプログラムを修正して下さい。
また、仮に変数の値を書き換えられてしまっていた場合に実行時に書き換えられた際にエラーが発生するようにして下さい。

function myFunction(){
    let name = "鈴木一郎";
    console.log(name + "の趣味はドライブです。");
    console.log(name + "の出身地は東京都です。");
    name = "山田太郎";
    console.log(name + "は現在東京の会社で働いています。");
}

事前に期待していた実行結果

鈴木一郎の趣味はドライブです。
鈴木一郎の出身地は東京都です。
鈴木一郎は現在東京の会社で働いています。

現在のコード実行結果

鈴木一郎の趣味はドライブです。
鈴木一郎の出身地は東京都です。
山田太郎は現在東京の会社で働いています。

解答例コード

function myFunction(){
    const name = "鈴木一郎";
    console.log(name + "の趣味はドライブです。");
    console.log(name + "の出身地は東京都です。");
    name = "山田太郎";
    console.log(name + "は現在東京の会社で働いています。");
}

なお解答例はあくまで例なので、必ずしも上記のようになっていないといけないわけではありません。

配列について

今回は配列について学習していきます。
配列とは一言で言うと、複数の値をひとまとめにしたデータ構造です。
今まで学習してきた変数を複数個連続して使用できるようにするものと思ってもらえると良いと思います。
まず下記のような変数を用意した場合のイメージ図を見てみましょう。

let num1 = 1;
let num2 = 2;
let num3 = 3;

0001.drawio.png

上図のように3つの変数を用意して、それぞれ1,2,3で初期化してあります。

次に配列を使用すると以下のようなコードになります。

配列変数を宣言して初期化する文法

配列変数名 = [配列の0番目のデータ, 配列の1番目のデータ, ...]
let nums = [1, 2, 3];

0002.drawio.png

変数を3つ用意しているのとイメージ図は似ていますが、重要なポイントは下記になります。

  • 3つの領域が連続している
  • 変数名としてはnumsという名称が付けられている

次に配列の中の値へのアクセス方法について見ていきます。

配列内のデータへアクセスする文法

配列変数名[アクセスする配列内のデータへの番号]
let nums = [1, 2, 3];
console.log(nums[0]);
console.log(nums[1]);
console.log(nums[2]);

上記のコードを実行すると

1
2
3

と表示されます。
ポイントとしては配列の何番目というように、連続した領域内の数値で指定するところです。
そして、先頭の数値は0ということもポイントです。
JavaScriptを含む多くのプログラミング言語では配列の先頭を0から数えるようになっています。
日常生活では1から数え始めるので、この辺りの感覚の違いみたいなものも、プログラミング入門におけるつまづきポイントかと思いますが、この辺りは慣れの部分も強いと思いますので、まずは配列の先頭は0番目ということに慣れていってもらえればと思います。

配列内のデータへ数値を指定してアクセスする方法について理解したところで、forを使用して配列内のデータを参照する方法を見ていきます。
と言っても、今までの学習内容の応用なので、新しいことは特にありません。

let nums = [1, 2, 3];
for(let i = 0; i < 3; i++){
    console.log(nums[i]);
}

上記コードを実行すると先ほどと同じように1,2,3が順番に表示されます。
変数を3つ別々に用意した場合と比べると、for文で繰り返し処理が出来ることも配列を使用するメリットの一つです。
また、先ほどのコードと比べると繰り返し処理を使用するようになった分だけ、少しコードがすっきりとしました。
しかし、上記の書き方では配列のサイズを変更した際にfor文の継続条件式を合わせて変更しなければいけません。
そこで、条件式には配列のサイズを取得するようにして、変更に強いコードにします。

let nums = [1, 2, 3, 4];
for(let i = 0; i < nums.length; i++){
    console.log(nums[i]);
}

上記のようにコードを変更すると、配列のサイズを変更しても、for文の部分は変更することなく実行することが可能になります。JavaScriptでは配列変数名.lengthと記述すると、その配列のサイズを参照することができるので、繰り返し処理を行う際によく使用されます。

更に、下記のように記述することも可能です。

let nums = [1, 2, 3, 4];
for(let num of nums){
    console.log(num);
}

上記は少し特殊なfor文でfor...of文と呼ばれるものです。
for...of文も配列のサイズを気にすることなく、配列のサイズ文だけ繰り返し処理を行ってくれます。

最後に、配列に格納できるのは変数同様に数値だけではなく、文字なども格納可能です。

let names = ["鈴木", "佐藤", "田中"];
for(let name of names){
    console.log(name);
}

上記コードを実行すると下記のようになります。

鈴木
佐藤
田中

演習問題

(1). 下記の配列を宣言して、実行結果の表示になるプログラムを作成して下さい。

let nums = [1, 2, 3, 5, 8, 13];

実行結果

numsの0番目のデータは「1」です。
numsの1番目のデータは「2」です。
numsの2番目のデータは「3」です。
numsの3番目のデータは「5」です。
numsの4番目のデータは「8」です。
numsの5番目のデータは「13」です。

(2). 下記の配列を宣言して、実行結果の表示になるプログラムを作成して下さい。
配列内のデータは可変しても平気なように考慮して、各種果物の個数を数えるようにして下さい。
ただし、配列内に出現する果物の種類は「りんご」、「みかん」、「ぶどう」の3種類のみで、それ以外の種類の果物に関しては個数を数える必要はありません。

let fruits = ["りんご", "みかん", "ぶどう", "みかん", "りんご", "りんご"];

実行結果

配列内に含まれる「りんご」の個数は3個です。
配列内に含まれる「みかん」の個数は2個です。
配列内に含まれる「ぶどう」の個数は1個です。

まとめ

いかがでしたでしょうか。
今回は配列について学びました。
配列はプログラミング学習において序盤で学ぶことが多いのですが、中々概念を理解できず、つまづくポイントの一つでないかなと思うので、しっかりと復習して、理解を深めておくと良いと思います!
それではまた次の記事でお会いしましょう。

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

【Visual Studio Code】コーディングに全集中できるショートカットキー

VScodeショートカットキー紹介

VScodeでコーディングに全集中できるショートカットキーを紹介します!!

Zenモード

command + K を押して、間を開けてからZを押すとコードの画面のみにできます!

通常の画面に戻すときも上記と同じです。

参考

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

AddEventListenerでイベントをキャンセルする機能を実装したい

やりたいこと

ページの入力フォームから情報を入力して送信する際、入力によってはアラートを出して情報を送信させないようにする機能をAddEventListenerを使って実装したい。
今回は下記のような入力フォームがあって、2つのセレクトボックスからそれぞれ入力する情報を選択する。
Screenshot from 2020-11-19 12-54-58.png
もし各セレクトボックスから同じ選択肢を選んで「合体」ボタン(submitボタン)を押した場合、
Screenshot from 2020-11-19 12-56-20.png
アラートが出て入力情報が送信されるのを防ぎたい。(違う入力であれば出さない)
Screenshot from 2020-11-19 13-01-16.png

実装

formのidはselect_form、各セレクトボックスのidはそれぞれfirst_personasecond_personaとなっている。
addEventListenerで送信ボタンが押された時に入力情報を判定する関数が動作するようにしている。

<script>
  function checkPersonaId(event){
    const first_persona_id = document.getElementById('first_persona').value;
    const second_persona_id = document.getElementById('second_persona').value;
    if(first_persona_id == second_persona_id){
      alert('同じペルソナは選べません');
      event.stopPropagation();
      event.preventDefault();
    }
  }
  const form = document.getElementById('select_form');
  form.addEventListener('submit', checkPersonaId);
</script>

ポイントはifブロック内のstopPropagation()preventDefault()AddEventListenerでイベントをキャンセルしたいときには、これら2つのメソッドをどちらも記述する必要がある。

return falseではイベントをキャンセルできない

イベントハンドラのonSubmitや、jQueryではreturn falseでイベントのキャンセルができるが、AddEventListenerを使っている場合にはreturn falseと記述してもイベントのキャンセルができない。

stopPropagationとpreventDefaultは何をしているのか

stopPropagationは親要素へのイベント伝搬をキャンセルし、preventDefaultはその要素のイベント自体をキャンセルしている。
要素のイベントをキャンセルすれば良いと思ってpreventDefaultだけ記述すると、イベント伝搬だけが行われて意図しない挙動となるので注意。
jQueryでのreturn falseはこのあたりのキャンセルをきちんと行ってくれるらしい。

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

ECMAScriptでは一つの日付文字列形式しか定義してない

ユーザーが記事を投稿して、それを表示するというシンプルなシステムを作っている時の話。
ある日、IEで記事投稿の時刻表示が出ないと連絡があった。
コードを調べてもIE特有で表示できない部分がぱっと見では分からず、調べてみるとnew Date()にちょっとした落とし穴があったのでまとめ。(どちらかというと落とし穴があったのではなく自分から引っ掛かったが正解かも...)

問題点

当初の記事の時刻表示をするコードが以下

var content = '';

var date = new Date(dateString); // APIで取得した日付の文字列をもとにDateインスタンス作成
if (date.toString() !== 'Invalid Date') {
  content += '<li>';
  // APIで受け取った日付を"YY.MM.DD HH:mm:ss"にする
  content += date.getFullYear().toString().slice(-2) + '.' + (date.getMonth() + 1) + '.' + date.getDate() + ' ' + ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2) + ':' + ('00' + date.getSeconds()).slice(-2);
  content += '</li>'; // このリストタグをappendする
  // コードの書き方が古風なのは許して
}

結論、何が悪かったかというと、日付文字列のフォーマット。
実はECMAScript公式は、ISO 8601 Extended FormatというYYYY-MM-DDTHH:mm:ss.sssZしか定義していない。
(※詳細はECMAScript公式を見てください。20.4.1.15 Date Time String Format

今回のAPIからもらってくる日付文字列はフォーマットはYYYY-MM-DD HH:mm
今回の文字列形式がGoogle chromeなどで正常に動くのはブラウザ依存の動き方で、全てで動くとは保証できない。
結果、IEでは"Invalid Date"になって描画されなかったんですね。
納得。

解決法

解決法は、正規表現を使って、もらったフォーマットを、ISO 8601 Extended Formatにすること。

// 今回受け取るフォーマットは'YYYY-MM-DD HH:mm'なので、それから年月日時刻を抜き出す正規表現
function convertToRightFormatString(dateString) {
  const regex = /([0-9]{2,4})-([0-1]?[0-9])-([0-3]?[0-9])(?: ([0-2][0-9]):([0-5][0-9]))/;
  return dateString.replace(regex, function(
    match, year, month, day, hour, minutes
  ){
    return year + '-' + month + '-' + day + 'T' + hour + ':' + minutes + ':00.000Z'
  });
}

var content = '';

var date = new Date(convertToRightFormatString(dateString)); // 文字列を変更
// ...以下略

これで公式に乗っ取ったフォーマットになるので、どのブラウザでもInvalid Dateにならないはず!

※ RegexでわかりやすいようにNamed capture groups使おうと思ったらIE非対応だった...。さすがIE。

参考

ECMAScript® 2020 Language Specification

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

2020年にNuxt.jsで実装してきたアニメーションをまとめてみた

hey / STORES advent calendar 2020 7日目を担当する @ume-kun1015 です。

2020年の振り返りとして、この記事ではNuxt.jsで実装してきたアニメーションをまとめようと思います。

概要

Nuxt.jsでの開発を行おうとすると、Vue.jsのコミュニティが活発だからか、自然と多くのUIライブラリやアニメーションライブラリを見ます。しかし、自分はそれらを使わず、ほとんどのケースで自分で実装していく派です。

理由としては、

  • 要件を満たすものを探し、実際にプロジェクトに入れてみて、要件を満たせるかの検証に時間がかかる。
  • ライブラリが提供しているUIとデザイナーから要求されるUIの調整が難しい。
  • 将来要求される機能追加や変更を叶えられるかがわからない。
  • tree-shakingが未対応であるライブラリの場合、使わない機能のJavaScriptまでimportし、結果プロジェクトのバンドルサイズが増えてしまう。
    • バージョン管理も長期的な運用コストにもなる。
  • Vue.jsの中にアニメーション実装のための<transition>タグや<transition-group>タグがある。

というのがあります。

2020年では、UI・UX向上のためWEBサイトのデザインリニューアルを担当していました。多くのアニメーション実装が必要でしたが、上の理由から、適宜要件に合うように自分で実装してきました。多くの学びがあったため、振り返りを兼ねて、それらの一部をまとめてみようと思います。Vue.jsの <transition> を使ったケースと使わなかったケースがあるので、その観点でグループ化しました。

開発環境

開発環境は以下のものになります。

  • Nuxt.js 2.14.7
    • 今回フロントの実装の記事になり、SSRモードで実装する必要はないので、ssrオプションはfalseにして、SPAアプリケーションとして開発しています。
    • componentsのオプションはtrueにして、 コンポーネントの自動importが効くようにしました。
  • TypeScript 3.9.7

紹介するアニメーションたち

Vue.jsのtransitionを使って実装したもの

  1. スライドメニュー
  2. ポップアップ
  3. アコーディオン

Vue.jsのtransitionを使わないで実装したもの

  1. モーダル
  2. ギャラリー

Vue.jsのtransitionを使って実装したもの

Vue.jsでアニメーションを実装すると言えば、上であげた <transition> タグを使うことがまず思いつくかと思います。自分も多く使ってきたので、実装してきたアニメーションの中で<transition> タグを使ったケースをあげたいと思います。

スライドメニュー

slide-menu.gif

まずはスライドメニューです。使い方としては、ヘッダーメニューをスライドで開閉を切り替えるようにし、横からスライドで表示するというのがあります。

実装内容もシンプルで、Vuejsの <transition> タグのドキュメンテーションの一番最初にあるサンプルを参考にして実装したものです。サンプルは開くときに右から左にスライドし、閉じるときには左から右にスライドします。

開くときは、<transition> タグの中身のdomがレンダリングされるのにフックして、

  • background: 初期値で opacity: 0 で透明から、transition: opacity 0.15s で少しずつ背景を黒に変化していきます。
  • menu: 初期値が translateX(10%) で右に少しずらした位置から、 transition: all 0.15s ease で少しずつ、レンダリングが終わったであろう位置までスライドしていきます。

逆に閉じるときは、<transition> タグの中身のDOMがなくなることにフックして、

  • background: 初期値で opacity: 0 を設定し、そのまま透明にします。そのままDOMが破棄される最中から破棄されたあとは同じ状態を維持するので、leave-activeクラスは何も書かなくても大丈夫です。
  • menu: 初期値を特に何も設定せず、domが破棄されている最中でtransition: all 0.15s cubic-bezier(1, 0.5, 0.8, 1) でスライドしていき、消える頃には右に少しずれた位置にopacity: 0で透明にされているため、ならめかにスライドメニューが消えるという挙動になっています。

スライドメニューの中のメニューと背景をクリックしたときに、スライドメニューを閉じる処理を忘れないようにしましょう。

components/molecules/ume-slide-menu.vue
<template>
  <div class="ume-slide-menu">
    <transition name="background">
      <div v-if="isOpened" class="background" @click="close">
        <div class="close-button">&times;</div>
      </div>
    </transition>

    <transition name="menu">
      <div v-if="isOpened" class="menu">
        <div class="menu-item-wrapper">
          <div class="menu-item" @click="close">
            <span>page1</span>
          </div>

          <div class="menu-item" @click="close">
            <span>page2</span>
          </div>

          <div class="menu-item" @click="close">
            <span>page3</span>
          </div>
        </div>
      </div>
    </transition>
  </div>
</template>

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

export default Vue.extend({
  model: {
    prop: 'isOpened',
    event: 'close',
  },

  props: {
    isOpened: {
      type: Boolean,
      required: true,
    },
  },

  methods: {
    close() {
      this.$emit('close')
    },
  },
})
</script>

<style lang="scss" scoped>
.ume-slide-menu {
  .background-enter,
  .background-leave-to {
    opacity: 0;
  }

  .background-enter-active {
    transition: opacity 0.15s;
  }

  .menu-enter,
  .menu-leave-to {
    transform: translateX(10%);
    opacity: 0;
  }

  .menu-enter-active {
    transition: all 0.15s ease;
  }

  .menu-leave-active {
    transition: all 0.15s cubic-bezier(1, 0.5, 0.8, 1);
  }

  .background {
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 98;
    top: 0;
    right: 0;
    overflow-x: hidden;
    background-color: rgba(0, 0, 0, 0.6);

    .close-button {
      position: absolute;
      top: 0;
      left: 25px;
      font-size: 36px;
      color: #fff;
    }
  }

  .menu {
    height: 100%;
    width: 64%;
    max-width: 320px;
    position: fixed;
    z-index: 99;
    top: 0;
    right: 0;
    background-color: #f3f3f3;
    overflow-x: hidden;

    .menu-item-wrapper {
      background-color: #fff;
      padding-top: 40px;
      padding-bottom: 52px;
    }

    .menu-item {
      display: block;
      cursor: pointer;
      margin: 0 20px;
      padding: 17px 0;
      font-size: 16px;
      font-weight: bold;
      border-bottom: thin solid #c7c7cc;
      color: #4a4a4a;
      text-decoration: none;
      line-height: 1;

      span {
        vertical-align: middle;
      }
    }
  }
}
</style>

ここでの注意点では、ume-slide-menu の呼び出しをv-ifなどで制御せずに、呼び出しているコンポーネントをマウントしているときには、スライドメニューのコンポーネントもレンダリングされている必要があります。

pages/index.vue
<template>
  <div class="index-page">
    <ume-slide-menu v-model="showSlideMenu" />
  </div>
</template>

ポップアップ

popup.gif

次は、ポップアップです。ページが表示されたあとにキャンペーンの告知として表示したり、条件によってボタンをクリックをしたにフックして表示します。スライドメニューと同じで、背景をクリックしたときにも、ポップアップを閉じる処理を入れるのを忘れないようにしましょう。

実装も上のスライドメニューとほぼ同じです。Vue.jsの <transition>タグを使って、DOMがレンダリングされるときと破棄されるときまでに、どのような挙動になって欲しいかを、xxx-enter-activexxx-leave-active のcssクラスに記載するだけで、なめらかにポップアップを表示することができます。

components/molecules/ume-popup.vue
<template>
  <div class="ume-popup">
    <transition name="background">
      <div v-if="showPopup" class="background" @click.prevent="$emit('change-popup', false)" />
    </transition>

    <transition name="popup">
      <div v-if="showPopup" class="popup-wrapper">
        <ume-close class="icon icon-close" @click.prevent="$emit('change-popup', false)" />

        <div class="image-wrapper">
          <img src="https://picsum.photos/seed/picsum/400/600" />
        </div>
      </div>
    </transition>
  </div>
</template>

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

import UmeClose from '~/assets/fonts/close.svg?inline'

export default Vue.extend({
  components: {
    UmeClose,
  },

  model: {
    prop: 'showPopup',
    event: 'change-popup',
  },

  props: {
    showPopup: {
      type: Boolean,
      required: true,
    },
  },
})
</script>

<style lang="scss" scoped>
.ume-popup {
  .background-enter-active {
    transition: opacity 0.15s;
  }

  .background-enter,
  .background-leave-to {
    opacity: 0;
  }

  .popup-enter-active {
    transition: all 0.25s ease;
  }

  .popup-leave-active {
    transition: all 0.25s cubic-bezier(1, 0.5, 0.8, 1);
  }

  .popup-enter,
  .popup-leave-to {
    opacity: 0;
  }

  .background {
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 1;
    top: 0;
    right: 0;
    overflow-x: hidden;
    background-color: rgba(35, 24, 21, 0.35);
  }

  .popup-wrapper {
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    position: fixed;
    padding: 0.5em 1em;
    z-index: 2;
    display: flex;
    flex-direction: column;

    .icon-close {
      height: 36px;
      fill: black;
      margin: 0 0 10px auto;
    }

    .image-wrapper {
      display: block;
      height: 350px;
      width: 330px;

      img {
        height: 100%;
        width: 100%;
        border-radius: 10px;
      }
    }
  }
}
</style>

ここでも、上のume-slide-menuと同じく、呼び出し側のほうでv-ifで制御せずに、事前にポップアップのコンポーネントもレンダリングされている必要があります。

pages/index.vue
<template>
  <div class="index-page">
    <ume-popup v-model="showPopup" />
  </div>
</template>

アコーディオン

accordion.gif

狭い限定された枠の中で、メニューをクリックしてコンテンツを開閉したいというケースがあり、その対応でアコーディオンを実装しました。これもOSSのライブラリで要求されるデザインや仕様を満たせるかが不安だったので、自分で実装しました。今回もVue.jsの<transition>タグを使っていますが、上2つとは少し違います。

どのように違うかというと、今まではトランジション状態をcssで任せていましたが、今回はJavaScriptのほうで制御しています。

コンテンツが開かれたあとのアコーディオンの高さがコンテンツの量に依存し、cssのクラスで言うところのxxx-enter-toで指定する高さが動的になることから、cssで静的に決め打ちすることができません。今回はサンプルなので、コンテンツは静的に決まっていますが、実際はAPIのレスポンスに依存するので、高さが動的になっています。

動的な高さをどう与えるかというと、トランジションが終わるとき、つまりxxx-enter-to のときに、そのコンテンツのラッパーの scrollHeightを渡すようにすれば解決できます。 Vue.jsの<transition>タグには、JavaScriptフックがあるので、ここでは@enter のフックで、アコーディオンのコンテンツのラッパーの高さを scrollHeight と同じにすれば、動的に高さを決めることができます。

ここで注意すべきなのは、アコーディオンを開く前に、一度アコーディオンの高さを0にしないとアニメーションが動きません。0と決め打ちしないとheight: auto が割り振られてしまい、height: auto から height: ${height}px へのケースでは、transition が効かなくなってしまいます。逆もしかりで、コンテンツを閉じるときのheight: ${height}px から height: auto へのケースでも、transition が効かなくなってしまいます。なので、コンテンツが開かれる前と閉じたあとのheight0にしましょう。この 0にするというのも、JavaScriptのフックで実現可能です。(下の例で言うと、@before-enter@leaveになります。)

components/atoms/ume-accordion.vue
<template>
  <div class="ume-accordion">
    <div class="header" @click="$emit('expand')">
      <slot name="header" />

      <down-arrow v-if="expandable" class="icon" :class="{ rotate: expanded }" />
    </div>

    <transition name="accordion" @before-enter="beforeEnter" @enter="enter" @before-leave="beforeLeave" @leave="leave">
      <div v-if="expanded" ref="content" class="content">
        <slot name="content" />
      </div>
    </transition>
  </div>
</template>

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

import DownArrow from '~/assets/fonts/down-arrow.svg?inline'

export default Vue.extend({
  components: {
    DownArrow,
  },

  model: {
    prop: 'expanded',
    event: 'expand',
  },

  props: {
    expanded: {
      type: Boolean,
      required: true,
    },

    expandable: {
      type: Boolean,
      required: true,
    },
  },

  mounted() {
    if (this.$refs.content) {
      (this.$refs.content as HTMLElement).style.height = `${this.$refs.content.clientHeight}px`
    }
  },

  methods: {
    beforeEnter(el: HTMLElement) {
      el.style.height = '0'
    },

    enter(el: HTMLElement) {
      el.style.height = el.scrollHeight + 'px'
    },

    beforeLeave(el: HTMLElement) {
      el.style.height = el.scrollHeight + 'px'
    },

    leave(el: HTMLElement) {
      el.style.height = '0'
    },
  },
})
</script>

<style lang="scss" scoped>
.ume-accordion {
  border-radius: 6px;
  padding-top: 16px;

  .header {
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: space-between;
    line-height: 1;
    padding-bottom: 16px;
    border-bottom: solid 1px #d1d1d6;

    .icon {
      display: block;
      fill: #c7c7cc;
      height: 14px;
      width: 14px;
      transform: rotate(0deg);
      transition-duration: 0.3s;
    }

    .rotate {
      transform: rotate(180deg);
      transition-duration: 0.3s;
    }
  }

  .content {
    padding: 0 12px;
    overflow: hidden;
    transition: 0.2s ease-out;
  }
}
</style>

呼び出し側はこのように書いています。

pages/accordion.vue
<template>
  <ume-accordion
    v-for="(group, index) in groups"
    :key="group.id"
    :expanded="accordionExpanded[index]"
    :expandable="group.children.length > 0"
    class="accordion"
    @click-header-arrow="toggleExpandAccordion($event, index)"
  >
    <template v-slot:header>
      <p>{{ group.name }}</p>
    </template>

    <template v-slot:content>
      <ul v-if="group.children.length > 0">
        <li v-for="groupChild in group.children" :key="`${group.id}-${groupChild.id}`">
          <div class="child-img">
            <img :src="groupChild.src" />
          </div>
        </li>
      </ul>
    </template>
  </ume-accordion>
</template>

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

import { ImageGroup } from '~/types/image'

type Data = {
  groups: ImageGroup[]
  accordionExpanded: boolean[]
}

export default Vue.extend({
  data(): Data {
    const groups = [
      {
        id: 1,
        name: 'scenes',
        children: [
          { id: 1015, src: 'https://picsum.photos/id/1015/200/300' },
          { id: 1016, src: 'https://picsum.photos/id/1016/200/300' },
          { id: 1018, src: 'https://picsum.photos/id/1018/200/300' },
          { id: 1019, src: 'https://picsum.photos/id/1019/200/300' },
          { id: 102, src: 'https://picsum.photos/id/102/200/300' },
        ],
      },
      {
        id: 2,
        name: 'scenes',
        children: [
          { id: 244, src: 'https://picsum.photos/id/244/200/300' },
          { id: 237, src: 'https://picsum.photos/id/237/200/300' },
          { id: 200, src: 'https://picsum.photos/id/200/200/300' },
          { id: 219, src: 'https://picsum.photos/id/219/200/300' },
          { id: 169, src: 'https://picsum.photos/id/169/200/300' },
        ],
      },
    ]

    return {
      groups,
      accordionExpanded: [true, ...groups.slice(1, groups.length).map(() => false)],
    }
  },

  computed: {
    accordionClass() {
      return (opened: boolean) => {
        return opened ? 'opened' : ''
      }
    },
  },

  methods: {
    toggleExpandAccordion(expanded: boolean, ingredientCategoryIndex: number) {
      this.accordionExpanded = this.accordionExpanded.map((_) => false)
      this.accordionExpanded[ingredientCategoryIndex] = expanded
    },
  },
})
</script>

Vue.jsのtransitionを使わないで実装したもの

上では、<transition>タグを使ったケースを紹介しましたが、逆に使わなかったものもあります。
使わなかった理由としては、個人的にVue.jsの<transition>タグはDOMの表示/非表示の切り替え時のアニメーションには有効ですが、要素の位置や高さを変更させるというアニメーションはあまり向いていないのかなと思っています。

下の2つのケースだと、要素の表示と非表示はせず、単純に要素の高さを変えるだけだったり、X軸の位置の変更だけで、要件を満たせることができました。どのように実装したかをまとめたいと思います。

モーダル

expandable-modal.gif

UI・UXリニューアルに伴い、類似サービスとの差別化を目指して、下から長さを伸ばすことができ、かつz-indexが効かして画面から浮かび上がっているモーダルを用意しようとなりました。上はサンプルですが、実際にはサービス上にある大量のコンテンツから欲しいものだけを絞り込みできるボタンが複数並べられており、それらをクリックすることで、欲しいものを絞り込みできるものとなっています。

アニメーションが入っている部分としては、モーダルの高さ変更のときのtransitionです。モーダルの上の帯の部分をクリックすることで、指定した高さまでモーダルの高さを広げることができます。逆に、広がったモーダルをデフォルトの高さまで縮めることができます。

今回では、要素の高さを調節するだけで要件を満たせるので、上記で書いた通り要素の表示/非表示をするわけではないため、Vue.jsの <transition>タグを使いませんでした。

components/molecules/ume-expandable-bottom-modal.vue
<template>
  <div ref="modal" class="ume-expandable-bottom-modal">
    <div class="modal-content-title" @click="toggleExpand">
      <p>タイトル</p>

      <up-arrow v-if="!expanded" class="icon icon-arrow-up" />
      <down-arrow v-else class="icon icon-arrow-down" />
    </div>

    <div class="content">      
    </div>
  </div>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue'

import { ImageGroup } from '~/types/image'

import UpArrow from '~/assets/fonts/up-arrow.svg?inline'
import DownArrow from '~/assets/fonts/down-arrow.svg?inline'

type Data = {
  defaultHeight: number
  transitionSeconds: number
}

export default Vue.extend({
  components: {
    UpArrow,
    DownArrow,
  },

  model: {
    prop: 'expanded',
    event: 'toggleExpand',
  },

  props: {
    expanded: {
      type: Boolean,
      required: true,
    },

    groups: {
      type: Array as PropType<ImageGroup[]>,
      required: true,
    },
  },

  data(): Data {
    return {
      defaultHeight: 48,
      transitionSeconds: 0.5,
    }
  },

  watch: {
    expanded(newValue) {
      if (newValue) {
        return
      }

      (this.$refs.modal as HTMLElement).style.height = `${this.defaultHeight}px`
    },

    transitionSeconds(newValue) {
      (this.$refs.modal as HTMLElement).style.transition = `${newValue}s ease-out`
    },
  },

  mounted() {
    (this.$refs.modal as HTMLElement).style.transition = `${this.transitionSeconds}s ease-out`
  },

  methods: {
    expandUpTo(height: number) {
      (this.$refs.modal as HTMLElement).style.height = `${height}px`
    },

    toggleExpand() {
      this.$emit('toggleExpand', !this.expanded)
    },
  },
})
</script>

<style lang="scss" scoped>
.ume-expandable-bottom-modal {
  width: 100vw;
  height: 48px;
  position: fixed;
  top: auto;
  right: 0;
  left: 0;
  bottom: 0;
  background: white;
  cursor: pointer;
  box-shadow: 0 -9px 10px 0 rgba(0, 0, 0, 0.1);
  border-radius: 8px 8px 0 0;
  z-index: 97;

  .modal-content-title {
    position: relative;
    height: 48px;
    background: 'red';
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 8px 8px 0 0;

    .category-title {
      margin: 6px auto;
      width: 80%;
      text-align: center;
      font-size: 14px;
      font-weight: bold;
      color: #000;
      line-height: 1;
    }

    .icon {
      display: block;
      text-align: left;
      fill: #000;
      position: absolute;
      right: 20px;
      height: 14px;
    }
  }
}
</style>

ギャラリー

gallery.gif

もう1つVue.jsの<transition>タグを使わずに実装したアニメーションとして、写真を複数枚並べて、それをスライドさせるギャラリーがあります。使われるシーンとしては、トップページの上部で複数のバナー画像の表示だったり、ランキング表示や1つの商品を紹介する写真を複数する表示するときなどに利用されることが多いかなと思います。

上で書いたように、これも <transition>タグを使わずに実装しています。矢印クリックで、要素のリストのラッパーのtranslateYを要素の長さ分、加算したり減算することで、リストの位置をずらす仕組みになっています。リストをラップしているDOMは、指定した長さで固定されているので、実際に目に見えるリストのコンテンツを表示するという考えです。スライドの速度はリストのcssにtransitionを書くことで調整できます。

component/molecules/ume-gallery.vue
<template>
  <div class="ume-gallery">
    <div @click="previous">
      <left-arrow :class="`icon icon-arrow-left ${previousclickableClass}`" />
    </div>

    <div ref="slide-list-wrapper" class="slide-list-wrapper">
      <ol ref="slide-list" class="slide-list">
        <li v-for="(slideListElement, index) in slideListElements" :key="index">
          <slot name="slide-list-element" :slide-list-element="slideListElement" />
        </li>
      </ol>
    </div>

    <div @click="following">
      <right-arrow :class="`icon icon-arrow-right ${followingClickableClass}`" />
    </div>
  </div>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue'

import { Image } from '~/types/image'

import RightArrow from '~/assets/fonts/right-arrow.svg?inline'
import LeftArrow from '~/assets/fonts/left-arrow.svg?inline'

const firstDisplayNum = 4

type Data = {
  transformX: number
  offset: number
  largestDisplayedNum: number
}

export default Vue.extend({
  components: {
    RightArrow,
    LeftArrow,
  },

  props: {
    slideListElements: {
      type: Array as PropType<Image[]>,
      required: true,
    },

    width: {
      type: Number,
      required: true,
    },
  },

  data(): Data {
    return {
      transformX: 0,
      offset: 24,
      largestDisplayedNum: firstDisplayNum,
    }
  },

  computed: {
    previousclickableClass(): string {
      return this.canSlidePrevious ? 'clickable' : 'non-clickable'
    },

    followingClickableClass(): string {
      return this.canSlideFollowing ? 'clickable' : 'non-clickable'
    },

    canSlidePrevious(): boolean {
      return this.largestDisplayedNum > firstDisplayNum
    },

    canSlideFollowing(): boolean {
      return this.largestDisplayedNum < this.slideListElements.length
    },

    slideElementStyle(): { 'min-width': string } {
      return { 'min-width': `${this.width}px` }
    },
  },

  mounted() {
    (this.$refs['slide-list-wrapper'] as HTMLElement).style.width = `${(this.width + this.offset) * firstDisplayNum}px`
  },

  methods: {
    previous(): void {
      if (!this.canSlidePrevious) {
        return
      }

      this.transformX += this.width + this.offset
      (this.$refs.slide as HTMLElement).style.transform = `translate(${this.transformX}px, 0)`
      this.largestDisplayedNum--
    },

    following(): void {
      if (!this.canSlideFollowing) {
        return
      }

      this.transformX -= this.width + this.offset
      (this.$refs.slide as HTMLElement).style.transform = `translate(${this.transformX}px, 0)`
      this.largestDisplayedNum++
    },
  },
})
</script>

<style lang="scss" scoped>
.ume-gallery {
  display: flex;
  justify-content: center;
  align-items: center;

  @media only screen and (max-width: 940px) {
    margin: 0;
  }

  .icon {
    height: 24px;
    width: 28px;
  }

  .clickable {
    fill: #c7c7cc;
  }

  .non-clickable {
    fill: #d1d1d6;
  }

  .slide-list-wrapper {
    overflow: hidden;
    margin: 0 auto;

    .slide-list {
      transition: 0.5s;
      display: flex;
    }
  }
}
</style>

まとめ

ざっと書いてきましたが、2020年にNuxt.jsで実装してきたアニメーションをVue.jsの<transition>タグを使ったケースと使わなかったケースでまとめてみました。元々はOSSのライブラリを使うときのコストを下げたい、またデザインや仕様変更に柔軟に対応できるように、自分で実装してきましたが、自分で手を動かして実装した分、多くのことを学んだと思っています。

振り返りをして思ったのは、アニメーションの実装って楽しいと再認識したことです。プログラミングに挑戦したいときっかけにもなった「自分が作ったものが動く」という感動を思い出し、初心に返ることができました。
少しはできることが増えたのかなと思いつつも、まだまだリッチなアニメーションだとスラスラ実装できないレベルです。来年は自分のフロント力をもっと伸ばしていける年になるといいなと思っています。

最後まで読んでいただき、ありがとうございました。
(上で書いたコードはGithubのレポジトリにまとめました。もし参考になったなどあれば、starしてくれると嬉しいです。)

明日はSTORES 予約の@yksihimotoさんによる、「next.js + Fullcalendar v5を攻略する」です!お楽しみに!

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

Javascriptで現在時刻をyyyy/mm/ddの文字列で取得する方法

Javascriptで現在時刻をyyyy/mm/ddの文字列で取得する方法

Javascriptで現在時刻をyyyy/mm/ddの文字列で取得する方法について備忘録的に記載します。

Javascriptで現在日時の取得

Javascriptで現在日時の取得は

new Date();

を使ってDate型で取得出来る。これをyyyy/mm/dd形式に編集する場合、以下の様に年月日に分解してから連結する。

let date = new Date();              // 現在日時の取得
let year = date.getFullYear();      // 年の取り出し
let month = date.getMonth()+1;      // 月の取り出し
let day = date.getDate();           // 日の取り出し

window.confirm(year + "/" + month + "/" + day); // 現在日付表示

現在日時が2020年10月10日の場合『2020/10/10』と表示される。
但し、現在日時が2021年1月1日の場合『2021/1/1』と表示される。
『2021/01/01』の様に月日を2桁に整形したい場合、以下の様に月日を取得する。

let month = ("00" + (date.getMonth()+1)).slice(-2); // 月の取り出し
let day = ("00" + date.getDate()).slice(-2);        // 日の取り出し

頭に”00″をつけてslice(-2)とすることで、2桁に整形する。

sliceについては、以下を参照。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/slice

時分秒まで文字列で取得したい場合

時分秒まで取得する場合は、以下の様に記載する。

let date = new Date();              // 現在日時の取得
let year = date.getFullYear();      // 年の取り出し
let month = date.getMonth()+1;      // 月の取り出し
let day = date.getDate();           // 日の取り出し

let hour = date.getHours();         // 時の取り出し
let min = date.getMinutes();        // 分の取り出し
let sec = date.getSeconds();        // 秒の取り出し

window.confirm(year + "/" + month + "/" + day + " " + hour + ":" + min + ":" + sec);    // 現在日時表示

※時分秒を2桁に整形する場合は、月日と同様に頭に”00″をつけてslice(-2)とする。

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

本当にあったUIの指摘事項(運用担当者からの指摘編)

運用担当(Linux をよく触る人達)からの指摘

始めに

これは、私が実際に業務で体験した、UIへの指摘事項の話です。

私は数年間、社内ポータルの開発に携わってきました。
これまで学んできた UI のお作法に則って開発をしてきましたが、
まさか、そこに様々な指摘事項を貰うことになるとは
思いもよりませんでした。

今回はそのうち、運用担当の方、
特に Linux に習熟している方々から頂いたお便り(指摘)を紹介します。

※個人情報、および業務上の情報は伏せています

サイドバー、邪魔

最初の状態

最近の流行りを取り入れ、各ページへのリンクをサイドバーにまとめた。
また、レスポンシブを考慮し、
ウィンドウを小さくした場合はサイドバーを隠し、
右上にハンバーガーボタン(線が3つくらい並んでいるアレ)を設置し、
サイドバーと同じリンクを出すようにした。

※サイドバー:Webページの左側などに出てくるメニュー
※レスポンシブ:画面サイズによってページのレイアウトを変える技法
(例:スマホで見るとレイアウトが変わるページ)
※ハンバーガーボタン:下図のような線が並んだボタン。通常、押すとメニューが表示される
ハンバーガーボタン

指摘

運用担当Aさんよりご指摘を頂いた。
サイドバーが横幅を占有しすぎていて見辛い、とのこと。
(リモートではラップトップPCなので余計見辛い)

ハンバーガーボタンについてはAさん以外の方からも指摘があり、
ボタンの存在に気付けなかった、とか
そもそもこのボタンって何なの?というご意見もあった。

対応

サイドバーの下に開閉ボタンを付け、サイドバーを収納できるようにした。
開閉ボタンには大きく日本語で メニューを閉じる と記載した。
(閉じているときは メニューを開ける

なお、ハンバーガーボタンは分かりづらいという事で削除した。

また、特定のページしか見ない人は
そもそもサイドバーすら不要(リンク辿らない)、とのことなので、
サイドバーの開閉状態を Cookie に記憶させるようにした。
(一度閉じれば閉じたまま)

反響

Aさんより、サイドバーを開閉できることで横幅が広くなった、とご意見を頂いた。

Cookie での開閉状態の記憶も、
特定のページしか見ない人にとっては、
いちいち閉じる手間が減って助かっているようだ。

教訓

  • サイドバーは開閉できるようにする
  • メニューをハンバーガーボタンだけに格納しない
    • ボタンの存在や用途に気付けない人もいる
    • 使うならサイドバーなどと併用した方がよい

ボタン類は左側にまとめて

最初の状態

データベースの情報をリスト表示するUIを作成した。
1行ごとに、データの編集や削除ができるボタンを 行の右端 に設けた。

指摘

運用担当Bさんよりご指摘を頂いた。
文字列が長い場合、一行が長くなり、右端のボタンが一画面で収まらなくなる。
このため、いちいちスクロールしてボタンを押すのが面倒、とのこと。

また、削除ボタンについては
"誤って押したら怖い" とのご相談も頂いた。
(一応、確認画面は出すようにしているが、それでも心配らしい)

なお、上述のボタン配置については、
サイト内の全ページで統一した方が戸惑わなくてよいのでは、という意見も頂いた。

対応

操作ボタンを各行の左端にまとめて表示した。

削除ボタンについては、削除したい行を選択した上で、
画面左上に1個だけある削除ボタンを押す仕様に変更。

加えて、サイト内の全ページに同じレイアウトを適用した。

反響

Bさんから、ボタンを見つける手間(スクロール)が減った、とご意見を頂いた。

教訓

操作ボタンなどのよく使う or よく見る項目は
スクロール無しで見えたほうがいい。

また、ページのレイアウトはサイト全体で一貫性を持たせると、
利用者の戸惑いを無くせる。

※ただ、必ずしも全ページで統一する必要はないと思う。
 例:同じレイアウトでは表現や操作が難しい場合

一覧を見ただけで内容を把握したい

最初の状態

障害対応の一覧ページがある。
このページでは、1つの障害に対してその進行状況などを記録でき、
詳細画面を開けば障害の情報が細かく出てくる。

ただ、一覧表示では障害の題名と発生日時だけを表示しており、
特に他の項目は出さないようにした(行が長くなるので)。

指摘

運用担当Cさんよりご指摘を頂いた。
題名と日時だけだと、似たような障害が多い場合に判別がつきづらい。
障害を一意に見分けるために必要な項目を
幾つか一覧に出して欲しい、とのこと。

対応

一覧に運用担当より依頼のあった項目も表示するようにした。
長文になる場合は途中まででカット、
または 非表示部分を展開する ボタンなどで展開できるようにした。

反響

Cさんより、一覧を見るだけで障害の判別が付くようになり、
作業がしやすくなった、とのご意見を頂いた。

教訓

一覧画面を作るときは、奇麗に表示することに考えが行き、
どうしてもデータ量を減らしたくなる。
しかし、あまり表示を抑制してしまうと
各行の内容を特定できなくなる、という悪影響もある。

そのページで作業を行う人に確認し、
その行を一意に判断可能な項目を出してあげることが大事。

詳細ページは短いURLで連携したい

最初の状態

前項で説明した障害対応の一覧ページについて、
運用担当Dさんより依頼を受けた。
障害対応の詳細画面の URL をチャットでメンバーに共有したい、という依頼。
※チャット:業務で利用している文章での会話ツール(Slack など)

ひとまず、障害の検索条件を URL の QUERY STRING に付け、コピペできるようにした。

指摘

Dさんよりご指摘を頂いた。
URLの共有はできるようになったが、
URLが長すぎてチャットが汚れる、とのこと。

確かに、検索条件をたくさん繋げているので長い。

また、チャットの仕様なのか
QUERY STRING が途中までしか認識されなくなり、
リンクとして機能しなくなる問題もあった。

修正前のチャット貼りつけ例(長い URL の場合)
修正前

  • URL が長すぎて複数行に渡ってしまい、前後の会話が見辛くなる。
  • 更に、途中でリンクが切れてしまう。
    • 上図の下線部分(リンク)が3行目で切れている
    • なので、リンクをダブルクリックしてブラウザを開いても、正しくページが表示できない
  • ※例は kibana というデータ可視化ツールの検索 URL(そのまま)

対応

URL に障害情報の hash キーだけを付与するように修正し、
短い URL を共有できるようにした。

具体的に書くと

  • 障害ごとに一意の hash キーを作り DB に登録
  • hash キーを QUERY STRING に付与
  • URL が叩かれたら、バックエンド側で hash キーを WHERE 句の条件にして DB から障害情報の検索を行う

修正後のチャット貼りつけ例(短い URL にした場合)
修正後

  • 大体1行で収まるので、会話の妨げにならない
  • ※例は kibana の検索 URL(ハッシュ版)

反響

Dさんより、URLがコンパクトになりチャットに貼りやすくなった、とのご意見を頂いた。

教訓

障害など、複数人と共有する可能性がある検索結果については、
検索結果の詳細ページに直接飛べる URL を用意しておく。
URL は共有しやすいように短くするとよい。

  • 例:URL には短いキーだけを付与し、そのキーで DB を検索する など
※本記事に登場する人物やサービスは仮名です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

本当にあったUIの指摘事項(Windows 利用者からの指摘編)

運用担当(主に Windows で作業する方々)から頂いた指摘

始めに

これは、私が実際に業務で体験した、UIへの指摘事項の話です。

私は数年間、社内ポータルの開発に携わってきました。
これまで学んできた UI のお作法に則って開発をしてきましたが、
まさか、そこに様々な指摘事項を貰うことになるとは
思いもよりませんでした。

今回はそのうち、運用担当の方、
特に Windows をよく使う方々から頂いたお便り(指摘)を紹介します。

※個人情報、および業務上の情報は伏せています

Windows のような選択操作がしたい

最初の状態

あるデータをリスト形式で複数行表示するUIを作成した。
当初の要望として、複数行選択して削除やダウンロードをしたい、
という意見があったので、チェックボックスで選択可能にした。

指摘

運用担当Aさんよりご指摘を頂いた。
削除したいデータが何件もある場合、
チェックボックスを何度も押すのが面倒、というもの。
試しにやってみると確かにめんどくさい。

Aさん曰く、Windows みたいに
Ctrl とか Shift を押して複数選べないの?とのこと。
その発想は無かった;

対応

チェックボックスを廃止し、
Ctrl と Shift で Windows の様に複数行選択できるようにした。

具体的には以下のような対応をした。

  • Ctrl を押している状態だと、前の選択行を維持しつつ、新たにクリックした行も選択状態(行の色を反転)になる
  • Shift を押している状態だと、前回クリックした行と、次にクリックした行の間にある全ての行が選択状態になる。

※Google Drive が近い

反響

Aさんより、直感的で使いやすい、との回答を頂いた。
なお、Aさんは元々運用経験の浅い方だったので、
Windows のフォルダ操作の方が自然だったのだろう。

教訓

Windows と同じようにして欲しい、という意見は結構多い。
全てを聞き入れる必要はないだろうが、
その画面を操作する人の経験に応じた対応を入れる、という観点も必要だと思った。

コピペはトリプルクリックで

最初の状態

障害調査用の検索画面を作成した。
検索結果をコピーして使う事があるそうなので、
CSV 形式でダウンロードできるようにした。

指摘

運用担当Bさんよりご意見を頂いた。
検索結果を画面上でトリプルクリックで一括選択したい、というもの。

CSV ダウンロードではダメ? と尋ねると、
ちょっとした項目をコピーするのに
いちいちローカルにファイルを落とすのは面倒、とのこと。

それならマウスで選択してコピーすれば・・・
と言おうとしたが、それを言うと負けになる気がしたので考えることにした。

没提案

よくある "クリップボードにコピー" ボタンを設けてはどうか?

回答

コピーボタンは直感的じゃない。
どうしてもクリックでコピーしたくなる、との反対意見。
また、テーブル形式で横に長い検索結果が表示されるので、
ボタンを配置すると、横に長くなって余計見づらくなる、とのご指摘。

対応

よくコピーされる項目だけ、トリプルクリックで全行コピーできるようにした。
リストがテーブル形式の場合、文字列の最後に空白 が入ることもあるので、trim もしておいた。
※trim:文頭、文末などの余分な空白や改行を除去する作業

反響

Bさんより、クリックでコピーできる方がやはり楽だね、というご意見を頂いた。
私はマウスをあまり使わないので(キーボード派なので)気付けなかった。

教訓

クリックでコピーしたい、という要望は未だに根強い。
また、ダウンロードやコピーボタンがあれば十分だろう、という思い込みも正解とは限らない、
UIの操作は作業者の目線になって操作、作成してみることが大事。

入力欄が小さすぎる

最初の状態

検索用の入力欄を作成した。
メールアドレスなどが入る項目で、画面のレイアウトを揃えるため、
大きさは漢字8文字程度のサイズにした。

指摘

運用担当Cさんよりご指摘を頂いた。
検索時に長いメールアドレスを入力することがあるが、
入力欄が狭すぎて何を貼りつけたのか(入力したのか)分からない、とのこと。

話によると、
運用作業では検索文字を入力欄にコピペしており、
検索実行前に ちゃんと入力できているか目視で確認 している。
なので、入力項目欄が小さいと、確認するのがとても面倒とのこと。

対応

入力した文字列のサイズに応じて、
入力欄が自動的に拡大・縮小するようにした。
コピペで入力した際も同様。

反響

Cさんより、コピペ時の文字列確認が楽になった、とのご意見を頂いた。

教訓

入力欄を作成しているときは、画面レイアウトの方にとらわれすぎ、
入力欄自体のサイズの考慮はほとんどしていなかった。

しかし、検索する側は 入力後の確認 も行う事があるので、
入力欄のサイズは、入力されるものに応じて変えるか、自動変更できるとよい。

直前に操作をした行はどこ?

最初の状態

データベースの内容をリスト表示するページを作成した。
各行に編集ボタンがあり、モーダルで開いた編集画面で内容を変更することが可能。
※モーダル:Webページにおいて、ポップアップで開く小さなウィンドウのこと
モーダル

指摘

運用担当Dさんよりご相談を頂いた。
編集が終わった後、自分がリストのどの行まで作業していたのか忘れた。
どうにかならないか?というご意見。

話を伺うと、
リストの上から順に処理をしているらしく、流れ作業なので、
さっきまで編集していた内容を覚えていないということ。
更に、似たような内容の行が多いのでなおさら分かりにくいらしい。

没提案

列に通番を振った。
しかし、通番なんていちいち覚えていない、というご指摘を頂いた。
そもそも、新しい項目が追加されると通番がずれてしまうので、微妙だった。

対応

編集画面(モーダル)を開いた場合、その行全体の色を赤くした。
※色は通常背景と被らなければ何でもOK

反響

Dさんより、直前に編集した行の色が変わり、
今の作業位置が分かるようになった、とのご意見を頂いた。

教訓

データ量が多く、かつ流れ作業で編集していくようなページでは、
編集作業をした行がアクティブ(背景色を変えるなど)になった方が、
流れ作業が遮断されず効率的。

これは、編集画面がモーダルでもページ遷移でも同じだと思う。

並び替えしている項目が分からない

最初の状態

Bootstrap の DataTables という機能を利用し、
データをリスト表示するページを作成した。
各項目でソートができ、ソートした項目に矢印が付く機能がある。

指摘

運用担当Eさんよりご指摘を頂いた。
ソートボタンが小さすぎて、どの項目でソートしているのか分かりづらい、ととのこと。

また、ソート対象の項目の内容が長いと、
列のヘッダ(ソート時に矢印が出るところ)が広くなり、
矢印がどの項目についているのか余計分かりにくくなる、とのこと。

修正前の UI
修正前

  • 件名で並び替えているが、並び替えを示す矢印(▼)が隣の障害内容に近いため、どちらで並び替えているのかが分かりにくい

対応

Bootstrap の Datatables では、デフォルトでテーブルの縦の罫線が出てこない。
なので、これをあえて表示することにした。
※ヘッダ行のみ対応。テーブル本体側は特に問題なしとのことで罫線は付けず
これにより、矢印が存在する項目が分かるようになった。

加えて、ソートした列のヘッダに色を付けることにした。

修正後の UI
修正後

  • 件名 で並び替えをしていることが一目で分かるようになった

反響

Eさんより、立ての罫線とヘッダの色付けにより、
ソートした項目を見間違えることが無くなった、とのご意見を頂いた。

なお、Eさんは普段、スプレッドシート(Excel)での編集に慣れている方で、
罫線がない表には違和感があったのかもしれない。

教訓

JSの外部ライブラリでは、テーブルの罫線を無くすレイアウトがある。
しかし、これはスプレッドシートの表示に慣れている人にとっては
境界が分かりづらく、見づらい。

選択した列に色が付く、など
スプレッドシート準拠の仕様になっていると喜ばれることが多い。

先の "Windows と同じようにして" という件もそうだが、
"Excel と同じような操作にして欲しい" という意見もよく頂く。
ただ、この辺りは利用者によって好みが分かれるため、
メインで利用する方々へのヒアリングや、
モックでの確認を行い、最善な UI を作り上げていくと良さそう。

※本記事に登場する人物やサービスは仮名です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

本当にあったUIの指摘事項(営業担当からの指摘編)

営業担当より頂いたUIの指摘

始めに

これは、私が実際に業務で体験した、UIへの指摘事項の話です。

私は数年間、社内ポータルの開発に携わってきました。
これまで学んできた UI のお作法に則って開発をしてきましたが、
まさか、そこに様々な指摘事項を貰うことになるとは
思いもよりませんでした。

今回はそのうち、
営業担当の方から頂いたお便り(指摘)を紹介します。

※個人情報、および業務上の情報は伏せています

自動検索は時と場合によりけり

最初の状態

最近の流行りに乗っかり、検索画面を自動検索(項目を入力 or 選択しただけで検索が走る UI)にした。

指摘

営業担当Aさんよりご指摘を頂いた。
まだ全項目を入力していないのに勝手に検索が走る。
いちいち入力や選択の度に検索が実行されると、ちょっとイラっとする、とのこと。

特に期間検索で広範囲を入力した際は、
検索に時間がかかるので猶更イライラするとのこと。

対応

入力項目が多い検索ページについては自動検索を廃止し、
検索ボタンで検索を開始するようにした。

反響

担当者にも満足して頂けた。

なお、検索項目が少ない場合は自動検索の方が喜ばれることもある。
特に運用者向けページでは、障害対応で急いでいる場合など、
日時や障害種別を選んだだけで検索開始できた方がよい、
という意見もあった。

教訓

自動検索は一見スマートで便利そうに見えるが、
検索が毎回走る、というのは人によってはストレスになるため、あまり多用しない方が良い。
ただし、ページの利用用途に応じて、自動検索の方が良い場合もある。
(前述の運用者向けページなど)

相対検索より期間検索

最初の状態

検索画面の日時入力項目を作成する際、
検索範囲を、デートピッカーで指定した日から過去n日、
のような相対的な検索方法(相対検索)で統一した。

※デートピッカー
 カレンダーのようなウィンドウが開き、日付や時刻を選択できる機能
デートピッカー

指摘

営業担当Bさんよりご意見を頂いた。
過去n日、だと開始日がいつなのか分からない。
特に、特定の月のデータが欲しいときに困る。

ここからここまで、みたいな期間検索の方が直感的で分かりやすい、とのこと。

対応

以下のような期間検索を実装した。

  • 開始日 と 終了日 を指定する
  • 検索ボタンをクリックすることで期間検索が走る

ただし、相対検索は 運用担当 が監視などでよく使うので、
文句を言われないように、どちらも使えるようにしておいた。

反響

営業担当Bさんより、自分の検索したい期間を自由に選べるので、効率的に探せるようになった、とのご意見を頂いた。

教訓

相対検索は日時検索のお作法の一つだが、
人によっては期間検索(絶対指定)の方が探しやすい
という場合もある。

両方の方式で検索できるのがベターだが、
実装が難しい場合、そのページの利用者(例:営業か運用か)によって使い分けるのも手。

英語より日本語の方がいい!

最初の状態

運用と営業担当向けに、自社サービスAの統計ページを作成した。
グラフの項目名に、サービスAの独自用語が幾つか出てくるのだが、そのまま※掲載した。
※運用や開発でよく使う言葉のまま記載。ほぼ英語

指摘

営業担当Cさんよりご指摘を頂いた。
グラフの項目の意味が分からない、とのこと。

営業部門には運用などで通用している用語では伝わらなかった模様。
サービスで外部公開している言葉に直し、
かつ漢字やカタカタで書いてくれた方が分かりやすい、とのこと。

対応

サービスAの独自用語を外部公開している言葉で、
かつ漢字やカタカタにして記載するようにした。

ただし、運用権限などでログインした際は、英語での表記にした。
※運用部門ではこちらの方が把握しやすい

反響

営業部門から、グラフの用語が分からない、という声は出なくなった。

教訓

サービスや社内に限った専門用語を使う場合、
ログインするユーザに応じて表記を出し分けたほうが良い。

  • 例:営業向け=日本語表記、運用向け=英語そのまま
  • ログイン権限などで表示方法を判定するのが無難

グラフにコメントを書きたい

最初の状態

とあるサービスの統計グラフを作成して欲しい、との依頼があった。
グラフ作成の某JSライブラリを使用し、いい感じのグラフが描けた!
そのときはそう思った。

指摘

営業担当Dさんよりご相談を頂いた。
データをグラフで可視化できたのはいいけれど、
このグラフを1つ1つJPG形式で保存し、毎回PDFを書くのが面倒、
とのこと。

話によると、グラフをJPG形式でダウンロードした後、
パワーポイントに貼りつけてコメント(例:今月はコロナの影響で下方修正で・・・)を書き、
それをPDFに変換してサービス利用実績などの報告をしているそうだ。

対応

個々の統計グラフの真下に コメント欄 を設置。
また、ページの右上に PDF出力ボタン を設置し、
クリックすることで グラフ と コメント を同時に PDF 化できるようにした。

反響

営業資料の作成の手間が減った、とのご意見を頂いた。
これはかなり喜んでもらえたものの一つ。

教訓

データの可視化は グラフに出したら終わり ではない。
活用用途によっては、そこにコメントを描いたり、
PDF としてまとめて出力したいこともある。

可視化とは、データを見えるようにして、
更に、利用者のニーズを反映さえてこそ、
完成といえるのかもしれない。

※本記事に登場する人物やサービスは仮名です
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】数値の表現

私自身の備忘録でもあります。
JavaScriptで数字を出力させる際、単に数字が並んでいるだけではなく
読みやすく、もしくは、簡素にする為の数字の表現方法です。

数字を3桁毎にカンマを打って表示

.toLocaleString()

test.js
const num = 12345;
num.toLocaleString();

次のようになります。
12,345

小数点以下切り捨て

Math.floor(hoge)

test.js
const num = 123.456;
Math.floor(num)

次のようになります。
123

小数点以下切り上げ

Math.ceil(fuga)

test.js
const num = 123.456;
Math.ceil(num)

次のようになります。
124

小数点以下四捨五入

Math.round(piyo)

小数点以下が"4"以下の場合

test.js
const num = 123.456;
Math.round(num)

次のようになります。
123

小数点以下が"5"以上の場合

test.js
const num = 123.567;
Math.round(num)

次のようになります。
124

桁を指定した 四捨五入・切り捨て・切り上げ

※今回は四捨五入を例にしています。

小数第一位を基準とした方法

test.js
const num = 123.456;
Math.round(num * 10) / 10

次のようになります。
123.5

十の位を基準とした方法

test.js
const num = 123.456;
Math.round(num / 10) * 10

次のようになります。
120

最後に (こんな使い方しています)

小数点以下切り捨て かつ 数字を3桁毎にカンマを打つ

test.js
const num = 1234567.89
Math.floor(num).toLocaleString();

次のようになります。
1,234,567

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

PWAを試してみよう

(最近あまり聞かなくなりましたが)扱ったことがなかったので、PWA(Progressive Web Apps) を試してみようと思います。

WebページをPWAとして設定することで以下のことができます。

  • Webアプリなのに、ネイティブアプリのように、Android/Windowsにアプリとして登録することができる。
  • アドレスバーのようなブラウザっぽさはなく、全画面でネイティブアプリのように起動することができる。

PWAのService Workerの機能を使った実装をすることで、以下のことができます。

  • Webコンテンツをキャッシュ化することができ、オフラインで動かすことができる。
  • サーバ側からPush通知ができる。

ということで、今回の投稿では、PWAの設定方法と、Push通知の実装をしてみます。

ただ作るだけではモチベーションが上がらないので、パスワード管理アプリ「パスワードマネージャ」を作成します。
世の中にいろいろなツールがありますが、やっぱり自分で管理したいので。。。

毎度ながら、ソースコード一式をGitHubに上げておきました。

poruruba/pwa_test
 https://github.com/poruruba/pwa_test

PWAの設定

以下を準備する必要があります。

  • manifest.jsonを作成し、配備します。
  • ServiceWorker起動のためのJavascriptを作成し、配備します。

※ただし、上記を配備するWebサーバは、HTTPSである必要があります。

manifest.json

こんな感じにします。

public/manifest.json
{
    "short_name": "パスワードマネージャ",
    "name": "パスワードマネージャ",
    "display": "standalone",
    "start_url": "index.html",
    "icons": [
        {
            "src": "img/192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        }
    ]
}

上述の通り、最低限192x192のpngファイルが必要です。
あとは、このmanifest.jsonをルートに配置し、index.htmlから、以下のようにして読み出すようにします。

public/index.html
・・・
  <link rel="manifest" href="manifest.json">
・・・

Service WorkerのためのJavascript

こんな感じです。

public/sw.js
var CACHE_NAME = 'pwa-sample-caches';
var urlsToCache = [
  // キャッシュ化したいコンテンツ
];

self.addEventListener('install', function(event) {
  console.log('sw event: install called');

  event.waitUntil(
    caches.open(CACHE_NAME)
    .then(function(cache) {
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener('fetch', function(event) {
  console.log('sw event: fetch called');

  event.respondWith(
    caches.match(event.request)
    .then(function(response) {
      return response ? response : fetch(event.request);
    })
  );  
});

self.addEventListener('push', function(event){
  console.log('sw event: push called');

  var notificationDataObj = event.data.json();
  var content = {
    body: notificationDataObj.body,
  };
  event.waitUntil(
    self.registration.showNotification(notificationDataObj.title, content)
  );
});

補足します。

self.addEventListener('install', function(event) {
self.addEventListener('fetch', function(event) {

は、Webコンテンツをキャッシュ化する場合に必要です。
ただし、Webコンテンツをキャッシュ化しなくとも、中身は無くてもよいですが、self.addEventListener('install', function(event){ は必要です。

self.addEventListener('push', function(event) {

は、Push通知を利用する場合に必要です。後述します。

あとは、Webページがロードされたときに、以下を実行すればServiceWorkerが登録されます。Javascriptのファイル名は、sw.jsの前提です。

public/js/start.js
・・・
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js').then(async (registration) => {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch((err) => {
                console.log('ServiceWorker registration failed: ', err);
            });
        }
・・・

ロードされたかどうかは、Chromeの開発ツールで確認することができます。
F12で開発ツールを開いたのち、Applicationタブを選択すると、左側に「Service Workers」があります。

image.png

ServiceWorkerのためのJavascriptはソースファイルを更新しても、Chrome内に登録されたものは更新されませんので、この開発ツールから、UpdeteやUnregisterをすることができます。

で、アドレスバーの右隅に、⊕ があるのがわかりますでしょうか。
これが、このWebページがPWAとして登録できることを示しています。
OSやブラウザによってここらへんの表現は異なります。これをクリックすると、こんなのが表示されます。

image.png

「インストール」ボタンを押下すると、PWAアプリとして登録され、完了すると、以下のように単独のページとして表示されます。

image.png

これが、PWAのアプリです。アドレスバーがないのがわかります。Webアプリではなくネイティブアプリに見えますよね。
スタートメニューにも、Chromeアプリのところですが、「パスワードマネージャ」が登録されています。

image.png

いつでも、これをクリックすることで、アプリとして起動できるようになりました。

Push通知の実装

Node.jsのnpmモジュールのおかげで、サーバ側をすぐに立ち上げることができます。

web-push-libs/web-push
 https://github.com/web-push-libs/web-push

また、パスワードマネージャの機能のために、以下のnpm モジュールも使っています。

uuidjs/uuid
 https://github.com/uuidjs/uuid

まず最初にするのが、VapidKeyの生成です。

api/controllers/password/index.js
async function readPasswordFile(apikey){
    try{
        var pwd = await fs.readFile(FILE_BASE + apikey + '.json', 'utf8');
        if( !pwd ){
            pwd = {
                vapidkey : webpush.generateVAPIDKeys(),
                list: [],
                objects: {}     
            };
            await writePasswordFile(apikey, pwd);
        }else{
            pwd = JSON.parse(pwd);
        }
        return pwd;
    }catch(error){
        throw "not found";
    }
}

大事なのは以下の部分です。VAPIKEYという公開鍵ペアの作成です。

vapidkey : webpush.generateVAPIDKeys()

最初に生成した後は、同じ値を使うので、上記の関数の中でこの値をファイルに保存しています。ファイルの中身はこんな感じです。

data/password/test.json
{
  "vapidkey": {
    "publicKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "privateKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  list: [],
  objects: {}
}

このVapidKeyのうちの公開鍵vapidkey.publicKeyをクライアントに渡します。以下の部分です。通知先クライアントを区別するために、client_idとしてuuidを生成しそれも一緒に渡しています。

api/controllers/password/index.js
    if( event.path == '/pwd-get-pubkey' ){
        var uuid = uuidv4();
        return new Response({ result: { vapidkey: pwd.vapidkey.publicKey, client_id: uuid } });
    }else

クライアント側では以下の処理をしています。

public/js/start.js
    var json = await do_post_apikey(base_url + '/pwd-get-pubkey', {}, this.apikey);

    var object = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: json.result.vapidkey
    });
    await do_post_apikey(base_url + '/pwd-put-object', { client_id: json.result.client_id, object: object }, this.apikey);

    this.client_id = json.result.client_id;
    Cookies.set('client_id', this.client_id, { expires: EXPIRES });

サーバ側から取得した公開鍵をregistration.pushManager.subscribeに渡しています。
そうすることで、通知のSubscribeが完了し、Push Subscriptionオブジェクトを取得できますので、それをサーバに返してあげます。

Push Subscriptionオブジェクトの内容はこんな感じです。

{
 endpoint: "https://fcm.googleapis.com/fcm/send/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
 expirationTime: null
 keys: {
  auth: "XXXXXXXXXXXXXXXXX"
  p256dh: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
 }
}

一緒に、さっきもらったclient_idも一緒にサーバに戻しておきます。client_idのやり取りはPush通知の実装では必須ではないので、適当な実装でもよいです。

サーバ側ではそのPush Subscriptionオブジェクトを受けとって、後でNotificationするときに必要なので、これもファイルに保存しておきます。

api/controllers/password/index.js
    if( event.path == '/pwd-put-object' ){
        pwd.objects[body.client_id] = body.object;
        await writePasswordFile(apikey, pwd);

        await sendNotification(pwd.vapidkey, pwd.objects[body.client_id], { title: "パスワードマネージャ", body: "通知を登録しました。"} );

        return new Response({});
    }else

準備ができたので、後は任意のタイミングで、sendNotification関数を呼び出します。上記のタイミングでも呼び出しています。
中身は以下の感じです。

api/controllers/password/index.js
async function sendNotification(vapidkey, object, content){
    var options = {
        vapidDetails: {
            subject: NOTIFICATION_SUBJECT,
            publicKey: vapidkey.publicKey,
            privateKey: vapidkey.privateKey
        }
    };
    var result = await webpush.sendNotification(object, Buffer.from(JSON.stringify(content)), options);
    if( result.statusCode != 201 )
        throw "status is not 201";
}

webpush.sendNotification() に必要なパラメータを設定して呼び出しています。

通知したい内容は、contentのところで指定します。
例えばこんな感じです。

 { title: "パスワードマネージャ", body: "通知を登録しました。"}

このフォーマットは、なんでもよいのですが、受信する側では把握している前提です。

クライアント側の方は、Push通知を受け付けられるように、コールバック関数を実装しておく必要があります。ServiceWorkerのためのJavascriptに実装します。

public/sw.js
self.addEventListener('push', function(event){
  console.log('sw event: push called');

  var notificationDataObj = event.data.json();
  var content = {
    body: notificationDataObj.body,
  };
  event.waitUntil(
    self.registration.showNotification(notificationDataObj.title, content)
  );
});

受信したデータのうち、bodyとtitleを使っています。

Push通知を登録したり解除したりできるようにボタンを用意しておきましたので、「Subscribe」ボタンを押してみましょう。

image.png

通知が来ました。

1点注意です。
Windowsで、通知をOffにしていると、いつまでたっても通知は受け取れません。
あらかじめ、通知をOnにしておきましょう。

image.png

システムの、通知とアクションにあります。
「アプリやその他の送信者からの通知を取得する」 をオンにします。

image.png

Androidの場合

Androidの場合についても示しておきます。
Webアプリなので、OSに依存せず同じように登録できるのはWebアプリのメリットです。

Androidの場合は、PWAアプリとして登録するのは、Chromeブラウザのメニューから「アプリをインストール」を選択することでインストールされます。

image.png

タッチすると以下が表示されます。そのまま、インストールをタップすると、インストールが完了します。

image.png

ホーム画面に登録されました。

image.png

さっそく、アイコンをタップして起動します。
最初に、スプラッシュ画面が表示された後、

image.png

めでたく、起動できました。
ブラウザで見た画面と同じで、なおかつアドレスバーもありません。

image.png

以下のようにアプリとして登録されるので、もうネイティブアプリと区別がつきません。

image.png

その他

パスワードマネージャとしての実装は、GitHubをご参照ください。。。
簡単に、使い方だけ。

トップ画面です。

image.png

先に、右上のAPI Keyから、apikeyを設定します。

image.png

それに対応するファイルをあらかじめサーバ側に作成しておく必要があります。

 \data\password\****.json

拡張子.json を抜いたファイル名を指定します。サンプルとして、test.jsを置いておきました。これはバレバレなので、乱数の長めの文字列をファイル名にして他人から推測されないようにしてください。

※当然ですが、パスワードが漏洩するなど、私は一切責任を持ちません!

apikeyを変更した場合は、「F5キー」で表示をリロードしてください。

それでは、パスワードを作成しましょう。「新規作成」ボタンを押下します。

image.png

ここに、記憶したいまたは作成したいユーザIDやパスワードを入力します。パスワードは、自動生成機能を用意しておきました。
nameやurlの入力は任意です。というより、nameを入れないと、他と区別がつかないです。
最後に、「作成」ボタンを押下します。

こんな感じで作成されました。パスワードはサーバ側の先ほどのjsonファイルに保存されています。

image.png

Copy列のボタンを押下すれば、パスワードがクリップボードにコピーされます。
変更したい場合は、「変更」ボタンを押下すると、サーバ側に保持した内容を変更します。

image.png

「変更」ボタンを押下したとき、サーバ側で変更が発生したことを知らせる通知がクライアント側に届くようにしました。

パスワードを削除したい場合は、「削除」ボタンを押下します。

終わりに

以下のページを参考にさせていただきました。

PWAの作り方をサクッと学ぶ - 「ホーム画面に追加」「キャッシュ操作」「プッシュ通知」の実装
 https://eh-career.com/engineerhub/entry/2019/10/24/103000

PWAのプッシュ通知の仕組み
 https://ajike.github.io/how-pwa-push-works/
 (っていうより、こちらのページの方が説明が丁寧です。。。)

W3C Push API
 https://w3c.github.io/push-api/

Voluntary Application Server Identification for Web Push (VAPID) (RFC 8292)
 https://tools.ietf.org/html/rfc8292

以上

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