- 投稿日:2020-05-23T23:14:28+09:00
【備忘録】HTMLのテンプレート
単独のHTMLのテンプレートです。Vue.js版とかも下に書いてますが、CDN利用なのでNode.jsで使う場合とは異なるのでご注意を。
時々バージョンアップしたり追加したりします。ソースコード
汎用版
特にフレームワークを使用しないパターン。
index.html<!doctype html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta http-equiv='X-UA-Compatible' content='IE=edge' /> <meta name='viewport' content='width=device-width, initial-scale=1' /> <title>...</title> <link rel="stylesheet" href="./style/basic.css"> </head> <body> <script src="./script/basic.js"></script> </body> </html>basic.csshtml, body { width: 100%; height: 100%; margin: 0px; overflow: hidden; }basic.js"use strict"Vue.js(CDN)版
index.html<!doctype html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta http-equiv='X-UA-Compatible' content='IE=edge' /> <meta name='viewport' content='width=device-width, initial-scale=1' /> <title>...</title> <link rel="stylesheet" href="./style/basic.css"> </head> <body> <div id='app' style="display:none"> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script> <script src="./script/basic.js"></script> <script> const vm = new Vue({ el: '#app', data: () => ({ }), methods: { }, mounted() { document.getElementById('app').style.display = "block"; }, }); </script> </body> </html>Vue.js + ElementUI(CDN)版
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta http-equiv='X-UA-Compatible' content='IE=edge' /> <meta name='viewport' content='width=device-width, initial-scale=1' /> <title>...</title> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> <link rel="stylesheet" href="./style/basic.css"> </head> <body> <div id='app' style="display:none"> <el-container> <el-header> </el-header> <el-main> </el-main> <el-footer> </el-footer> </el-container> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script> <script src="https://unpkg.com/element-ui@2.13.2/lib/index.js"></script> <script src="https://unpkg.com/element-ui@2.13.2/lib/umd/locale/ja.js"></script> <script src="./script/basic.js"></script> <script> ELEMENT.locale(ELEMENT.lang.ja); const vm = new Vue({ el: '#app', data: () => ({ }), methods: { }, mounted() { document.getElementById('app').style.display = "block"; }, }); </script> </body> </html>チラシの裏(読まなくてもいい余談)
仕事でもプライベートでもモックや検証用なんかにHTMLファイルを仕立てるんですが、毎回ウェブで拾ってくるか、既存のソースから関係ないところを削除して使っていたので、備忘録も兼ねて初投稿です。
今後も、なにかパーツに使えそうなものとかを備忘録として残していこうかと思います。
- 投稿日:2020-05-23T14:16:45+09:00
初心者でもタグ1つで秒速PWA対応。GUIエディタ(また)作りました。
概要
- 前回記事のLGTMとストックありがとうございます!
- PWAって便利ですよね。
- PWAってなかなか複雑ですよね。
- Service Workerとその登録スクリプト、1つにまとまっちゃいました。
- manifest.jsonも動的(?)に読み込めますね。
- 1行だけソース書けば対応できるようになりそう
- できちゃったのでまたエディタつくっちゃいました(こちら)
はじめに
こんにちは。いーちゃんです。
突然ですが、あなたのサイトPWA、対応してますか?
すでに実装している人、これから実装する人、面倒でできていない人など、様々だと思います。
manifestとService Workerを書けば済むんですがそれすらも面倒だと時々思ったり...。
ということでPWA対応を秒でしていきましょう。
(例によって前置きが長いのでエディタを早く使いたい方はこちらへどうぞ)そもそもPWA(Progressive Web Apps)って何?
(Nuxt.jsのサイトより)
スマートフォンなどでWebページを見ていて、このような「ホーム画面に追加」ボタンを見かけたことはありませんか?
押してみるとあたかも普通のアプリのようにホーム画面に追加されます。サイトによっては、ネットがつながらない環境でも読み込めたりもします。利用者にとっては、いい事づくしです。この機能を自分のページでも使えるようにするためには、PWA(Progressive Web Apps)に対応させなければなりません。
逆に言えば、この仕様に準拠さえすればブラウザが勝手に追加してくれるので、とても楽です。
...その実装に少し手間がかかったりもするのですが。PWAは主に、
- Web App Manifest (JSON)
- Service Worker (JavaScript)の2つで構成されています。それぞれ、
- Web App Manifest: サイトの名前やアイコンを定義する
- Service Worker: キャッシュなどの処理を裏でする
という役割です。キャッシュが必要なければService Workerはファイルだけ用意してもいいのかもしれません(未検証)。
しかし、キャッシュの実装はページの2回目以降の読み込みには大きな影響を及ぼしますから、「できることなら実装しておきたい」。そう思いませんか?Service Workerの実装
Service Workerのキャッシュの仕組み
Service Workerでは主に2回、キャッシュを取るタイミングがあります。それは、
- はじめにページが開かれた時(=Service Workerをインストールするとき)
- ファイルを実際に取得する時
です。Service Workerが取得したキャッシュは、ブラウザに保存されています。
Chromeだと、ここで見れたりします。
(Nuxt.jsのサイトより)そのため、2回目以降はサーバーと通信する必要が(少)なくなるので、ページ読み込み速度が大きく改善するのです。
通信する必要がなくなるかはサイトの仕組み次第ですが。
Service Workerの登録
Service Workerの処理内容を
sw.js
に記述したとします。そのスクリプトをアプリに関連付けるために、またスクリプトを書きます。― 普通なら 単に
<script>
タグにService Workerを書いたりsrc
属性でsw.js
を指定するだけではうまくいかないものです。HTMLのheadタグに以下を追記します。
<script> if ("serviceWorker" in navigator) { // 対応ブラウザのみ処理 navigator.serviceWorker.register("/sw.js"); } </script>あら簡単。
ちなみに、この登録用スクリプト自体は<script src="">
での指定も可能です。Web App Manifestには何を書く?
Web App Manifestは、アプリの名前、概要、アイコンなど、アプリ自体の挙動…というよりかは見た目を定義するようなファイルです。規格上
*.webmanifest
ファイルへの定義が基本ですが、中身はJSONなので*.json
への定義も許可されています。(そのため、以下manifest.jsonと呼ばせてください。)Chrome等の拡張機能でも同名の
manifest.json
が用いられますが、定義すべき内容は異なります。PWAが最低限動作するために必要な定義は、
- アプリの名前
- アイコン(1つ以上指定)
です。簡単。
ですが、例えば「PWAからのアクセスは別の処理を」や、「ホーム画面に置くにはアプリ名が長すぎる」など、アプリによっては情報を追記したほうが良いかもしれません。ということで、基本的にこのようなファイルが作成できます。
{ "name": "アプリのフルネーム", "short_name": "短い名前", "description": "アプリの概要", "start_url": "/起点URL", "theme_color": "#テーマ色", "icons": [ { "src": "/アイコン画像のパス", "sizes": "192x192" } ] }パスのはじめに/をつけ、絶対パスを指定しています。
manifest.json
はサブディレクトリから参照されることも多いので、(個人的には)絶対パスで指定しておくと便利だと思います。アイコン画像は、最低限192x192を用意しておくことが望ましいでしょう(Googleの方針)。
さらに、テーマ色も定義しておくと、ブラウザによってはいろいろな部分の表示色を変えてくれたりもするので、統一感を出すためにも指定をおすすめします。
なお、これ以外にも様々な設定ができますが、説明は割愛させていただきます。こちらのサイトに詳しい説明がありますので、興味がありましたらこちらも参照してみてください。
manifest.jsonの登録
こちらは
link
タグ1つで登録できます。<head> <link rel="manifest" href="/manifest.json"> </head>唯一注意点があるとすれば、CSSではないので
rel="manifest"
を書くことです。個人的にはhrefなのかsrcなのかもよく迷いますが。
いざ実装。
こう見るとJavaScript書いてJSON書いて、と(文法似てるけど)なかなか大変ですね。お待たせいたしました。ここでやっと1行実装の登場です。
ということで、またエディタを作りました
(こちら)。
…前回の記事をお読み頂いていると察されているかもしれませんが、ソースコードは前回から転用してます。
必須欄を最低限打ち込んでいただければService Workerとmanifest.jsonを勝手に作ります。zipにします。ダウンロードしていただき、<script>
タグ1つで動きます。Service Workerのスクリプトを見ると、「1行実装」できる理由が見つかるかもしれませんね。
エディタ実装について
サーバー構成や言語などは前回と同じなので割愛します。
コピーですから。なんで1行でいいの?
答えは簡単。
- manifest.jsonの読み込み
- Service Workerの登録
- Service Workerの処理
をService Workerのスクリプトに全部まとめちゃったからです。
何ならmanifest.jsonも消せないかな…と思い試しにBLOBのURLを渡してみましたがプロトコルがだめみたいです。難しい。
(Data URLとかならいけたり…?(未検証))Service WorkerはDOMにアクセスできないので、manifest.jsonの読み込み、Service Workerの登録を1ファイルで..なんて通常ではできません。そのため、
- 普通のスクリプトとして呼び出す
- manifest.jsonを追加する
- Service Workerとして呼び出す
- Service Workerとして処理する
という処理をするために、
(window.)navigator
の存在で処理を分岐しています。終わりに
思いつきでService Workerの登録スクリプトと処理スクリプトをまとめてみたら、意外とうまくいっちゃいました。
「思いついたらとりあえずやってみる。」大事なのかもしれませんね。例によってもう一度URL貼ります。(https://pwa.app.e-chan.cf/)
これまた例によってGitHubも公開しています。スパゲッティーです。この記事の感想、あるいはご指摘等ありましたらぜひコメントよろしくお願いします!
※特に機能テストが不十分な可能性もあるので...。SGGのLGTMが1000に到達しました〜! pic.twitter.com/mdaYy5OfjA
— しんぶんぶん (@shinbunbun_) May 22, 2020ちなみに、私も参加しているOrganization、SGGのLGTMが1000超えしたようです。
いつも閲覧ありがとうございます。
Organizationのページは こちらです。ぜひご覧ください。
- 投稿日:2020-05-23T11:00:05+09:00
Vue.jsで作る!自動保存するToDoリスト~その2~Bootstrap編
初めに
前回の
Vue.jsで作る!自動保存するToDoリスト~その1~の続きです。
今回はBootstrapを使用して、前回作成したToDoリストの見た目を装飾していこうと思います。目次
・このパートでの完成イメージ
・Bootstrapの記述
・classの解説
・まとめこのパートでの完成イメージ
Bootstrapの知識が既にある人は中をいじって自己流で綺麗にしてください。
Bootstrapの記述
index.html<div class="m-4" id="app"> <!-- v-modelはこのinputをVueとバインディングさせる役割 --> <input class="ml-4 form-control col-sm-11 border-dark" placeholder="ここにtodo入力" v-model="message"> <div class="row m-4"> <!-- @click=""の中はVueで記述するメソッド(@はv-on:の省略です) --> <button class=" col-sm-6 btn btn-primary btn-lg" @click="add_item()">追加</button> <button class="col-sm-6 btn btn-danger btn-lg" @click="all_del_item">全削除</button> </div> <div class=" border container-fluid mb-1" v-for="(item,idx) in items"> <!-- v-for( 配列, index(配列内の要素に振られた添字)を(item,idx)と表している ) --> <ul><li class="border-left"> {{ item }} <button class="col-sm-3 float-right btn btn-warning " @click="del_item( idx )"> 削除 </button> </li></ul> </div> </div>クラス名が一気に増えましたね!次にこのクラスでどう変化するかを解説します!
classの解説
<!-- * <div class="m-4" id="app"> * m-4 @margin: 4; * <input class="ml-4 form-control col-sm-11 border-dark" placeholder="ここにtodo入力" v-model="message"> * ml-4 @margin-left: 4; * form-control @text系にこのclassをつけるだけで多少良い見た目になる * col-sm-11 @画面幅を12個に分けた分の11を使って表示する(sm:タブレットサイズ以下になったら縦並び、または余白を無視する) * border-dark @dark色のborderをつける(そのまま) * <div class="row m-4"> * row @グリッドシステムで要素を分割する時の親要素につける * m-4 @margin: 4; * <button class=" col-sm-6 btn btn-primary btn-lg" @click="add_item()"> * col-sm-6 @画面幅を12個に分けた分の6を使って表示する(sm:タブレットサイズ以下になったら縦並び、または余白を無視する) * btn @Bootstrapで用意されたボタンのデザインになる * btn-primary @ボタンの色をprimaryにする * btn-lg @ボタンの大きさLargeにする * <button class="col-sm-6 btn btn-danger btn-lg" @click="all_del_item"> * col-sm-6 @画面幅を12個に分けた分の6を使って表示する(sm:タブレットサイズ以下になったら縦並び、または余白を無視する) * btn @Bootstrapで用意されたボタンのデザインになる * btn-danger @ボタンの色をdangerにする * btn-lg @ボタンの大きさLargeにする * <div class=" border container-fluid mb-1" v-for="(item,idx) in items"> * border @全方位にborderをつける * container-fluid @画面サイズに合わせて流動的に変わる * mb-1 @margin-bottom: 1; * <li class=" border-left" > * border-left @左にborderをつける * <button class="col-sm-3 float-right btn btn-warning " @click="del_item( idx )"> * col-sm-3 @画面幅を12個に分けた分の3を使って表示する(sm:タブレットサイズ以下になったら縦並び、または余白を無視する) * float-right @float: right;右に寄せる * btn @Bootstrapで用意されたボタンのデザインになる * btn-warning @ボタンの色をwarningにする * -->まとめ
実はBootstrapを模写以外で記述するのは初めてです。
ですが自分で書くことによってより一層理解が深まって一歩成長できたと思います!
この記事を見てくださったあなたの成長を応援させていただきます!!!
- 投稿日:2020-05-23T09:34:51+09:00
Django初学者の自分へ(4) - メモアプリ作成 -
チュートリアル一覧
No. タイトル 1 Django初学者の自分へ(1) - プロジェクト・アプリ - 2 Django初学者の自分へ(2) - MTVとは - 3 Django初学者の自分へ(3) - こんにちわ世界! - 4 Django初学者の自分へ(4) - メモアプリ作成 - ☆ 前回のおさらい
前回は Django で「こんにちわ世界!」を検証しました。
今回は MTV 全てを用いて、簡単なメモアプリを作成します!レッツジャンゴ -メモアプリの作成-
今回の目標はメモアプリの作成を通じて、前回使用しなかった Model と Template の理解を深めることです。また、これまで学んだことを踏まえ、Django による Web アプリ作成の一連の流れを把握できたら、この上なく嬉しいです!
以下の流れに沿って作成していきます。
1. Model の定義
2. データベースへの反映(通例行事:migrate)
3. 管理者ページ
4. View の設定
5. Template の設定
6. URL の設定Model の定義
Model ではデータベースに格納するデータの定義を記述するのでした。
それでは記述していきます。
app1/models.py
from django.db import models # Create your models here. class Memo(models.Model): title = models.CharField(verbose_name='タイトル', max_length=100) text = models.TextField(verbose_name='内容') created_date = models.DateTimeField(verbose_name='作成日', auto_now_add=True) def __str__(self): return self.title一つ一つ見ていきましょう。
- Memo はモデルの名前です。モデルの名前は頭文字を大文字にします。
- title, text, created_date はデータベースに登録するフィールドです。
- フィールドを定義する際は、それがテキストなのか、日付なのか、はたまた数字なのか、といったフィールドのタイプを決める必要があります。
models.CharField
: テキストの長さを定義するフィールドです。()内でmax_length=100
となっていますね。そうです、つまりタイトルは100文字以内で記述してくださいね、ということになります。models.TextField
: これも同じくテキストですが、長さは指定しません。models.DateTimeField
: これは日付と時間のフィールドです。auto_now_add=True
としたことでデータ作成時の時刻が自動的に入力されます。verbose_name
は管理画面での表示を指定します。管理画面については後述します。以上で Model の定義は終了です。モデルのフィールドのタイプについては他にも多くの種類があるので、気になった方は公式ドキュメントを覗いてみてください。
データベースへの反映(通例行事:migrate)
Model の定義が完了したら、それらの情報をデータベースに反映させるためにmigrate(マイグレート)という処理を行わなければなりません。これはもう通例行事というか伝統行事というか、Model の追加・変更等があった場合は必ず行わなければならないものなのです。
Model の定義からデータベースへの反映は以下の処理を実行します。
- Model の追加/変更
- マイグレーションファイルの作成(makemigrations)
- マイグレート(migrate)
マイグレーションファイルの作成
コマンドプロンプトで以下のコマンドを実行します。
C:\Users\User_name\myapp>python manage.py makemigrationsすると次のような出力が確認されるかと思います。この時点でマイグレートするためのファイルは作成されたものの、未だデータベースには反映されていません。
Migrations for 'app1': app1\migrations\0001_initial.py - Create model Memoマイグレート
先ほどに続いて次のコマンドを実行します。
C:\Users\User_name\myapp>python manage.py migrateすると次の出力が確認されるかと思います。
Operations to perform: Apply all migrations: admin, app1, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying app1.0001_initial... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying sessions.0001_initial... OKたったこれだけでマイグレートは完了です!
Memo モデルが無事にデータベースに反映されました!
...とは言うものの、実際どんなことが起こったのかイメージしづらいですよね。そんな悩みを解決するために、今日はとっておきのモノをご用意いたしました。管理者ページ
Django には管理者ページがデフォルトで搭載されています。ここでは管理者ページからデータベースにデータを格納し、先ほどのマイグレートで何が起こったのか確認してみたいと思います。
早速サーバを起動し、管理者ページにアクセスしましょう!
C:\Users\User_name\myapp>python manage.py runserverサーバが起動されたことを確認したら、
http://127.0.0.1:8000/admin
にアクセスします。すると以下のようなページに行きつくかと思います。
ん?あれ、Username に Password ? そんなもの持ってないよ。
そうなんです、でも安心してください。これもまたコマンドプロンプトから簡単に作成できちゃいます。サーバを一度切断するか、もしくは新たなコンソールを用意し、次のコマンドを実行します。C:\Users\User_name\myapp>python manage.py createsuperuser Username (leave blank to use 'User_name'): memo_user Email address: memo@mail.com Password: Password (again): Superuser created successfully.上から1行ずつ表示されていきます。Username は何も入力せずに Enter を押すとあなたの 'User_name' が登録されます。メールとパスワードまで正しく入力したところでユーザー登録は完了です。もう一度サーバを起動し、先ほどの /admin ページにてログインしてみましょう。
これが Django にデフォルトで搭載されている管理者ページです。シンプルで洗練されてますよね。んー、好き。
管理者ページにて Model を確認するにはapp1/admin.py
にて設定してあげる必要があります。
app1/admin.py
from django.contrib import admin from .models import Memo # Memo をインポート # Register your models here. admin.site.register(Memo) # 追加これらを記述したら、先ほどの管理者ページに戻ってページを更新してみてください。
我が Memo アプリのモデルが確認できました!
それでは Memo ページに行き、右上の ADD MEMO からメモを作成してみましょう!
皆さんはどんなメモを書きましたか?いやそんなことはどうでもよかったですね(笑)
メモのページに戻ると先ほどのメモが追加されているのが確認できるかと思います。Django ではこのように管理者ページを経由してデータベースにデータを追加したり、中身を確認することができます。どうやらこれはとんでもなく便利な機能らしく、本来ならばコンソールからデータベース言語を記述して操作したり、一から管理者ページを作成したりする必要があるんだとか。僕も正直データベースに関してはまだまだ発展途上なので、このように視覚的にデータを操作できるのは非常に助かります。
Django、本当にありがとう。View の設定
ここからは View を設定していきます。View はリクエストに応じてデータベースからデータを取得し、それを画面にどのように表示するか決定するんでしたよね!
app1/views.py
from django.shortcuts import render from .models import Memo # Memo をインポート # Create your views here. def memo_list(request): memos = Memo.objects.all() context = {'memos': memos} return render(request, 'app1/memo_list.html', context)今回はメモの一覧を画面に表示させるようにします。そのためには Memo のモデルのデータを取得する必要があります。
memos = Memo.objects.all()
: Memo の持つすべてのオブジェクト(タイトル、内容、作成日)を取得します。context = {'memos': memos}
: 必要な情報を辞書型にして Template に渡します。render(request, 'app1/memo_list.html', context)
:context
をapp1/memo_list.html
に渡します。ここで言うmemo_list.html
が Template です。(memo_list.html
はこの後作成します)View の設定は以上です。データを取得する際には条件を絞ったりすることも可能ですが、今回はシンプルイズベストで全て取得しちゃいます。データの取得方法も公式ドキュメントが参考になると思います。
Template の設定
いよいよ大詰めです。Template はデフォルトで用意されていないので自分で用意する必要があります。アプリ配下に templates というディレクトリを用意し、その中にさらに app1 というディレクトリを用意します。そして app1 の中に html ファイルを納めます。なぜこのような構図にするかということに関しては、naritoブログさんのこちらの記事が参考になりますので、ご参照ください。
myapp/templates/app1/memo_list.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Memo</title> </head> <style> .app-name { text-align: center; } .contents { margin-left: 300px; margin-right: 300px; } </style> <div class='app-name'> <h1><a href="">ぼく・わたしのメモ</a></h1> </div> <!-- メモの内容 --> {% for memo in memos %} <div class='contents'> <hr> <h2>タイトル : {{ memo.title }}</h2> <p>作成日 : {{ memo.created_date }}</p> <p>{{ memo.text | linebreaks }}</p> </div> {% endfor %} </html>ここでは HTML/CSS については触れず、先ほどの View から受け取った memos を如何に Template に表示させるかにフォーカスします。
- if, for 構文は {% %} で囲います。
memo.title
はメモのタイトルを、memo.created_date
はメモの作成日を、memo.text
はメモの内容をそれぞれ取得します。これらのデータを表示させるには {{ }} で囲う必要があります。linebreaks
はtext
内の改行を適宜 HTML タグに変換してくれます。ここまで来たらゴールはもうすぐそこです!
残すは URL の設定だけです!URL の設定
memo_list/
という URL を設定し、views.memo_list
に処理を振り分けるようにします。
app1/urls.py
from django.urls import path from . import views urlpatterns = [ path('', views.hello), path('memo_list/', views.memo_list), # 追加 ]それではいつものようにサーバを起動し、
http://127.0.0.1:8000/memo_list
にアクセスしましょう!
遂にやりましたね!
Model - Template - View がしっかりと手を取り合い、メモアプリを完成させることができました!おめでとうございます!
もちろんこれは初歩中の初歩中の初歩に過ぎませんし、物足りないと感じるのも無理はありません。
メモアプリといえば、
- 投稿・編集・削除
- 一覧表示
- 詳細表示
- 検索
...といった機能があるともっと便利ですよね。安心してください、今ここにあげたものは全て Django で実装できてしまうのです!ほら、Django のこともっと知りたくなったでしょ?さらに HTML/CSS を学んで装飾してあげれば、デザインにも富んだWebアプリケーションを作成できるのです!
最後に
さて、いかがだったでしょうか。この度は僭越ながら計4回に渡って Django のチュートリアルを書かせていただきました。本チュートリアルの目標である「 Django の仕組みを超絶ざっくりと理解し、Djangoって怖くないんだよ、面白いんだよ、と感じてもらう 」は達成していただけたのではないでしょうか。
学習開始当初の僕もきっと「Django 怖くない!面白い!」って言ってるはず。参考文献
- 投稿日:2020-05-23T05:10:56+09:00
DropdownのUIを実装
はじめに
JavaScriptのフレームワークやライブラリを使わずに JavaScript、HTML、そしてCSSでドロップダウンUIを実装 したので、そのメモです。
ちなみにスタイル用途として、CSSフレームワークbulmaとWebアイコンのfont-awesomeを利用しています。
DropwdownのUI動作確認
実際のDropdownのUIは以下のCodepensより確認できます。
See the Pen OJyqPmJ by shinji uyama (@ushinji_0612) on CodePen.
解説
HTML
まず初めにDropwDownのHTMLの解説です。今回のDropwdownのHTMLコードは、bulmaのコード例を参考にしています。
https://versions.bulma.io/0.7.1/documentation/components/dropdown/今回のDropdownのHTMLを簡略化すると以下の要素になります。
<!-- Dropwdown全体 --> <div class="dropdown is-active"> <div class="dropdown-trigger"> <button> <!-- ボタンの開閉ボタン --> </button> </div> <div class="dropdown-menu" id="dropdown-menu" role="menu"> <!-- ドロップダウンのMenu --> </div> </div>DropdownのMenuの表示/非表示は
<div class="dropdown">
に当てられているis-active
の有無で管理します。また、Dropdownの開閉UIは
<div class="dropdown-trigger">
配下のbutton
によって行います。Buttonクリックが行われた際はis-active
を追加 oris-active
を削除することで、開閉を実現しています。CSS
次にbulmaのCSSクラスの中で、Dropdownの開閉を行う
is-active
の挙動を解説します。まず初めに開閉対象であるMenuのCSSクラス
.dropdown-menu
を見るとdisplay: none;
が当てられています。そのため、デフォルトではブラウザ上では表示させないようにできます。一方で、
is-active
が追加することで.dropdown-menu
に対してdisplay: block
を当たるため、.dropdown-menu
要素が表示されます。<!-- ※ コードを一部省略しています --> .dropdown &.is-active .dropdown-menu display: block .dropdown-menu display: noneCSS詳細を知りたい方は、以下のリンクよりbulmaの該当コードを確認ください。
https://github.com/jgthms/bulma/blob/9a28ea17876715d00d0a8a59b9fdabfee967e56b/sass/components/dropdown.sass#L20JavaScript
Dropdownを開く場合
次にDropwdownの開閉を制御するJavaScriptについての解説です。
以下のコードがDropdownを開くコードになります。
document.addEventListener('DOMContentLoaded', function() { // 1. DOMが読み込まれた際に`.dropdown-trigger`のClassを持つHTMLElementを検索 var nodelist = document.querySelectorAll('.dropdown-trigger'); var elements = Array.prototype.slice.call(nodelist, 0); elements.forEach(function(element) { // 2. Dropdownの開閉ボタンを取得と、開閉を管理するDropdownのElementを取得 var button = element.querySelector('button'); var dropdown = element.parentNode; // 3. Dropdownの開閉ボタンがクリックされた際に、`is-active`クラスを追加するイベント追加 button.addEventListener('click', function() { dropdown.classList.add('is-active'); }); }); });処理の流れとしては、DOMがマウントされた際にDropdownに関連するHTMLに対してクリックイベントを登録することで、DropdownUIを実現させています。
具体的にはDropdownのTriggerとなるButtonがクリックされた際に
<div class="dropdown">
に.is-active
を当てることで、DropwdownのMenuを表示させています。Dropdownを閉じる場合
次に Dropdownを閉じるUIの処理 について説明します。
そもそもDropdownを閉じたいケースを考えると、以下の2つが考えられます。
1. DropdownのMenu項目がクリックされた場合
2. Dropdown以外の範囲がクリックされた場合1については、Menuのそれぞれの要素がクリックされた場合に、個別に閉じる処理を行う必要があります。また、Menuの要素が
<a>
タグの場合、クリック時にページ遷移が行われるのでDropdownが閉じる処理は考えなくても良いです。そのため今回は「2. Dropdown以外の範囲がクリックされた場合」にフォーカスして話をします。
onBlurを利用する
Dropdown以外の範囲がクリックされた場合を検知する一番簡単な方法は、TriggerであるButtonのfocusが外れた場合、つまりblurを検知 すれば良いです。
具体的には以下のコードを追記すれば大丈夫です。
document.addEventListener('DOMContentLoaded', function() { var nodelist = document.querySelectorAll('.dropdown-trigger'); var elements = Array.prototype.slice.call(nodelist, 0); elements.forEach(function(element) { var button = element.querySelector('button'); var dropdown = element.parentNode; button.addEventListener('click', function() { dropdown.classList.add('is-active'); }); // 【追記】 Dropdownを閉じるコード追記 + button.addEventListener('blur', function() { + dropdown.classList.remove('is-active'); + }); }); });この処理のメリットは一番簡単に実装できることです。一方で、focusが外れた際にDropdownを閉じるため、キーボード操作ではMenuを選択する前にDropdownMenuが消えてしまいます。そのため、より良いアクセシビリティを考えると、別の方法考える必要があります。
クリック位置を検知する
次は クリック位置を検知して、Dropdown Menu範囲外の場合にDropDownを閉じる方法 です。
document.addEventListener('DOMContentLoaded', function() { var nodelist = document.querySelectorAll('.dropdown-trigger'); var elements = Array.prototype.slice.call(nodelist, 0); // ※ Dropwdonwを開く処理は省略 // Dropdownを閉じる処理 // 1. window全体に対して、クリックイベントを登録 window.onclick = function(event) { elements.forEach(function(element) { var button = element.querySelector('button'); var dropdown = element.parentNode; // 2. aria-controlsより対象のDropdownのMenuを取得 var menu = document.querySelector('#' + button.getAttribute('aria-controls')); // 3. 自身のTriggerButtonクリック時はMenuを閉じない if(event.target && element.contains(event.target)) { return; } // 4. クリックがDropdownのMenuの範囲外の場合は、Dropdownを閉じる if(event.target && !menu.contains(event.target)) { dropdown.classList.remove('is-active'); } }); }; });処理の流れとして、window全体に対してクリックイベントを設定します。設定するクリックイベントは、クリック位置がDropdownのMenuの範囲内であるか調べ、範囲外の場合はドロップダウンを閉じるという処理となっています。
クリック範囲を調べる方法はincludesメソッドを利用します。これは指定したHTML要素の子要素の中に、目的の要素が含まれるか調べるメソッドになります。
今回は
event.target(クリック位置にあるHTML要素)
が、DropdownのMenuのHTML要素に含まれるか調べること、すなわりクリック位置がMenu範囲に含まれるかを調べることができます。また、クリック位置を調べる前に、そのクリックイベントがMenuを開くTrigger Buttonのイベントであるかチェックします。理由として、Menuを開くクリックイベント自体がMenu要素の範囲外であるため、以降の処理でMenuを閉じてしまうからです。そのため、Trigger Buttonのクリックイベントかどうか調べ、その場合は以降の処理を行わないようにしています。
最後に
今回自前でDropdownの開閉UIを実装することで、すごく勉強になりました。
簡単に実装する場合は
blur
を使えば良いですし、よりよいUIを目指すのであればクリック位置を判定する処理にすれば良いですね。今後もより良いUIを実装できるよう、日々努力していきたいです。
- 投稿日:2020-05-23T05:10:56+09:00
【js】DropdownのUIを実装
はじめに
JavaScriptのフレームワークやライブラリを使わずに JavaScript、HTML、そしてCSSでドロップダウンUIを実装 したので、そのメモです。
ちなみにスタイル用途として、CSSフレームワークbulmaとWebアイコンのfont-awesomeを利用しています。
DropwdownのUI動作確認
実際のDropdownのUIは以下のCodepensより確認できます。
See the Pen OJyqPmJ by shinji uyama (@ushinji_0612) on CodePen.
解説
HTML
まず初めにDropwDownのHTMLの解説です。今回のDropwdownのHTMLコードは、bulmaのコード例を参考にしています。
https://versions.bulma.io/0.7.1/documentation/components/dropdown/今回のDropdownのHTMLを簡略化すると以下の要素になります。
<!-- Dropwdown全体 --> <div class="dropdown is-active"> <div class="dropdown-trigger"> <button> <!-- ボタンの開閉ボタン --> </button> </div> <div class="dropdown-menu" id="dropdown-menu" role="menu"> <!-- ドロップダウンのMenu --> </div> </div>DropdownのMenuの表示/非表示は
<div class="dropdown">
に当てられているis-active
の有無で管理します。また、Dropdownの開閉UIは
<div class="dropdown-trigger">
配下のbutton
によって行います。Buttonクリックが行われた際はis-active
を追加 oris-active
を削除することで、開閉を実現しています。CSS
次にbulmaのCSSクラスの中で、Dropdownの開閉を行う
is-active
の挙動を解説します。まず初めに開閉対象であるMenuのCSSクラス
.dropdown-menu
を見るとdisplay: none;
が当てられています。そのため、デフォルトではブラウザ上では表示させないようにできます。一方で、
is-active
が追加することで.dropdown-menu
に対してdisplay: block
を当たるため、.dropdown-menu
要素が表示されます。<!-- ※ コードを一部省略しています --> .dropdown &.is-active .dropdown-menu display: block .dropdown-menu display: noneCSS詳細を知りたい方は、以下のリンクよりbulmaの該当コードを確認ください。
https://github.com/jgthms/bulma/blob/9a28ea17876715d00d0a8a59b9fdabfee967e56b/sass/components/dropdown.sass#L20JavaScript
Dropdownを開く場合
次にDropwdownの開閉を制御するJavaScriptについての解説です。
以下のコードがDropdownを開くコードになります。
document.addEventListener('DOMContentLoaded', function() { // 1. DOMが読み込まれた際に`.dropdown-trigger`のClassを持つHTMLElementを検索 var nodelist = document.querySelectorAll('.dropdown-trigger'); var elements = Array.prototype.slice.call(nodelist, 0); elements.forEach(function(element) { // 2. Dropdownの開閉ボタンを取得と、開閉を管理するDropdownのElementを取得 var button = element.querySelector('button'); var dropdown = element.parentNode; // 3. Dropdownの開閉ボタンがクリックされた際に、`is-active`クラスを追加するイベント追加 button.addEventListener('click', function() { dropdown.classList.add('is-active'); }); }); });処理の流れとしては、DOMがマウントされた際にDropdownに関連するHTMLに対してクリックイベントを登録することで、DropdownUIを実現させています。
具体的にはDropdownのTriggerとなるButtonがクリックされた際に
<div class="dropdown">
に.is-active
を当てることで、DropwdownのMenuを表示させています。Dropdownを閉じる場合
次に Dropdownを閉じるUIの処理 について説明します。
そもそもDropdownを閉じたいケースを考えると、以下の2つが考えられます。
1. DropdownのMenu項目がクリックされた場合
2. Dropdown以外の範囲がクリックされた場合1については、Menuのそれぞれの要素がクリックされた場合に、個別に閉じる処理を行う必要があります。また、Menuの要素が
<a>
タグの場合、クリック時にページ遷移が行われるのでDropdownが閉じる処理は考えなくても良いです。そのため今回は「2. Dropdown以外の範囲がクリックされた場合」にフォーカスして話をします。
onBlurを利用する
Dropdown以外の範囲がクリックされた場合を検知する一番簡単な方法は、TriggerであるButtonのfocusが外れた場合、つまりblurを検知 すれば良いです。
具体的には以下のコードを追記すれば大丈夫です。
document.addEventListener('DOMContentLoaded', function() { var nodelist = document.querySelectorAll('.dropdown-trigger'); var elements = Array.prototype.slice.call(nodelist, 0); elements.forEach(function(element) { var button = element.querySelector('button'); var dropdown = element.parentNode; button.addEventListener('click', function() { dropdown.classList.add('is-active'); }); // 【追記】 Dropdownを閉じるコード追記 + button.addEventListener('blur', function() { + dropdown.classList.remove('is-active'); + }); }); });この処理のメリットは一番簡単に実装できることです。一方で、focusが外れた際にDropdownを閉じるため、キーボード操作ではMenuを選択する前にDropdownMenuが消えてしまいます。そのため、より良いアクセシビリティを考えると、別の方法考える必要があります。
クリック位置を検知する
次は クリック位置を検知して、Dropdown Menu範囲外の場合にDropDownを閉じる方法 です。
document.addEventListener('DOMContentLoaded', function() { var nodelist = document.querySelectorAll('.dropdown-trigger'); var elements = Array.prototype.slice.call(nodelist, 0); // ※ Dropwdonwを開く処理は省略 // Dropdownを閉じる処理 // 1. window全体に対して、クリックイベントを登録 window.onclick = function(event) { elements.forEach(function(element) { var button = element.querySelector('button'); var dropdown = element.parentNode; // 2. aria-controlsより対象のDropdownのMenuを取得 var menu = document.querySelector('#' + button.getAttribute('aria-controls')); // 3. 自身のTriggerButtonクリック時はMenuを閉じない if(event.target && element.contains(event.target)) { return; } // 4. クリックがDropdownのMenuの範囲外の場合は、Dropdownを閉じる if(event.target && !menu.contains(event.target)) { dropdown.classList.remove('is-active'); } }); }; });処理の流れとして、window全体に対してクリックイベントを設定します。設定するクリックイベントは、クリック位置がDropdownのMenuの範囲内であるか調べ、範囲外の場合はドロップダウンを閉じるという処理となっています。
クリック範囲を調べる方法はincludesメソッドを利用します。これは指定したHTML要素の子要素の中に、目的の要素が含まれるか調べるメソッドになります。
今回は
event.target(クリック位置にあるHTML要素)
が、DropdownのMenuのHTML要素に含まれるか調べること、すなわりクリック位置がMenu範囲に含まれるかを調べることができます。また、クリック位置を調べる前に、そのクリックイベントがMenuを開くTrigger Buttonのイベントであるかチェックします。理由として、Menuを開くクリックイベント自体がMenu要素の範囲外であるため、以降の処理でMenuを閉じてしまうからです。そのため、Trigger Buttonのクリックイベントかどうか調べ、その場合は以降の処理を行わないようにしています。
最後に
今回自前でDropdownの開閉UIを実装することで、すごく勉強になりました。
簡単に実装する場合は
blur
を使えば良いですし、よりよいUIを目指すのであればクリック位置を判定する処理にすれば良いですね。今後もより良いUIを実装できるよう、日々努力していきたいです。
- 投稿日:2020-05-23T05:10:35+09:00
初心者によるプログラミング学習ログ 324日目
100日チャレンジの324日目
twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。
100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
324日目は、
おはようございます
— ぱぺまぺ@webエンジニアを目指したい社畜 (@yudapinokio) May 22, 2020
324日目 2h
・ポートフォリオ作成
・ふわっとでる演出追加#早起きチャレンジ#駆け出しエンジニアと繋がりたい#100DaysOfCode