Web Componentsを試してみた

今回はWeb Componentsを使って実際にコードを書いていきたいと思います。

アイコンと文言がセットになったざーーっくりとしたプロフィール用のテンプレートを例に作っていきます。

いきなりですが、↓が全体のコードになります。 一つ一つ解説を交えながら進めていきます!

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
  </style>
</head>
<body>
  <profile-template></profile-template>
  <profile-template>
    <img src='images/jyojyo.jpg' slot="img" alt=''>
    <p slot="description">よちよち開発よちよち開発よちよち開発よちよち開発</p>
  </profile-template>
  <script>
  class Profile extends HTMLElement {
    static get template(){
      return `
      <style>
        .profile {
          display: flex;
          align-items:center;
          justify-content: space-around;
        }
        .profile_img {
          width:200px;
        }
        img, #img::slotted(*) {
          width:200px;
        }
      </style>
       <div class="profile">
        <p class="profile_img">
          <slot name="img" id="img">
            <img src="images/riki.jpg">
          </slot>
        </p>
        <slot name="description">滑舌の悪いおじさん滑舌の悪いおじさん滑舌の悪いおじさん</slot>
       </div>
    `;
    }
    constructor(){
      super();
      this.attachShadow({
        mode: 'open'
      }).innerHTML = Profile.template;
    }
  }
  customElements.define('profile-template', Profile);
  </script>
</body>
</html>

-------以下から解説-------

class Profile extends HTMLElement {
//省略
}

ここではextends HTMLElementHTMLElement を継承します。

constructor(){
 super();
   this.attachShadow({
     mode: 'open'
   }).innerHTML = Profile.template;
}

attachShadow を利用することでShadow DOMを有効にしています。その後innerHTMLでstyleとHTMLを突っ込んでいます。

<div class="profile">
  <p class="profile_img">
    <slot name="img" id="img">
      <img src="images/riki.jpg">
    </slot>
  </p>
  <slot name="description">
    滑舌の悪いおじさん滑舌の悪いおじさん滑舌の悪いおじさん
  </slot>
</div>

htmlのコードの中にslotという文字があります。 Shadow DOM は、 要素を使用して複数の異なる DOM ツリーを合成します。これを使って自由に独自のマークアップを入れることができます。

customElements.define('profile-template', Profile);

customElements.defineでCustom Elementを定義しています。 第一引数が実際に使用する要素のタグ名で、第二引数はクラス名になります。ブラウザにCustom Elementだと認識させるためにタグ名はダッシュをつけます。

ブラウザに表示させた結果はこちらになります。(スタイルがイケてなくてすみません。。。)

f:id:top_men:20180401040354p:plain

まだ一部のブラウザ標準でまだ使用することはできないので、未対応のブラウザにも Web Componentsを使用できるためのPolyllを導入することが必要になると思います。

github.com

Web Componentsとは

今回はWeb Componentsについて自分の理解も深めるために書いていきます。

Web Componentsについては公式サイトにおいて下記のように説明されています。

Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Custom components and widgets build on the Web Component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.

簡単にいうとWeアプリケーションなどで共通となる部品を独自のタグを使っていつでも呼び出せる(使える)ようにしようといった意味になります。

headerタグやfooterなどは何を意味しているのか視覚的にわかりますが、divタグとか見ても作った本人ならまだしも、他の人がそのコードを見た時にどのような意味なのか実際に表示されたものと照らし合わせて見ないとわかりずらいですよね?

ReactVueなどは共通部品となるものなどをコンポーネント化してそれぞれ役割に合った命名をするかと思います。また大切なのはそれぞれが独立しても機能するために、HTML/CSS/JavaScriptなどを一つのスコープとしてコンポーネント化します。そうすることで例えばCSSのグローバルスコープの汚染問題を回避することができます。

また更に重要なこととしてWeb Componentsはブラウザの標準の機能を使用するので特別なビルド環境などが不要になります。

ではここでWeb Componentsを構成する4つの要素について説明していきたいと思います。

1 Custom Elements

Custom ElementsとはHTMLのタグに独自の名前をつけることができるものです。すでにこのことについては言及しましたが、divタグなどがたくさんある時よりもheaderfooterなど役割をもたせ、視覚的にわかりやすい方がコーディングがしやすいです。 文書構造に意味付けとして非常に大切な要素だと思います。

2 Shadow DOM

CSS スコープ、DOM カプセル化コンポジションなどを活用し、Custom Elementsで新しいHTMLを作成します。したがってShadow DOMだけでェブ コンポーネントを作成するわけではありません。 詳細を知りたい方はこちらを参考にしてみてください。

developers.google.com

3 HTML Imports

CSSJavaScriptなどを含むHTMLファイルを一つのモジュールとしてロードすることができる仕組みです。

4 HTML Template

普段使用しているタグなどを<template>要素で囲い、テンプレートを宣言します。宣言するだけでは実際に描画されません。JavaScriptなどを使用することでテンプレートを描画します。

Web Componentsは上の4つの仕様から構成されています。 実際に手を動かしてみないと実感が湧かないと思いますので次回はコードを例にしながらWeb Componentsの理解を深めていこうと思います。

jQueryを使わずES2015でスライダー対応のポップアップテンプレートをつくってみた。

今回はスライダー対応のポップアップテンプレートを作成してみました。簡単なポップアップのテンプレートが欲しい場合はこちらを参考にしてみてください!

top-men.hatenablog.com

スライダー対応のポップアップのイメージはこちら になります。

ライブラリに依存するることは学習コストが掛かるので極力使いたくはありません。ですがプロジェクトによってはスケジュールやメンバーなどを考慮して使用することが正しいときもあるでしょう。

ですが、今回はjQueryなどを使わずに流用できるものを紹介します。

早速ですがコードを見てみましょう。

<!DOCTYPE html>
<html lang='en'>
<head>
   <meta charset='UTF-8'>
   <meta name='viewport' content='width=device-width, initial-scale=1.0'>
   <meta http-equiv='X-UA-Compatible' content='ie=edge'>
   <title>Document</title>
   <link rel='stylesheet' href='css/style.css'>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.min.js"></script>
</head>
<body>
  <h1>ポップアップデモ</h1>
    <p>画像をクリック</p>
    <p><img src='images/img_01.jpg' alt='' class="popup_image"></p>
    <p>大海原だぁぁ</p>
    <p><img src='images/img_02.jpg' class="popup_image" alt=''></p>
    <p>海もいいけど山菜食べたい</p>
    <p>山登ろう</p>
    <p>頂上の風景綺麗だな</p>
    <p><img src='images/img_03.jpg' alt='' class="popup_image"></p>
    <p>気づけばもう冬かぁ</p>
    <p><img src='images/img_04.jpg' alt='' class="popup_image"></p>
    <!---ポップアップ-->
    <div class="popupSlider" id="popupWrap">
        <div class="popupSlider_content">
            <div class='closeBtn' id="js-closeBtn">X</div>
            <p class="poupSldier_content_arrow prev" id="sliderPrevArrow"></p>
            <p class="popupSlider_content_img"><img src='' alt='' class="" id="poupSliderImg"></p>
            <p class="poupSldier_content_arrow next" id="sliderNextArrow"></p>
        </div>
    </div>
    <!-- <script src="js/Popup.js"></script> -->
    <script src="js/PopupSlider.js"></script>
</body>
</html>
img {
    max-width: 100%;
}

.popupButton {
    border: none;
    background: #323232;
    color: #fff;
    padding: 20px;
    border-radius: 33px;
    cursor: pointer;
}
.popup {
    width: 100%;
    height: 100%;
    position: fixed;
    top: 0;
    left: 0;
    background: rgba(5,5,5,0.8);
    opacity: 0;
    transition:all .3s;
    transform: scale(0);
}

.popup.is-active {
    transform: scale(1);
    opacity: 1;
}

.popup_content {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #fff;
    width: 400px;
    height: 360px;
    padding: 30px;
}

.popup_content_closeButton {
    cursor: pointer;
    text-align: right;
}

.popup_content_description {
    margin-top: 30px;
}

.popup_image {
    width: 400px;
    cursor: pointer;
}

/* ポップアップのスライダー用のクラス*/

.popupSlider {
    width: 100vw;
    height: 100vh;
    position: fixed;
    opacity: 0;
    top: 0;
    left: 0;
    background:rgba(0, 0, 0, .6);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: -1;
    transition: all .3s;
}

.popupSlider.is-active {
    opacity: 1;
    z-index: 1000;
}

.popupSlider_content {
    display: flex;
    width: 600px;
    justify-content: center;
    align-items: center;
    position: relative;
    padding: 30px;
    background: #fff;
}

.popupSlider_content_img {
    width:100%;
    padding: 0 20px;
}

.poupSldier_content_arrow:before {
    content: "◀";
    position: absolute;
    display: block;
    color: #fff;
    display: flex;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
}

.poupSldier_content_arrow {
    width: 80px;
    height: 60px;
    background: #000;
    cursor: pointer;
    margin: 0;
    border-radius: 50%;
    padding: 0;
    position: relative;
}

.poupSldier_content_arrow.next {
    transform: rotate(180deg);
}

.closeBtn {
    position: absolute;
    top: 10px;
    right: 40px;
    position: absolute;
    top: 10px;
    right: 15px;
    border-radius: 50%;
    border: 1px solid #323232;
    padding: 10px;
    width: 20px;
    height: 20px;
    background: #323232;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
}
class PopupSlider {
  constructor(imageElements) {
    this.el = document.getElementById("popupWrap");
    this.imgEl = document.getElementById("poupSliderImg");
    this.closeBtn = document.getElementById("js-closeBtn");
    this.allImgElms = imageElements;
    this.setEventListeners();
  }

  togglePopupState(flag) {
    if (flag) {
      this.el.classList.add("is-active")
    } else {
      this.el.classList.remove("is-active")
    }
  }

  changeImage(src) {
    this.imgEl.setAttribute("src", src);
  }

  slideNext(src) {
    this.changeImage(src);
  }

  slideBack(src) {
    this.changeImage(src);
  }

  setEventListeners() {
        for(let i = 0; i < this.allImgElms.length; i++) {
      this.allImgElms[i].addEventListener("click", e => {
        const clickImgSrc = e.target.getAttribute("src");
        this.imgEl.setAttribute("src", clickImgSrc)
        this.togglePopupState(true);
      });
    }
    this.closeBtn.addEventListener("click", e => {
      this.togglePopupState(false);
    })

    document.getElementById("sliderNextArrow").addEventListener("click", e => {
      const currentSrc = document.getElementById("poupSliderImg").getAttribute("src");
      for(let i = 0; i < this.allImgElms.length; i++) {
        const src = this.allImgElms[i].getAttribute("src");
        if (src === currentSrc) {
          if (this.allImgElms.length <= i+1) {
            this.slideNext(this.allImgElms[0].getAttribute("src"));
            return
          };
          this.slideNext(this.allImgElms[i+1].getAttribute("src"));
        }
      }
    });

    document.getElementById("sliderPrevArrow").addEventListener("click", e => {
      const currentSrc = document.getElementById("poupSliderImg").getAttribute("src");
      for(let i = 0; i < this.allImgElms.length; i++) {
        const src = this.allImgElms[i].getAttribute("src");
        if (src === currentSrc) {
          if (Array.from(this.allImgElms).indexOf(this.allImgElms[i]) <= 0) {
            this.slideBack(this.allImgElms[this.allImgElms.length -1].getAttribute("src"));
            return;
          };
          this.slideBack(this.allImgElms[i-1].getAttribute("src"));
        }
      }
    });
  }
}

const imageElements = document.getElementsByClassName("popup_image");
const slider = new PopupSlider(imageElements);

const toArray = (htmlCollection) => {
  Array.prototype.slice.call(htmlCollection);
}

ここでテンプレートとして使用するために気をつける箇所いくつかあります。

  • ポップアップ&スライドショーで表示させたいimgタグにpopup_imageクラスを付与することです。
const imageElements = document.getElementsByClassName("popup_image");

上記のコードのクラス名をご自身の好きなクラス名にしていただいても構いません!

またArray.fromとコードの中で出てきていますが、こちらは配列型のオブジェクトから新しい配列インスタンスを生成しています。 ここでは配列にしたデータの方が扱い易くなるからです。

ポップアップによってスタイルを変更したい場合は、cssなどで調整してみてください。

簡単ではありますが以上になります。

Node+Mysqlでブラウザにデータを表示させてみる

NodeMysqlを使ってブラウザにデータを表示させてみようと思います。

大まかな流れとしましては

  1. mysqlに接続
  2. データを取得
  3. viewファイルにデータを渡す
  4. viewファイルでデータの整形をして表示させる

ディレクトリ構成はこのようになります。

root
 ├ index.js
 ├ views
 |  ├ css
 |     style.css
 |  └ index.pug
 └ package.json

まずはじめにプロジェクトフォルダを作成し、移動します。

そこでnpm init -yと実行し、package.jsonファイルが作成されます。

その後、必要なパッケージをインストールします。

npm i -D mysql pug express

{
  "name": "express_sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "express": "^4.16.3",
    "mysql": "^2.15.0",
    "pug": "^2.0.1"
  }
}

index.jsファイルにmysqlのからテーブルのデータの取得、ルーティングの設定をしたいと思います。

const express = require('express');
const app = express();
const mysql = require('mysql');

const connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'hoge',
  password : 'password',
  database : 'sample_db'
});

app.set('view engine', 'pug');
app.use(express.static('views'));

const recordLog = function (req, res, next) {
  console.log('localhost:3000にアクセスしました');
  next();
};

app.use(recordLog);

app.get('/', function (req, res) {
  let sql = 'select * from sample_db.personal';
  connection.query(sql, (err, rows, fields) => {
    if (err) throw err;
    res.render('index', { title: 'node + mysql practice', PesoralDatas: rows});
  });
});

app.listen(3000);

あらかじめmysqlにデータベースとテーブルを作成しておきました。

f:id:top_men:20180321232045p:plain

データベース名はsample_dbでテーブル名がpersonalとなります。

ファイルの中身を解説していきます。

const connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'hoge',
  password : 'password',
  database : 'sample_db'
});

host・・・データベースのホスト名
user・・・データベースにログインしているユーザー名
password・・・パスワード
database・・・データベース名

app.set('view engine', 'pug');
app.use(express.static('views'));

こちらhtmlのテンプレートエンジンとしてpugを使用することを宣言しています。 静的アセットディレクトリーをviewsと指定しています。

app.get('/', function (req, res) {
  let sql = 'select * from sample_db.personal';
  connection.query(sql, (err, rows, fields) => {
    if (err) throw err;
    res.render('index', { title: 'node + mysql practice', PesoralDatas: rows});
  });
});

localhost:3000でアクセスした時にsample_dbにあるpersonalテーブルに接続します。 その後、index.pugファイルにこちら側で指定したタイトルの値とカラムのデータを渡しています。

viewsディレクトリ直下にindex.pugファイルを作成します。

html
  head
    title #{title}
    link(rel="stylesheet" href="css/style.css")
  body
    h1 Person Datas
    each person in PesoralDatas
      .person
        p.person__id id #{person.id}
        p.person__old old #{person.old}
        p.person__name name #{person.name}

pugではeach文が使用できるので、受け取ったデータを整形して表示させます。

cssファイルはこちらになります。

.person {
    border: 1px solid #323232;
    padding: 10px;
}
.person + .person {
    margin-top: 30px;
}

これで準備が整いました。
プロジェクトフォルダ直下でnode index.jsと実行してください。

その後、localhost:3000にアクセスしてみてください。

f:id:top_men:20180321235230p:plain

このように表示できれば成功です!! フロントからデータベースまで一通り触ることができました。

全体を通じてexpressは本当に手軽に利用できるものだと思いました。ルーティングからローカルサーバーの起動、テンプレートエンジンの設定(nodeで動いているのでejsも利用可能)など視覚的でわかりやすいです。

mysqlは全く触ったことがなかったのですが、ユーザーを作成し、実際にテーブルにカラムデータを入れるところまで行いました。

使用したコマンドを備忘録として残しておいたのでよかったら参考にしてみてください。

top-men.hatenablog.com

簡単ではありますが以上になります。

mysqlコマンドの備忘録

今後業務でmysqlを使用していくことが多くなってくると思うので、備忘録としてよく使うコマンドを書いていこうと思います。

  • ログイン中のユーザーを確認する

select USER();
  • テーブルの中のカラムを確認する

desc テーブル名;
  • ユーザーを追加する

create user 'ユーザー名'@localhost identified by "パスワード";
  • 作成したユーザーでMySQLサーバへ接続する

mysql -u "ユーザー名" -p
  • データベースの作成

create database データベース名;
  • データベースの一覧確認

show databases;
  • データベースの選択

use データベース名;
  • ユーザーに権限追加

grant {権限の内容} on {権限の対象} to {ユーザー名}@{ホスト名} identified by {パスワード}
  • データベースにあるテーブルを参照する時

show tables from データベース名;
  • テーブルの削除

drop table テーブル名
  • テーブルの中身を確認

select * from テーブル名
  • データベースの削除

drop database `データベース名`
  • mysqlで接続しているデータベースサーバのホスト名を確認する方法

show variables like 'hostname';

まだ学び始めたばかりなので今後更に追加していきたいと思います! 簡単ではありますが以上になります。

ExpressでテンプレートエンジンPugを利用してみる

今回はExpresspugを使ってみようと思います。

まずプロジェクトディレクトリを作成してください。

mkdir express_practice

その後、npm init -y

package.jsonが作成されます。

その後expressをインストールします。

npm i -D express

今回はpugを使用するので

npm i -D pug

viewフォルダにindex.pugファイルを用意します。

html
  head
    title!= title
  body
    h1!= message

プロジェクトフォルダ配下にapp.jsファイルを追加します。

const express = require('express');
const app = express();
app.set('view engine', 'pug');

const recordLog = function (req, res, next) {
  console.log('localhost:3000にアクセスしました');
  next();
};

app.use(recordLog);

app.get('/', function (req, res) {
  res.render('index', { title: 'Hello', message: 'Express'});
});


app.listen(3000);

res.render('index', { title: 'Hey', message: 'Hello there!'});

↑ではindex.pugで設定した変数に値がそれぞれ入ります。

その後、node app.jsと実行後、ブラウザでlocalhost:3000にアクセスしてみてください。

f:id:top_men:20180318002323p:plain

このように表示されればOKです!

簡単ではありますが以上になります。

今後はmysqlを使ってデータのやりとりをしてみようと思います。

ReactのFragmentsって便利

今回はReactの開発において今後積極的に使っていこうと思ったものを紹介したいと思います!

タイトルにも書いてある通り、Fragmentsです。

言葉で説明するよりも下記のコードを見た方が理解し易いです。

import * as React from "react";

class Columns extends React.Component {
  render() {
    return (
      <div>
        <td>データ1</td>
        <td>データ2</td>
      </div>
    );
  }
}

ここで<td>タグのリストをレンダリングしたいのに余計なノードをDOMに追加しなけばいけません。階層が余計に深くなったりルートの要素に余分なスタイルを当てなければいけない時などが出てくる可能性があります。

もやもやしますよね・・・

React v16 からはこの忌まわしき問題から解放されます!!

新たにFragmentを読み込みます。

このように書くことで上記の問題を回避することができるのです!簡単ですね!

import React, { Fragment } from 'react'

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>データ1</td>
        <td>データ2</td>
      </React.Fragment>
    );
  }
}

レンダリングされた結果がこちらになります。

<td>データ1</td>
<td>データ2</td>

また別の記述の仕方があります。

import React, { Fragment } from 'react'

class Columns extends React.Component {
  render() {
    return (
      <>
        <td>データ1</td>
        <td>データ2</td>
      </>
    );
  }
}

こちらの書き方はまだサポート他のツールでサポートされていないようなので<React.Fragment>を使用することが推奨されています。

簡単ではありますが、是非活用してみてください!