20200523のCSSに関する記事は7件です。

テスト

会話アイコン.png

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

Vue.jsで作る!自動保存するToDoリスト~その2~Bootstrap編

初めに

前回の
Vue.jsで作る!自動保存するToDoリスト~その1~の続きです。
今回はBootstrapを使用して、前回作成したToDoリストの見た目を装飾していこうと思います。

目次

・このパートでの完成イメージ
・Bootstrapの記述
・classの解説
・まとめ

このパートでの完成イメージ

スクリーンショット 2020-05-23 10.50.23.png

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を模写以外で記述するのは初めてです。
ですが自分で書くことによってより一層理解が深まって一歩成長できたと思います!
この記事を見てくださったあなたの成長を応援させていただきます!!!

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

Django初学者の自分へ(4) - メモアプリ作成 -

チュートリアル一覧

No. タイトル
1 Django初学者の自分へ(1) - プロジェクト・アプリ -
2 Django初学者の自分へ(2) - MTVとは -
3 Django初学者の自分へ(3) - こんにちわ世界! -
4 Django初学者の自分へ(4) - メモアプリ作成 -

前回のおさらい

前回は Django で「こんにちわ世界!」を検証しました。
今回は MTV 全てを用いて、簡単なメモアプリを作成します!

レッツジャンゴ -メモアプリの作成-

今回の目標はメモアプリの作成を通じて、前回使用しなかった ModelTemplate の理解を深めることです。また、これまで学んだことを踏まえ、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 にアクセスします。すると以下のようなページに行きつくかと思います。
django_admin.PNG

ん?あれ、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_admin_page.PNG
これが 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)         # 追加

これらを記述したら、先ほどの管理者ページに戻ってページを更新してみてください。
django_admin_add_model.PNG
我が Memo アプリのモデルが確認できました!
それでは Memo ページに行き、右上の ADD MEMO からメモを作成してみましょう!
add_memo.PNG
皆さんはどんなメモを書きましたか?いやそんなことはどうでもよかったですね(笑)
メモのページに戻ると先ほどのメモが追加されているのが確認できるかと思います。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) : contextapp1/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 はメモの内容をそれぞれ取得します。これらのデータを表示させるには {{ }} で囲う必要があります。
  • linebreakstext 内の改行を適宜 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 にアクセスしましょう!
memo_app.PNG
遂にやりましたね!
Model - Template - View がしっかりと手を取り合い、メモアプリを完成させることができました!おめでとうございます!
もちろんこれは初歩中の初歩中の初歩に過ぎませんし、物足りないと感じるのも無理はありません。
メモアプリといえば、

  • 投稿・編集・削除
  • 一覧表示
  • 詳細表示
  • 検索
    ...

といった機能があるともっと便利ですよね。安心してください、今ここにあげたものは全て Django で実装できてしまうのです!ほら、Django のこともっと知りたくなったでしょ?さらに HTML/CSS を学んで装飾してあげれば、デザインにも富んだWebアプリケーションを作成できるのです!

最後に

さて、いかがだったでしょうか。この度は僭越ながら計4回に渡って Django のチュートリアルを書かせていただきました。本チュートリアルの目標である「 Django の仕組みを超絶ざっくりと理解し、Djangoって怖くないんだよ、面白いんだよ、と感じてもらう 」は達成していただけたのではないでしょうか。

学習開始当初の僕もきっと「Django 怖くない!面白い!」って言ってるはず。

参考文献

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

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を追加 or is-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: none

CSS詳細を知りたい方は、以下のリンクよりbulmaの該当コードを確認ください。
https://github.com/jgthms/bulma/blob/9a28ea17876715d00d0a8a59b9fdabfee967e56b/sass/components/dropdown.sass#L20

JavaScript

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を実装できるよう、日々努力していきたいです。

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

【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を追加 or is-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: none

CSS詳細を知りたい方は、以下のリンクよりbulmaの該当コードを確認ください。
https://github.com/jgthms/bulma/blob/9a28ea17876715d00d0a8a59b9fdabfee967e56b/sass/components/dropdown.sass#L20

JavaScript

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を実装できるよう、日々努力していきたいです。

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

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

100日チャレンジの324日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。
100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。
324日目は、

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

rails s で Renderedから動かなくなった。原因と解決までについて (grid-template)

エラーの原因

初めに、今回のエラーの原因と、どうやって解決したかについて書きます。
原因は cssファイルの grid-templateの記述ミスでした。
NGなcss
d3acc8a81392e422606a5e5337afa2b0.png
ここでgrid-templateの「;」の位置が最後の記述の下に来ているのが原因でした。

正しくは、
67796e390fb12e755bf217acc3a5aee2.png
のように、 grid-templateの記述を"footer"; と変更してアクセスできるようになりました。

grid-templateでエラーが出ているような記事などなかったためこれだけのエラーに半日使ってしまったので、
同じようなエラーで困った人の参考になれば幸いです

エラーの内容

rails sでサーバーを起動後 localhost:3000 にアクセスすると読み込み状態のままになった。
renderd ~~/~.html.erb within layouts/application
から動かず、ctrl+cも効かない状態になった。

どこが原因と考えたか

htmlファイルやコントローラーにエラーがある状態なら、エラー画面が表示された。
->rubyなどの環境が原因ではない。pcの環境ではない。と考えました。
->変更したファイルの中身が原因になる。

何を行ったのか

githubで新しくブランチを作成して、commitを一つずつファイルを確認した。
(git cherry-pick commit_id でcommitを参照)
cssファイルをコメントアウトした際、画面が表示されたのでcssの中身が原因だと判明。
順にコメントアウトを消して、grid-templateでエラーが出ていることが判明。
;の位置を変えて画面の表示を確認。

迷走の記録

初めのうちは、エラーの原因究明の方法がわからず迷走してました。
ちなみに、以下の記事を試しましたので記録として
https://qiita.com/sakuraniumarete/items/ac07d9d56c876601748c
https://teratail.com/questions/77742
https://teratail.com/questions/97670
https://b0npu.hatenablog.com/entry/2016/04/11/032826

また、今回よく使ったコマンドとしてkillコマンドを使いましたがその参考記事も
lsof -wni tcp:3000 から kill -9 killしたいPID
https://qiita.com/motty93/items/d22c1eb8f5128f8cd7f8

まとめ

grid-templateの「;」は誤った位置にあってもエラーが出ない時がある。

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