20191202のCSSに関する記事は14件です。

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

100日チャレンジの174日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

174日目は

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

大きく表示するタイマーが欲しかったからdivのborderで数字を描いてみた

はじめに

divのborderで数字を描いてみた話です。

やりたかったこと

小学校の教室でタイマーを大きく表示したい。

背景

月に数回程度、小学校にお邪魔してK3Tunnelを使ったプログラミング授業をお届け しています。

普通の授業時間にお邪魔するので、時間厳守。
時間厳守しないと給食時間や休み時間が短くなってしまいます。
出張授業なので、45分×2コマの1本勝負。
続きはまた今度ね。は許されません。

私たちがやっているプログラミング授業は、シナリオが用意された体験モノです。そのため、ここまでは到達してほしい、ここの面白さを感じてほしいというポイントがあります。

なので、時には、作業途中の子どもたちの手を止めて「次」に移ってもらったり。
ゆっくりゆっくりやっている子のペースアップを促したりすることもあります。

が。こんな感じのこどもたちを目にしたら。
kids_doro_asobi_s.png

それがなかなか難しい。

言いにくいことはAIに言わせる。

これ。エンジニアの常識です。

時間が来たよと機械的に伝える。

終了時間を指定して時計を見てもらうだけでは弱い。
タイマーをセットして突然音を鳴らすのは心臓に悪い。
子どもたち自身のペースメイクを促したい。

タイマーを大きく誰もが見える場所に表示するのがベスト。

というのが私の結論でした。

対応方針

物理的な時計やタイマーアプリなどには目もくれず、つくる。いいから作る。
nichiyoudaiku_s.png

作成方針

実装に使うモノについて学習しない。
なぜならJavaScript/html/cssで何か作りたい熱が高まったのがトリガーで、「今日は時間が取れる」休日がぽっと生まれたときに作ることにしたから。

1日でぱぱっとつくれることが重要。
慣れない言語に手を出すなど論外。
初めてのライブラリやフレームワークに挑戦するとかもなし。

だがしかし。つくる。

つくったもの

できあがりはこちらで確認できます。
https://cocoakamen.github.io/cocoa-timer/

構想する

学習せずに作るといっても、特大フォントで時間表示するだけでは、いくらなんでも芸がない。
少しくらいはチャレンジ要素を入れたい。

と。

しばし考えて、思いついた。

ひょっとして。

divのborderを太くしたら、デジタル時計っぽくなるのでは?

とりあえず、このHTMLで、上下と左右で色を変えたdiv(縦横100pxで太さ20px)を表示してみた。

<html>
  <div style="border-color:black gray; height:100px; width:100px; border-style:solid; border-width:20px;"></div>
</html>

結果。
timer-try.jpg

デジタル数字ができそうな気がする見た目をしている。

作る

divを並べる

こんな感じにdivを並べることに。太枠のボックスが数字を描こうとしているdiv。
timer-div.jpg
各数字、二つのdivを使うイメージ。
箱の中に書いてあるのはid名。
id名が書いていないのは、スペース調整用だったりなんだり。
classがいっぱいあるのは、スタイル設定するための色々。
もしかしたら使っていないclassもあるかもしれない。。

数字を表示しているところのHTMLはこちら。
上下それぞれ横に並べるのが少しややこしい。

index.html
<div class="timer-container">
  <div class="number-row">
      <div class="digital-number number-left number-up" id="number-minute-left-up"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-up" id="number-minute-right-up"></div>
      <div class="digital-separater" id="separater-up"></div>
      <div class="digital-number number-left number-up" id="number-second-left-up"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-up" id="number-second-right-up"></div>    
  </div>
  <div class="number-row">
      <div class="digital-number number-left number-down" id="number-minute-left-down"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-down" id="number-minute-right-down"></div>
      <div class="digital-separater" id="separater-down"></div>  
      <div class="digital-number number-left number-down" id="number-second-left-down"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-down" id="number-second-right-down"></div>    
  </div>  
</div>

borderのスタイルを決めていく

文字の太さ

cssの:root 疑似クラスカスタムプロパティで定義。

timer.css
:root {
 --number-line-width: 30px;
}

digital-numberクラスにベースの設定をカキカキ。(数字を描くdiv要素は全部digital-number)

timer.css
main .digital-number{
  border: solid;
  width:200px; 
  height:170px;
  border-width: var(--number-line-width); /* :root要素で設定しているカスタムプロパティ参照 */
  box-sizing: border-box; /* パディングとボーダーを幅と高さに含める */
}
/* 全体的にmain要素で囲っているのでmainがついてます */

上と下が重なり合うところは、それぞれ幅を2分の1にします。
こういうとき、カスタムプロパティを定義しておくと便利です。

timer.css
main .number-up {
  border-bottom-width: calc( var(--number-line-width)/2);
}

main .number-down {
  border-top-width: calc( var(--number-line-width)/2);
}

文字の色

文字の色は、JavaScriptの中で指定。
コンストラクタ定義でベタ打ち定義。

timer.js
function CocoaTimer() {
  this.numberColor = 'black'; // 文字の色
  this.GRAY_OUT_COLOR = '#EFEFEF'; // 線がない部分の色
  this.remainingTime = 0;
  this.timer = {};
};

数字をかく

表示する数字ごとに色を指定します。
こんなイメージ。
number_color.jpg

あとは地道に定義していきました。引数は数字を描くdivの上と下の要素。

timer.js
CocoaTimer.prototype.drawNumber = function(upElement, downElement, num) {
  // 中略
  // 数字
  switch(num) {
    case 0:
      upElement.style.borderColor = this.numberColor;
      upElement.style.borderBottomColor = this.GRAY_OUT_COLOR;
      downElement.style.borderColor = this.numberColor;
      downElement.style.borderTopColor = this.GRAY_OUT_COLOR;
      break;
    case 1:
      upElement.style.borderColor = this.GRAY_OUT_COLOR;
      upElement.style.borderRightColor = this.numberColor;
      downElement.style.borderColor = this.GRAY_OUT_COLOR;
      downElement.style.borderRightColor = this.numberColor;
      break;
    // 中略 以下9まで地道に定義
      break;
    default:
      break;
  }
};

時間は普通に計算

時間はミリ秒で計算。
残り時間のミリ秒から、分、秒それぞれ1の位と10の位の数字を求める計算をsetInterval使って実行して再描画。詳細は省略。
時間になったら点滅。今のところ音はなりません。

ソースはこちらにも

https://github.com/cocoakamen/cocoa-timer

子どもたちの反応

授業で実際に使った時の反応はおおむね「やばい爆弾だ。爆弾。」
jigenbakudan_s.png
なんですが、残念ながら、爆弾イフェクトは実装されておりません。

将来ゲームクリエイターになりたいんだ♪という子に、あのタイマー、私がつくったんだよと自慢したところ、尊敬のまなざしを得られた気がします。

もちろん、狙い通り、子どもたち自身が残り時間を意識している様子も見受けられ、ほくほくです。

まとめ

タイマーを導入したら、子どもたちのフォローをするときに、時間を気にする脳ミソ使用量が減った気がします。ワークショップの進行がとてもラクになるのでお勧めです。

ちなみに、自分の子の小学校授業参観にいったら、物理タイマーを実物投影機で大きいディスプレイに映してました。

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

Mixinを使う

Mixinって何に使うやつ?

  • 内部にCSSのプロパティ群を持つことができる。
  • 共通のCSSを一元管理したい時に便利。

SCSSでかく

SCSS
//mixin設定
@mixin hoge($argmentWidth: 10px) { //引数を渡さない場合は$argmentWidthは10px(デフォルト値)
  width: $argmentWidth;
  .add {
    @content; //mixin使用先でプロパティを記述したい場合に使用
  }
}

//mixinを使う対象
.target1 {
  @include hoge(5px) {
    //@contentに代入
    display: inline-block;
  }
  height: auto;
}

.target2 {
  @include hoge {
    display: inline;
  }
  height: 0;
}

CSSにすると…

CSS
.target1 {
  width: 5px;
  height: auto;
}

.target1 .add {
  display: inline-block;
}

.target2 {
  width: 10px;
  height: 0;
}

.target2 .add {
  display: inline;
}

こんな場所に使う

レスポンシブのメディアクエリに

SCSS
//mixin設定
@mixin sp($breakPoint: 768px) {
  @media screen and (max-width: $breakPoint) {
    @content;
  }
}

//mixinを使う対象
.target {
  max-width: 960px;
  @include sp(560px) {
    max-width: 100%;
  }
}
CSS
.target {
  max-width: 960px;
}

@media screen and (max-width: 560px) {
  .target {
    max-width: 100%;
  }
}

ブレークポイントを弄るのはあまりよくないかも…
引数のところは変数とかで共通化しよう。

最後に

みなさんMixinをどのように使っていますか…?

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

【WEB版 WEAR】position: sticky で似ているアイテム検索の使いやすい UI/UX を実現する

この記事はZOZOテクノロジーズ #2 Advent Calendar 2019 19日目の記事になります。
昨日は、@saitoryuji さんによる「新卒2年目のエンジニアが単体テストをやってみる」でした。

また、今年は全部で5つのAdvent Calendarが公開されています。

はじめに

最近、スマートフォンのWEB版 WEARで、コーデ画像をもとにAIがアイテムを検出し、さらにそのアイテムに似ているアイテムを検出して一覧で表示する機能をリリースしました:tada:
真似したいコーデの服が簡単に見つかる!

この機能をWEBで実装するにあたり、使いやすいUI/UXを目指した奮闘記を紹介します!

こんなのをやりたい

まず、大まかな要件は以下の通りです。

  1. コーデ画像の左下の検索アイコンをタッチすると検索スクリーンが下から上がってくる
  2. コーデ画像からアイテムが検出された箇所を「◯」で表示する
  3. スクリーン下部に、選択されたアイテムの「似ているアイテム」の一覧を表示する
  4. スクロールすると「似ているアイテム」の一覧が上がってくる
  5. 「似ているアイテム」の一覧はヘッダーが固定され、一覧内でスクロールできる

コーデ詳細.png → デフォルト.png → スクロール.png → 一覧表示.png

最初にこの要件を聞いたとき、経験上難しそうだなーと感じたのは、
windowのスクロールとは別に、一覧内もスクロールできるようにしないとけないところでした。
実装自体は可能なのですが、スクロールできる要素の中にスクロールできる要素を配置すると、スクロールしたい方じゃない方がスクロールされるなど、とても使いにくいイメージがありました。。。:sweat:

今回は上記の要件のうち、1、4、5の部分について説明します。
※以下に出てくるソースコードは、説明用のサンプルです。

ボツになった案

それでも、まずは思いついたままに、以下のような構成で実装してみた。
(最初に言っておきますが、これはボツです笑)

実装

HTML
<div id="searchScreen" style="position: relative; transform: translateY(0px);">
    <div id="image" style="position: absolute;">...</div>
    <div id="list">
        <div id="listHead"></div>
        <div id="listBody" style="height: 812px; overflow-y: scroll;">
            <ul>
                <li></li>
                :
            </ul>
        </div>
    </div>
</div>

See the Pen Sample1 by mitanih (@mitanih) on CodePen.

#searchScreen
コーデ画像の検索アイコンボタンのクリックイベントで、下から上がってくる検索スクリーン
下から上げる処理として、CSSアニメーションでtransformtranslateY(100%)からtranslateY(0px)に切り替えて表現する
※marginの切り替えでも表現できるが、残像ができたり動きがカクつくなどの問題があった

#image
検索スクリーン内上部のイメージエリア
windowがスクロールされても固定したいので、position: absoluteを指定して、常に画面の同じ位置にいるようJavaScriptで制御する
※親要素にtranslateを指定しているのでposition: fixedは使えない

#list
検索スクリーン内下部の一覧

#listHead
一覧のヘッダー

#listBody
一覧のボディ
一覧内をスクロールさせるためheightを設定し、overflow-y: scrollを指定

問題点

触ってみて、やっぱり使いにくかった。。。:sweat_smile:
iOSのSafariで動作確認した範囲ですが、windowをスクロールさせて一覧を上に持っていきたいのに、一覧内がスクロールされてしまったり、慣性スクロールの影響なのか、windowがスクロールして完全に止まらないと、一覧内がスクロールできないなどの現象も起きた。
スクロールしやすいように工夫して、一覧が一番上まで来ていなければ一覧内はスクロールできないようJavaScriptで制御も入れてみたが、スムーズな動きとはいかなかった。
あと、#imageの位置を固定するためスクロールの度にJavaScriptで位置を計算して設定したが、フラフラ動いてなんか変。
何より無理し過ぎな気がしました。。。:sweat:

採用した方法

いや、もっといい方法はあるはずだ。他のサービスでこんなぎこちないの見たことないぞ。。。:thinking:
と、いろいろ調べていて、position: stickyに辿り着きました!
Excelの行固定みたいな機能が簡単に実現できるあのスタイルです。

position: stickyの説明
https://developer.mozilla.org/ja/docs/Web/CSS/position

メジャーなiOS、Androidのブラウザ対応も問題なさそう!
https://caniuse.com/#search=sticky

実装

HTML
<div id="searchScreen" style="position: relative; transform: translateY(0px);">
    <div id="image" style="position: sticky;">...</div>
    <div id="list">
        <div id="listHead" style="position: sticky;"></div>
        <div id="listBody">
            <ul>
                <li></li>
                :
            </ul>
        </div>
    </div>
</div>

See the Pen Sample2 by mitanih (@mitanih) on CodePen.

細かい調整は省略しますが、要は#imageと#listHeadにposition: stickyを設定するだけで解決しました!
windowのスクロールで#listが上に来るまでは、#imageがstickyしてくれる。#listが上までくると#listHeadがstickyし、windowのスクロールでそのまま#listBodyが下まで見られる。
まさに理想的なスタイル!
無理していない実装なので動きもスムーズです!:blush:

まとめ

JavaScriptやCSSのプロパティを覚えると、自力でゴリゴリ書けちゃうんですが、実機で確認すると動きがぎこちなくてなんか無理してる感じがすることってあると思います。
そんなときは視点を変えてもっとシンプルな方法を探すと、よりよい解決策が見つかるということに気付かされます。
プログラミングって、そんなことのくり返しのような気がする。。。

明日は、@ikkou さんによる「WebAR の現状確認 2019 Winter」です。
お楽しみに~!

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

Flaskで自然言語処理を使ったWebアプリを作りherokuでデプロイした

説明

Flaskで英文を入れると時制を解析するWebアプリを作りました。

環境

Python3がインストールされていることとGitHubで作成していることを前提に進めます。

Flaskアプリケーションの作成

ディレクトリ構成

.
├── static
│   ├── js
│   │   └── main.js
│   └── css
│       └── style.css
├── templates
│   └── index.html
├── main.py
└── analyzer.py

htmlを作成

templates/index.html
<!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>TENSE ANALYZER</title>
</head>

<body>
    <div id="container">
        <div id="title">
            <h1>TENSE ANALYZER</h1>
        </div>
    </div>
</body>

</html>

Flaskのインストール

pip install Flask

main.pyを作成

main.py
from flask import *

app = Flask(__name__)


@app.route("/")
def init():
    return render_template('index.html')


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    app.run(host='127.0.0.1', port=5000, debug=True)

アプリケーションを立ち上げる

python main.py

http://127.0.0.1:5000/ を開いて"TENSE ANALYZER"の文字が表示されていれば成功。

フォームを作る

htmlにフォームを追加

templates/index.html
...
<div id="container">
        <div id="title">
            <h1>TENSE ANALYZER</h1>
        </div>

        <div id="analyze">
            <h2>英文を入力してEnterキーを押してください</h2>
            <form id="form" action="/input" method="post" name="formStr">
                <input id="textField" class="input" type="text" name="str" value="{{value_str}}">
                <input class="submit" type="submit" name="" value="送信">
            </form>
        </div>

        <div class="result">
            <h2>結果</h2>
            <p>{{value_str}}</p>
        </div>
    </div>
...

formタグにaction="/input"
inputタグにvalue="{{value_str}}", name="str"
の属性を追加。

フォームから送信した値を受け取る処理をmain.pyに追加

flask_socketioをインストール
pip install flask_socketio

main.py
from flask import *
from flask_socketio import SocketIO

app = Flask(__name__)
socketio = SocketIO(app, async_mode=None)  # [変更] socketioを追加

value_str = ""  # [変更] value_strを定義


@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    return render_template('index.html', value_str=value_str)

#  action="/input"
@app.route("/input", methods=["GET", "POST"])
def get_form():
    global value_str
    # フォームの値を受け取る
    try:
        value_str = request.form['str']  # name="str"のinputタグの値を取得
    # ページ読み込み時
    except:
        value_str = ""
    # index.html内にて{{ value_str }}で挿入できる
    return render_template('index.html', value_str=value_str)


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    socketio.run(app, host='127.0.0.1', port=5000, debug=True)  # [変更] 


index.htmlの<p>{{value_str}}</p>の部分にフォームから送信した値が表示されれば成功。

NLPで解析部分を実装

今回はNLTK(Natural Language Toolkit)というライブラリを使用しました。

NLTKをインストール

pip install nltk

analyzer.pyを作成

analyzer.py
analyzer.py
import re
import nltk
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')


class Analyzer:
    def tense_analyze(self, value_str):
        if(len(value_str) > 0 and "." != value_str[-1]):
            value_str = value_str+"."
        s = value_str
        morph = nltk.word_tokenize(value_str)
        result = nltk.pos_tag(morph)
        tense = ''
        q = 0
        will = re.search('will', value_str)
        for i in range(len(s.split())):
            b = int(i)
            if result[b][1] != 'VBP' and result[b][1] != 'VBZ' and result[b][1] != 'VBG' and result[b][1] != 'VB' and result[b][1] != 'VBD' and result[b][1] != 'VBN':
                q = q+1
            elif (result[b][1] == 'VBP' or result[b][1] == 'VBZ' or result[b][1] == 'VBD' or result[b][1] == 'VB') and (result[b][0] != 'am' and result[b][0] != 'are' and result[b][0] != 'was' and result[b][0] != 'were' and result[b][0] != "be" and result[b][0] != 'is'):
                if result[b][0] == 'have' or result[b][0] == 'has' or result[b][0] == 'had':
                    if result[b+1][1] == 'VBN' or result[b+2][1] == 'VBN' or result[b+1][1] == 'VBD' or result[b+2][1] == 'VBD':
                        if result[b+1][0] == 'been':
                            if result[b+2][1] == 'VBG' or result[b+3][1] == 'VBG':
                                tense = 'PerfectContiuous'
                                break
                            else:
                                tense = 'Perfect'
                                break
                        else:
                            tense = 'Perfect'
                            break
                    else:
                        tense = 'Simple'
                        break
                else:
                    tense = 'Simple'
            elif result[b][1] == "VBP" or result[b][0] == "was" or result[b][0] == "were" or result[b][1] == "VBZ" or result[b][0] == "be" or result[b][0] == "is" or result[b][0] == "VBD":
                if result[b+1][1] == 'VBG':
                    tense = 'Continuous'
                    break
                else:
                    tense = 'Simple'
                    break

        for j in range(len(s.split())):
            c = int(j)
            if tense == 'Simple':
                if result[c][1] == 'VBD':
                    tense = 'PastSimple'
                    break
                elif result[c][1] == 'VBD' and (result[c-1][1] == 'VBP' or result[c-1][1] == 'VBD' or result[c-1][1] == 'VBZ'):
                    tense = 'PastSimple'
                    break
                elif (not will) and result[c][1] == "VBP" or result[c][1] == "VBZ":
                    tense = "PresentSimple"
                elif will and (result[c-1][1] != 'VBP' or result[c-1][1] != 'VBD' or result[c-1][1] != 'VBZ'):
                    tense = "FutureSimple"
                else:
                    q = q+1
            elif tense == "Perfect":
                if result[c][0] == 'had':
                    tense = 'PastPerfectSimple'
                    break
                elif not will and (result[c][0] == "have" or result[c][0] == "has"):
                    tense = "PresentPerfectSimple"
                elif will and result[c][0] == "have":
                    tense = "FuturePerfectSimple"
            elif tense == "Continuous":
                if result[c][0] == 'was' or result[c][0] == 'were':
                    tense = 'PastContinuous'
                    break
                elif (not will) and result[c][1] == "VBP" or result[c][1] == "VBZ":
                    tense = "PresentContinuous"
                elif will:
                    tense = "FutureContinuous"
                else:
                    q = q+1
            elif tense == "PerfectContiuous":
                if result[c][0] == 'had':
                    tense = "PastPerfectContinuous"
                elif not will and (result[c][0] == 'have' or result[c][0] == "has"):
                    tense = 'PresentPerfectContinuous'
                elif will:
                    tense = "FuturePerfectContinuous"
            else:
                q = q+1
        value_str.strip()

        return value_str, tense

Analyzerクラスに文章の時制の手がかりとなる部分と時制を返す関数def tense_analyze(self, value_str):を作成。
アルゴリズムの説明等は省略します。

main.py内でanalyzer.pyをimportして使う

main.py
from flask import *
from flask_socketio import SocketIO
# analyzer.pyをimport
import analyzer

app = Flask(__name__)
socketio = SocketIO(app, async_mode=None)

# [削除]value_str = ""
result = []  # [変更] 結果を格納する配列


@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    # [変更] value_str -> result
    return render_template('index.html', result=result)

#  action="/input"
@app.route("/input", methods=["GET", "POST"])
def get_form():
    global value_str
    # フォームの値を受け取る
    try:
        value_str = request.form['str']  # name="str"のinputタグの値を取得
    # ページ読み込み時
    except:
        value_str = ""

    # tense_analyze関数の実行
    Analyzer = analyzer.Analyzer()
    value_str, tense = Analyzer.tense_analyze(value_str)

    # 結果をresultに追加
    appendList = []
    if(value_str):
        appendList.append(value_str)
    if(tense):
        appendList.append(tense)
    result.append(appendList)

    # [変更] value_str -> result
    # index.html内にて{{ result }}で挿入できる
    return render_template('index.html', result=result)


if __name__ == "__main__":
    # debugはデプロイ時にFalseにする
    socketio.run(app, host='127.0.0.1', port=5000, debug=True)  # 変更


index.htmlでresultを表示する。

Python用のテンプレートエンジンJinja2をインストール
pip install Jinja2

Jinja2をインストールすることでhtml内でif文やfor文が使えます。
Jinja2のドキュメント

templates/index.html
...
  <div class="result">
            <h2>結果</h2>
            <!-- result配列の長さが0より大きい場合表示 -->
            {% if result|length > 0 %}
            <div class="result-content">
                <div class="items">
                    <!-- resultの中身ををループで表示(ここではreverseで逆順にしています) -->
                    {% for r in result|reverse %}

                    <!-- r[0](文章)が存在するなら表示する -->
                    {% if r[0] %}

                    <div class="item">
                        <p class="value_str">{{ r[0] }}</p>

                        <!-- r[1](時制)が存在するなら表示する -->
                        {% if r[1] %}
                        <p class="tense">{{ r[1] }}</p>
                        {% endif %}
                        <!--  -->
                    </div>

                    {% endif %}
                    <!--  -->

                    {% endfor %}
                    <!--  -->
                </div>
            </div>
            {% endif %}
            <!--  -->
        </div>
...

リセットボタンを作成

index.html

templates/index.html
...
<div class="result-content">
        <form id="reset" action="/reset" method="post">
                <button name="reset_result">結果をリセットする</button>
        </form>
<div class="items">
...

formにaction="/reset"属性を追加

main.py

main.py
...

@app.route("/")
def init():
    # value_strをhtml側で使えるようにする
    return render_template('index.html', result=result)

#  action="/reset"
@app.route("/reset", methods=["GET", "POST"])
def reset_result():
    global result
    result = []
    return render_template('index.html', result=result)


#  action="/input"
@app.route("/input", methods=["GET", "POST"])
...


CSSとJavaScript

CSS

style.css
static/css/style.css
/* css reset */

body {
    font-family: 'Inconsolata', monospace;
    background-color: #fdfeff;
    display: flex;
    padding: 0 1rem;
}

h1, h2, h3 {
    color: rgb(70, 70, 70);
    margin-bottom: 0;
}

h1 {
    font-size: 4vw;
}

h2 {
    font-size: 3vw;
}

h3 {
    font-size: 2vw;
}

p {
    margin: 0;
    color: dimgrey;
    font-size: 1.6vw;
    font-weight: bold;
}

ul, li {
    padding: 0;
}

li {
    list-style-type: none;
    font-size: 1.6vw;
    font-weight: bold;
    color: dimgrey;
}

li::before {
    content: "・";
}

input {
    background: white;
}

input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active {
    -webkit-text-fill-color: rgb(70, 70, 70);
    -webkit-box-shadow: 0 0 0px 1000px white inset;
    box-shadow: 0 0 0px 1000px white inset;
}

input:focus {
    outline: 0;
}

button {
    color: dimgrey;
}

button:hover {
    cursor: pointer;
}

button:focus {
    outline: 0;
}

/*  */

/* container */

#container {
    margin: auto;
    width: 80vw;
}

@media screen and (max-width:768px) {
    #container {
        width: 90vw;
    }
}

#container>div {
    margin-bottom: 2rem;
}

/*  */

/* nlp */

#nlp {
    /* border-radius: 5px; */
    /* background: #34456314; */
}

#form {
    margin: 0.5rem 0;
}

#form .input {
    height: 25px;
    width: calc(100% - 15px);
    padding: 5px;
    margin-left: 1px;
    background: transparent;
    font-size: 1.8vw;
    border: 2px solid rgb(70, 70, 70);
}

#form .submit {
    display: none;
}

.result-content {
    margin: 0.5rem 0;
    padding: 5px 10px;
    background-color: #f1f1f1;
    border-radius: 5px;
}

.result-content #reset {
    padding: 5px 0;
}

.result-content #reset button {
    margin: auto 0 0 0;
    padding: 5px 10px;
    display: block;
    border: 1px solid #fdfeff;
    background: #fdfeff;
    border: 2px solid dimgray;
    border-radius: 5px;
    font-size: 1.6vw;
    font-weight: bold;
    transition: all 0.3s ease;
}

.result-content #reset button:hover {
    background: dimgrey;
    color: #fdfeff;
}

.result-content .items {
    overflow: scroll;
    max-height: 50vh;
}

.result-content .items .item {
    display: flex;
    flex-wrap: wrap;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid #34456314;
    margin: 1rem 0;
}

.result-content p {
    padding: 5px 0;
    font-size: 1.8vw;
}

.result-content .tense {
    background: #fdfeff;
    border-radius: 15px;
    padding: 5px 10px;
}

/*  */

#task li {
    color: aliceblue;
    padding: 5px;
    display: inline-block;
    border-radius: 5px;
    background: #344563;
}

JavaScript

static/js/main.js
// ウィンドウ読み込み時にフォームにfocusする
window.onload = function () {
    document.formStr.str.focus()
}

html内で読み込む

html内でcssとJavaScriptを読み込むのを忘れずに

templates/index.html
<head>
...
        <script src="{{ url_for('static', filename='js/main.js') }}"></script>
        <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>

Herokuにデプロイ

ディレクトリ構成

.
├── static
│   ├── js
│   │   └── main.js
│   └── css
│       └── style.css
├── templates
│   └── index.html
├── main.py
├── analyzer.py
├── Procfile
└── requirements.txt

Procfileを作成

gunicornをインストール

pip install gunicorn

Procfile

Procfile
web: gunicorn [ファイル名]:app --log-file=-

mainの部分は自分で作ったファイル名に合わせる(今回はmain.pyだからmain)

Procfile
web: gunicorn main:app --log-file=-

requirements.txtを作成

以下のコマンドを実行すると、requirements.txt何に必要なライブラリ等が自動的に書き込まれる。

$ pip freeze > requirements.txt

requirements.txt
autopep8==1.4.4
Click==7.0
Flask==1.1.1
Flask-SocketIO==4.2.1
gunicorn==20.0.4
itsdangerous==1.1.0
Jinja2==2.10.3
MarkupSafe==1.1.1
nltk==3.4.5
pycodestyle==2.5.0
python-engineio==3.10.0
python-socketio==4.4.0
six==1.13.0
Werkzeug==0.16.0

Herokuに登録する

このページでアカウントを作成する。

Herokuをインストールする

brew tap heroku/brew && brew install heroku

Herokuにログインする

heroku login

Herokuアプリを作成

heroku create [アプリ名]

今回は、tenseにします。

heroku create tense


$ heroku create tense
Creating ⬢ tense... !
 ▸    Name tense is already taken

既に使用されてたため変更

heroku create english-tense-analyzer


$ heroku create english-tense-analyzer
Creating ⬢ english-tense-analyzer... done
https://english-tense-analyzer.herokuapp.com/ | https://git.heroku.com/english-tense-analyzer.git

デプロイ

以下のコマンドを実行するだけで完了です

git push heroku master

heroku openを実行するとブラウザでWebアプリケーションが開きます。

終わりに

感想

使用してみて、とても軽く、簡単に手軽に作れるという点で学習コストが低く、使いやすいフレームワークだと思いました。
機械学習を使ってWebアプリケーションを作りたい方や少し試したいという方には丁度いいかと思います。
Djangoなどの他のPythonのWebフレームワークはまだ使用したことがないので試してみたいです。

参考

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

元バックエンドのコーダーがSassに感動した話

はじめに

こんにちは。
surimi_panと申します。

昨年までバックエンドエンジニアとして勤めていたのですが、
ある日を境にフロントエンドのコーダーに変身しました。
業務内容が変わったことで新しい用語や技術が次々入ってきて毎日が新発見とカルチャーショックの連続です。

今回はSassについてお話させていただきます。

序章

「CSSは.cssファイルを直接編集したことなら...」

前述の通り↑程度の知識だった当初の私がCSSに抱いていた印象といえば、

  • 同じようなプロパティを複数の要素に記入する時は何度も書く
  • 色を変えるときは関連箇所全て洗い出して変えないといけない
  • ファイルの規模が大きくなるほどCSSファイルが長くなって編集が大変
  • どの箇所を編集しているのか、gitで確認しにくい

...といったもので、
バックエンド時代に「変更に強いコードを」「共通箇所はまとめる」「変更箇所は少なく抑える」
と学んでいた私は、CSSに対して「変更に弱い」印象を抱くようになっていました。

そこでSassに出会ったわけですが...。

Sassとは

Sassとは、CSSの拡張メタ言語(言語を作成するためのの言語)のことです。

スタイルを記述する点においてはCSSと共通していますが、構文に違いがあり、
要素の入れ子や関数などのCSSには備わっていない機能を利用します。
そうして作成されたファイルをコンパイラに通すことによって、CSSファイルが生成されます。

Sassの構文には2種類の記法があり、インデントを利用する「SASS」記法と、
波括弧やセミコロンを利用してCSSに似た書き方ができる「SCSS」記法が利用できます。

SASS記法
.test
  display: flex
  align-items: center

  a
    color: #2b2b2b
SCSS記法
.test {
  display: flex;
  align-items: center;

  a {
    color: #2b2b2b;
  }
}

↓ どちらも同じ内容のCSSが生成されます。

CSS
.test {
  display: flex;
  align-items: center;
}

.test a {
  color: #2b2b2b;
}

何に感動したか

Sassにはいろんな便利機能があるのですが、特に感動したのは以下の3つです。

  1. 変数
  2. mixin
  3. ファイル分割

変数

文字や数値、色などを変数として保存することができます。

$main-color: #ccc;

.news-list {
  width: 450px;
  background-color: $main-color;
}
.banner-list {
  min-height: 50px;
  background-color: $main-color;
}

mixin

スタイルから使いまわしたい箇所を分離することができる機能で、引数も設定できます。
「外側だけ共通化して内部のコンテンツは個別に記入させる」ということも可能で、
こちらを利用することでメディアクエリを共通化する、といった使い方もできます。

@mixin smartphone() {
  @media screen and (min-width: 768px) {
    @content;
  }
}

@include smartphone {
  // 内容
}

ファイル分割

複数のSassファイルを結合する@importという機能があります。

_part.scss
.test-contents {
  display: inline-block;
}

@import による参照時、アンダースコア( _ )と拡張子(.sassまたは.scss)は省略できます。

main.scss
@import "part"

#container {
  display: block;
}

↓ コンパイル

main.css
.test-contents {
  display: inline-block;
}

#container {
  display: block;
}

どこに感動したか

これらのSassの機能を利用することでCSSを変更に強くすることができます。

変数、mixinを利用して共通箇所を一か所にまとめることで、
同じ内容を何度も書くリスク(記入ミス、仕様変更時の修正箇所増大、等々...)を減らすことができ、
ファイルを分けることで、gitで管理した際にどの部分を編集したのかを管理しやすくできます。

その結果、
変更が発生した場合でも修正箇所を見つけやすく、修正時のヒューマンエラーが発生しにくい、
変更に強いCSSができあがります。

まとめ

CSSを知っていながらSassを知らなかった人は他にもいるはず...と思い記事を作りました。
どうか同じ境遇の方に届いてほしいと願っています。

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

relativeとabsolute

ポジションを操作できる

relativeとは...(相対的)

自身がいる位置を基準にして移動する

absoluteとは...(絶対的)

左上を基準にして移動
スタート位置が決まっています。

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

ハローハリネズミ - Browserslist でターゲットブラウザの設定

はじめに

今年 6 月に Autoprefixer が 9.6 にバージョンアップしました。(執筆時点の最新バージョンは 9.7.3 になります。)その中で browsers オプションが非推奨になりました。次の 10.0 のリリースを機に browsers オプションを廃止する予定です。今回は、browsers オプションに代わる Browserslist について紹介します。

Autoprefixer とは

Autoprefixer は、ターゲットブラウザにベンダープレフィックスを自動的に追加する CSS ポストプロセッサ1です。ちなみに、Autoprefixer の 9.7 で Browserslist でのパフォーマンス改善が行われました。詳しくはこちらを参照ください。

browsers オプション廃止理由

当初、Autoprefixer にはターゲットブラウザを設定する browsers オプションがありました。しかし、多くのツール(e.g. Babel)にもターゲットブラウザを必要とします。これが Browserslist の誕生、browsers オプションの廃止理由になります。

Browserslist とは

Browserslist は、異なるツール(e.g. Babel)間でターゲットブラウザのバージョンを共有するための設定ファイルです。

設定方法

Browserslist でターゲットブラウザを設定する方法は 2 通りありますが、どちらも非常に簡単です。

  1. package.json で設定する方法
  2. .browserslistrc で設定する方法

今回は 1. の package.json で設定する方法 を紹介します。下記のような記述を package.json に記述しましょう。

package.json
{
  "browserslist": [
    "last 2 versions",
    "ie >= 11",
    "android >= 4.4.4",
    "not dead"
  ]
}

設定は以上になります。ちなみに、ターゲットブラウザのバージョンを確認するには下記のコマンドを実行しましょう。

$ npx browserslist

まとめ

今回は Autoprefixer に焦点を当てて Browserslist について解説しました。Browserslist は Autoprefixer 以外のツールでも使用できます。先述したように Autoprefixer 10.0 で browsers オプションが廃止予定です。今年の非推奨は今年のうちに推奨にしましょう。


  1. CSS ポストプロセッサとは、純粋な CSS をより良い CSS に最適化する処理を施すものを指します。 

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

HTMLコーディングで良く使うwebサービス様

HTMLのコード整形ツール

はちゃめちゃ大混乱なHTMLを治す時。<button>とか<a>はインデントされないので、その辺は手動で調整。
https://lab.syncer.jp/Tool/HTML-PrettyPrint/

Base64エンコーダー

Emmetでもできるけど、プロジェクト内に画像を移動するのすら面倒な時にここでエンコードしてそのままcssにぶち込む。
https://lab.syncer.jp/Tool/Base64-encode/

beforeやafter疑似要素のcontentプロパティで日本語の文字化けを回避する方法

cssの content:""; 内で文字化け防ぐ用。
https://webllica.com/css-content-property-mojibake/

CSS三角形作成ツール

吹き出しパーツデザイン用。
http://apps.eky.hk/css-triangle-generator/ja

SVGデータをCSSのbackground-image向けのBase64コードにかんたん変換ツール

background-imageにSVG使いたいよ
https://blog.s0014.com/posts/2017-01-19-il-to-svg/

CSS3のtransition-timing-functionの値、cubic-bezier()に関して

jquery Easingとか使ってる時、CSS側でもアニメーション揃えたい時
http://www.knockknock.jp/archives/184

Placehold.jp

ダミー画像とりあえず入れたいよ
http://placehold.jp/

HTMLタグ除去

テキストだけ抜いてサイトの調査とかしたいよ
https://crocro.com/tools/item/del_html_tag.html

Geocoding - 住所から緯度経度を検索

場所の緯度経度確認したいとき
https://www.geocoding.jp/

正規表現

どれが何やったっけなるとき
http://shanabrian.com/web/regular_expression.php

You might not need jQuery

いい加減脱jqueryしたいとき
http://youmightnotneedjquery.com/

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

【中央寄せ】text-align: center;とmargin: 0 auto;の違い・使い分け

【結論】

インライン要素には、text-align: center;

ブロック要素には、margin: 0 auto;



そもそもインライン要素・ブロック要素とはなんぞやという方はこちらが参考になります。

https://saruwakakun.com/html-css/basic/display


text-align: center;を使う時

ただのテキストもしくは、
aタグimgタグなどのインライン要素が中央に寄る。

※親要素にあてること!

xxx.html
<div class="manbow">
  <a href="...">マンボウ</a>
</div>
xxx.css
.manbow {
  text-align: center;
}

aタグはdiv要素の中で中央に寄りました。


margin: 0 auto;を使う時

divタグpタグなどのブロック要素が中央に寄る。

※中央寄せしたい要素本体にあてること!

xxx.html
<div class="manbow">
  <a href="...">マンボウ</a>
</div>
xxx.css
.manbow {
  width: 100px;
  margin: 0 auto;
}

ブロック要素は基本的に横いっぱいに広がっています。
それだと中央に寄らないので、widthで幅を指定してあげます。

注意したいのは、div要素自体は中央に寄りましたが、中のリンクは中央に寄っていない点です。
div要素を中央に寄せ、更に中のリンクもその中で中央に寄せたい場合はtext-align: center;をあてる必要があります。

xxx.css
.manbow {
  width: 100px;
  margin: 0 auto;
  text-align: center;
}

すべてが中央に寄りました。


まとめ

text-align: center;

・インライン要素が中央に寄る
・親要素にあてる

margin: 0 auto;

・ブロック要素が中央に寄る
・要素本体にあてる
・要素の幅を指定してあげる



参考

https://saruwakakun.com/html-css/basic/centering



ではまた!

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

最近勉強して、知った言葉まとめてみた

はじめに

私が最近、知った言葉を、自分なりにまとめてみました。参考にしてもらえると、嬉しいです:relaxed:

ハッシュ化

パスワードを、保管する際に用いられる手法。
平文のパスワードからハッシュ値と呼ばれる値を求め、そのハッシュ値という値でパスワードを保管する手法。暗号化とは違い、不可逆変換であり、元の平文のパスワードに戻すことが不可能である。

データベースのNOTNULL制約

データベースに、nullを入れることを許容しない制約。制約をつけることにより、nullが入るとエラーが起きることにより、nullを入れることが出来なくなる。データベースの必須項目などに付けるといいと思います。

キーワード引数(Ruby)

関数を作った際の引数の末尾に : をつけることで、関数を呼び出す際にどの引数に、どの数や文字、変数を渡すのか指定することが出来る。引数が多くなった場合などに便利。

CSSセレクタ

CSSを指定することにより、要素を取得できる。多くのセレクタの種類があり、それらを組み合わせることにより、細かいとこまで指定して、要素を取得できる。おすすめチートシート

最後に

説明が曖昧ですが、もっと上手く説明できるように精進致します。

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

LPを作るときに役立ちそうなTips集

この記事は、 ドワンゴ Advent Calendar 2019 の21日目の記事です。

はじめに

2017年はエンジニアさんで埋められるカレンダーの中、わりと緊張しながらデザイナーの僕も「コンポーネント指向フロントエンド開発におけるデザイナーの参画について」という記事を書くことができました。

記事としても反響を頂き、登壇までさせていただくなど、大変大きな一歩になる記事でした。

さて、あれから2年経ったわけですが、 今年の9月にニコニコ生放送の開発を離れ、特設ランディングページ(以下LPと表記)やポータルサイトなどを作る部署に異動しました

異動による環境/技術スタックの変化

部署の異動により、環境が大きく変化しました

ニコニコ生放送

  • 規模の大きなプロダクト・チーム
  • プラットフォームのUIデザイン
  • React/TypeScript/scss/css-modules
  • 堅牢な機能開発・ワークフロー

現在の部署

  • ペライチ ~ 3ページ、多くても5テンプレートの軽量~ポータル程度のページを2人作業
  • グラフィックデザイン、演出、アニメーションやインタラクションを重視
  • レスポンシブが標準
  • ejs/scss/vanila js + babel/jQuery ~ Nuxt(Vue)
  • イベント合わせの納期・期間限定の時間の制約、変更が大きい短期開発

のようになり、大きく環境が変わりました。

環境が新しくなったことにより、まだ異動してわずか3ヶ月満たないぐらいですが、非常に多くの知見を得ました。
今回はそのなかでも特に有用そうな技術的Tipsを、実例とともに細々と紹介していきます。

Intersection Observer API

LPでは、要素がウィンドウ内に表示されたときにアニメーションする というようなことがとても多くあります。
今まで、これを書くには「jQueryなどでscrollTopとウィンドウサイズとその要素の位置関係を計算」したり、in-view.jsなどのライブラリを使うことが多かったと思います。

よく出てくる割には、計算が面倒だったり、ライブラリを探す手間、scroll/resizeイベントの抜け漏れ、イベントの発生回数によるパフォーマンスの低下など厄介なことが多くありました。

今ではこれは、Intersection Observer APIというブラウザ標準のAPIとして提供されており、これによってかなり楽に実装できるようになり、イベントの発生回数もscroll/resizeと比べるとかなり少なく、パフォーマンスも良くなります。
基本的にIE以外はすでに実装されている上、polyfillも準備されており、スムーズに導入できます。

使用例: 「画面内に入ってきたらフェードインさせる」をサクッと書く

HTML

index.html
<div class="foo-section">...</div>
<div class="bar-section">...</div>

SCSS

will-change を入れる必然性はないですが、LPでは面積の大きな要素を動かすことも多く、なめらかなアニメーションによる快適な体験が求められるので、僕は will-change も書いておくことが多いです。

style.scss
@mixin fade {
  transition: opacity .4s ease, transition .4s ease;
  will-change: opacity, transition;

  &[data-intersecting="false"],
  &:not([data-intersecting="true"] {
    opacity: 0;
    transform: translateY(32px);
  }

  &[data-intersecting="true"] {
    opacity: 1;
    transform: translateY(0);
  }
}

.foo-section {
  @include fade;
}

.bar-section {
  @include fade;
}

JavaScript(es6/babel環境)

polyfillは npm install intersection-observer などで入れておくと便利です

script.js
// polyfillの読み込み
require('intersection-observer');

// observeしたときのaction定義
const action = (entry) => {
  // entry.target に検出したDOMが入ってくる
  // entry.isIntersecting に画面内かどうかが入ってくる
  entry.target.setAttribute('data-intersecting', entry.isIntersecting);
}

// IntersectionObserverの利用
const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => { 
    action(entry);
  });
});

// observeするDOMを取得
// 他の要素にも適応したい際は、ここにどんどん追加しておくだけでOK
const targets = [
  document.querySelector('.foo-section'),
  document.querySelector('.bar-section')
];

// observeする
targets.forEach(target => {
  observer.observe(target);
});

これだけでよくあるアニメーションをサクッと実装することができます。なれると何も見ずに5分程度で作ることができ、要素の追加/削除にも柔軟に適応できるため非常に重宝しています。

LPに限らず、Reactのようなコンポーネント指向な世界でも、scrollやresizeはwindowのようなglobalなものを持ちがちになってしまうので、このようなAPIがあるのは非常に嬉しいことだと思います。

ECSSによるHTML/CSSのワークフロー

LPでは、書き捨てで納期も短いものも多く、要素の変更にも柔軟に対応する必要があるため、厳密で堅牢や再利用性というよりは、終盤で最低限秩序が崩れない(namespaceがある/変更で崩れないよう要素とスタイルが1:1対応である)、理解が容易なものが良いです。

そのため、BEMよりラフ(だと思っている)で冗長なECSSを採用しています。

ECSSの概要と考え方のまとめ が非常にわかりやすいですが、要点は以下の1行です。

namespace-ComponentName_ChildNode-variant

現在は namespace = ページ名を2文字にしたprefixとして採用しています。

使用例: カンプからHTMLをおこし、SCSSファイルを作成する

僕がコーディングを担当したページではないため、実際とは異なりますが、良いサンプルだったので ハレスタ の以下の部分(大人の事情で画像はぼかしました)をHTMLに起こしてみようと思います。

スクリーンショット

topページなので、namespaceは tp- とします。
RootのComponentNameは ScheduleSection とします。

schedule-section.ejs
<section class="tp-ScheduleSection">
  <div class="tp-ScheduleSectionBackground" aria-hidden="true">
    <div class="tp-ScheduleSectionBackground_DecorationText">Pick up</div>
    <img class="tp-ScheduleSectionBackground_Image" src="" alt="">
  <div>
  <header class="tp-ScheduleSectionHeader">
    <h2 class="tp-ScheduleSectionHeader_Heading-en">Schedule</h2>
    <div class="tp-ScheduleSectionHeader_Heading-ja" aria-hidden="true">スケジュール</div>
  </header>
  <div class="tp-ScheduleContent">
    <section class="tp-SchedulePickUpContainer">
      <div class="tp-SchedulePickUpThumbnail">
        <img class="tp-SchedulePickUpThumbnail_Image" src="<%= list[i].imageUrl %>" alt="">
        <div class="tp-SchedulePickUpThumbnail_Filter">
      </div>
      <div class="tp-SchedulePickUpInfo">
        <time class="tp-SchedulePickUpDateTime" datatime="略">
          <span class="tp-SchedulePickUpDateTime_Date">
            <span class="tp-SchedulePickUpDateTime_Month"><%= list[0].date.month %></span>
            <span class="tp-SchedulePickUpDateTime_DateSeparator">/</span>
            <span class="tp-SchedulePickUpDateTime_Day"><%= list[0].date.day %></span>
          </span>
          <span class="tp-SchedulePickUpDateTime_WeekDay">(<%= list[0].date.weekday %>)</span>
          <span class="tp-SchedulePickUpDateTime_Time"><%= list[0].date.startTime %><%= list[0].date.endTime %></span>
        </time>
        <h3 class="tp-SchedulePickUpTitle"><%= list[0].title %></h3>
        </div>
    </section>
    <div class="tp-ScheduleSubContainer">
      <% for(let i = 1; i < list.length; i++) { %>
        <section class="tp-ScheduleSubContainer">
          <div class="tp-ScheduleSubThumbnail">
            <img class="tp-ScheduleSubThumbnail_Image" src="<%= list[i].imageUrl %>" alt="">
            <div class="tp-ScheduleSubThumbnail_Filter">
          </div>
          <div class="tp-ScheduleSubInfo">
            <time class="tp-ScheduleSubDateTime" datatime="略">
              <span class="tp-ScheduleSubDateTime_Date">
                <span class="tp-ScheduleSubDateTime_Month"><%= list[i].date.month %></span>
                <span class="tp-ScheduleSubDateTime_DateSeparator">/</span>
                <span class="tp-ScheduleSubDateTime_Day"><%= list[i].date.day %></span>
              </span>
              <span class="tp-ScheduleSubDateTime_WeekDay">(<%= list[i].date.weekday %>)</span>
              <span class="tp-ScheduleSubDateTime_Time"><%= list[i].date.startTime %><%= list[i].date.endTime %></span>
            </time>
            <h3 class="tp-ScheduleSubTitle"><%= list[i].title %></h3>
          </div>
        </section>
      <% } %>
    </div>
  </div>
</section>

長くなりましたが、上記のような感じになります。

命名/HTML設計の際のポイント

class名とHTML要素は1:1の関係にする

後述するJSでのDOMの拾い方にも影響しますが、基本的にclass名は1文書でユニークになるようにします(liなど繰り返すもの以外)。また、1つのelementに複数のclass名が当たらないようにします。
仮になにか属性を付けたいときはVariantか、変化するものであればdata属性やaria属性を利用します。
レイアウトやスタイルで繰り返すパターンが有る際は、mixinやextendを利用するようにします。(スタイルの話をclass名に持ち込まず、スタイルの話はスタイルファイルで解決するようにする。)

ChildNode よりも ComponentName がたくさんあるような状態になるようどんどん区切ることを心がける

単純に書いていくとたった2階層で ChildNode になってしまうため、ComponentNameを意識して増やしていくことが大切です。

divが必要になったが命名に迷ったらとりあえず {ChildName}Container としておいて、あとからリネーム

上の ComponentName の積極利用と合わせての話ですが、 ComponentName の名付けは結構難しいです。これは、全体を書いたあとで分かるというケースが経験的には多く、全部書いてみてあとからリファクタするほうが早いので、ひとまず Container という名前を便利に使って {ChildName}Container としてしまって、あとから全置換で対応するほうが早くできる事が多いです。

また、LPだと特に、以下のような名付け/HTML設計のパターンを予めやっておくと有効です。

Backgroundでも要素として作り、Sectionの直下に持ってくる

LPでは演出を盛ることが多く、Backgroundにも演出を入れることが多いため、cssの background プロパティや、疑似要素では限界になることが多いです。そのため、最初からBackgroundという要素を持っておくと柔軟に対応できます。

h1~h6タグには Header ではなく Heading

これらのタグに Header を指定すると、 <header> がきたときに困ることが多いので、 Heading という名前が有効です

サムネイルの <img> はとりあえず <div> で囲っておく

UI要素としてのサムネイルもフィルターやhoverで拡大のような装飾、インタラクションをもたせることが多いため、 Thumbnail > Thumbnail_Image / Thumbnail_Filter のように、常に <img> にWrapperをもたせるようにしておくと、あとからアニメーションを入れるなどのときに有効になります。

ECSSで命名した後のSCSSの生成

OneClickCSS という、HTMLを投げるとセレクタを吐き出してくれるという便利なサービスがあります。

スクリーンショット 2019-12-02 2.25.52.png

画像のようにHTMLをペーストし、「SimpleCSS」 ボタンで一発でセレクタを吐き出してくれ、これをコピーして.scssとして保存すればそれであとはスタイルを追加していくだけのscssの雛形が完成します。

ECSSで書くことによるJSでのアクセス

今まで、jsでアクセスするHTML要素は js- prefixをつけたり、idをつけたりすることが多かったとおもいます。
しかし、これではHTMLがjsに依存を持ってしまいます。
例えば、jsで扱う要素の数が増えると、必ずHTMLになにか手を加えることになります。
変更の多いLPでは地味に手数が増える作業になります。
これは、jsはHTMLに依存するが、HTMLはjsに依存しないようにして、依存の方向性を単方向にするほうが良いと思います。

そこで、ECSSではclass名と要素が基本的に1:1の関係になっているため、そのまま document.querySelector('.ns-ComponentName_ChildName') で呼び出しています。
HTMLのclass名をインターフェースとし、JSとCSSはそれを参照する、HTMLはJSとCSSに依存しないようにすることで、あまり複雑にならずに保つことができます。

(querySelectorにはパフォーマンス上の懸念があるため、場合によっては最適化することはあります)

ECSSの冗長性とclass名と要素が1:1の関係になっているおかげで、このようなスピーディで、最後までclass名のスコープで悩まないワークフローを成立させることができます。

all: unset

普段、reset.css, normalize.css, sanitize.cssなどを使うことが多いと思いますが、詳細度への配慮や、環境によってreset, normalize, sanitizeどれになっているかの確認、また作るときの選定にも地味に時間がかかります。

そこで、cssには all というプロパティがあります。

https://developer.mozilla.org/ja/docs/Web/CSS/all

この all: unset は デフォルトの値がinheritでないものはすべてinitializeしてくれる便利なプロパティで、reset.cssに近いものですが、上書きなどをあまり考えずに使うことができ、cssも見やすくなるため有用だと思います。

使用例: <a><button> が交じるけど同じスタイルのmixinで共通化したい

all: unset; だと以下のように書けます

@mixin blue-button-style() {
  display: inline-block;
  padding: 12px 24px;
  min-width: 120px;
  font-size: 14px;
  line-height: 1.25;
  text-align: center;
  white-space: nowrap;
  color: blue;
  border: 2px solid blue;
  border-radius: 4px;

  &:hover {
    cursor: pointer;
  }
}

.anchor {
  all: unset;
  @include blue-button-style;
}

.button {
  all: unset;
  @include blue-button-style;
}

というように書くと、 <button><a> に対して同じボタンのスタイルを適応させることができます。

スクリーンショット
(アカウント取るのが面倒だったのでスクショですみません :bow:

all: unsetとのセットで、他のページやプロジェクトにも使い回すことができて非常に便利だと思います。

使用上の注意

IE, Edgeが非対応の上、Safariで意図通りに動かないケースがあります(特にcolor周り)。

↓safariでの表示
Safariでの表示

polyfillがあるのですが、これでも意図通りに動かないケースを確認しています。
https://www.npmjs.com/package/postcss-all-unset

ですので、現在では以下のようにmixinを作る方法が現実的です。

mixinの作り方( @supports を使わない)

上記のpolyfillから、以下のようなmixinをつくり、いらない/使わないものを軽量化するというのが一つの手です。

ひとまずそのままコピペ

@mixin all-unset() {
  animation: none 0s ease 0s 1 normal none running;
  backface-visibility: visible;
  background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
  border: medium none currentColor;
  border-radius: 0;
  border-collapse: separate;
  //(...以下略)
}

出典を書いておき、使わないプロパティをコメントアウトしておくと、ある程度保守性の高い、コントロールされたmixinにできるかと思います。

@mixin all-unset() {
  // polyfill: https://www.npmjs.com/package/postcss-all-unset から改変
  background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
  border: medium none currentColor;
  border-radius: 0;
  border-collapse: separate;
  // (...以下略)

  // ==== 以下は使わない ====
  // animation: none 0s ease 0s 1 normal none running;
  // backface-visibility: visible;
  // (...以下略)
}

mixinの作り方( @supports を使う / 少し未来の話)

将来的にsafariがきれいに動けば、chromeやfirefoxではうまく動くので、 @supports 構文をつかったほうが、インスペクタに大量のプロパティが出てこなくなって良いと思います。

単純に考えると、mixinは以下のようになると思います。

@mixin all-unset() {
  @supports (all: unset) {
    all: unset;
  }

  @supports not (all: unset) {
    background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
    border: medium none currentColor;
    border-radius: 0;
    border-collapse: separate;
    // (...以下略)
  }
}

がこれでは

.anchor {
  @include all-unset;
  color: red;
}

と書いたとき、展開されるcssは

.anchor {
  color: red;
}

@supports (all: unset) {
  .anchor {
    all: unset;
  }
}

@supports not (all: unset) {
  .anchor {
    background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
    border: medium none currentColor;
    border-radius: 0;
    border-collapse: separate;
    // (...以下略)
  }
}

となってしまい、あとに書いたはずのcolorが打ち消されてしまいます。

そのため、mixinとそれのincludeは以下のように書く必要があります。

@mixin all-unset() {
  @supports (all: unset) {
    all: unset;
    @content;
  }

  @supports not (all: unset) {
    background: transparent none repeat 0% 0% / auto auto padding-box border-box scroll;
    border: medium none currentColor;
    border-radius: 0;
    border-collapse: separate;
    // (...以下略)
    @content;
  }
}

.anchor {
  @include all-unset() {
    color: red;
  }
}

この書き方は将来的にall: unsetが実用段階になったときに、もしかしたらちょっと置換がしづらいかもしれません。

WAI-ARIAの利用

.isShow.hide など、表示非表示のクラス名は人によってぶれがちですが、 WAI-ARIA の利用によって割と楽になります。

こちらは Webアクセシビリティ Advent Calendar 2019 の 14日目の記事として以下ページに書きました。合わせてご参照ください。

https://qiita.com/ln-north/items/8571782a9ec28ce622f4

終わりに

非常に長くなってしまいましたが、以上がTipsの紹介になります。

部署を移って期間が短いため、まだ正直いろんな感覚になれていないところがあるのですが、ぐいぐいコードを書くことが多くなり、ロゴなどのグラフィックデザインをすることも増え、非常に充実した日々を送っています。

現在すこし規模の大きいページも受けるようになったため、ejs/scss/jsからNuxt(Vue).jsでの開発にも挑戦しています。

エフェクトやモーションについても、シェーダ、パーティクル、WebGL、pixi.jsやGLSLやなど、演出手法、そしてその実現方法についてもたくさん勉強を重ねる必要を感じています。

また、そこで知見が溜まってきたら小出しに紹介できるように頑張りたいと思います。

ここまで読んでいただきありがとうございました。

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

[React] 年末だしstyled-componentsで爆速レスポンシブ対応していこうな

はじめに

こんにちは、ねりこ @nerikosans と申します。もう2019年も終わりが近づいてきましたね。
皆様におかれましてはますます import * as React from 'react'; の由、何よりと存じます。日頃は特別の export default にあずかり心より御礼申し上げます。

さて、Styled Components は、DOMのスタイルを、その定義ファイル( .jsx, .tsx )内で完結させてしまおうという思想のフレームワークです。コードの見通しの良さ、動的レンダリングのしやすさなどから昨今は人気になってい(ると感じてい)ます。

そして、DOMスタイリングにはレスポンシブ対応がつきものですが、やっぱり爆速で書きたいですよね。
ということで、 styled-componentsでレスポンシブするときのメモ書きです。

目標

最低限の記法で書きたいので、これ↓だけ書けば対応が済むように構築します。

const Box = styled.div`
  background: black;

  /* PC */
  ${media.pc`
    background: red;
  `}

  /* Smartphones */
  ${media.sp`
    background: red;
  `}
`;

Step1. styled-media-query

morajabi/styled-media-query
よく使うサイズをサクッと使いたい場合はこれだけで大丈夫。
small, medium, large, huge の4つの width breakpoint を備えていて、media queryを自動で生成してくれます。

公式サンプルが以下の通り。

const Box = styled.div`
  background: black;

  ${media.lessThan("medium")`
    /* screen width is less than 768px (medium) */
    background: red;
  `}

  ${media.between("medium", "large")`
    /* screen width is between 768px (medium) and 1170px (large) */
    background: green;
  `}

  ${media.greaterThan("large")`
    /* screen width is greater than 1170px (large) */
    background: blue;
  `}
`;

便利ですね。でも .lessThan("medium") って毎回書きたくないので、これをwrapします。
styled-media-query の export されていない type を使用しているところがあります。

media.tsx
import media from 'styled-media-query';
import {
  ThemedStyledProps,
  InterpolationValue,
  FlattenInterpolation,
} from 'styled-components';

/**
 * https://github.com/morajabi/styled-media-query/blob/master/src/index.d.ts
 */
type InterpolationFunction<Props, Theme> = (
  props: ThemedStyledProps<Props, Theme>
) => InterpolationValue | FlattenInterpolation<ThemedStyledProps<Props, Theme>>;

type GeneratorFunction<Props, Theme> = (
  strings: TemplateStringsArray,
  ...interpolations: (
    | InterpolationValue
    | InterpolationFunction<Props, Theme>
    | FlattenInterpolation<ThemedStyledProps<Props, Theme>>
  )[]
) => any;

const rules: { [v: string]: GeneratorFunction<unknown, any> } = {
  pc: (...args) => media.greaterThan('medium')(...args),
  sp: (...args) => media.lessThan('medium')(...args),
};

export default rules;

よし、これでこのファイルを media として default importすれば、 ${media.pc` .... `} だけでpc専用スタイルを書けるようになりました!

Step2. カスタマイズ

さて、これだけでも便利ですが、styled-media-query では今のところ pre-defined な4つのサイズ以外は指定できないようなので、これに加えて自由なmedia queryを書きたい場合 (例えば、heightで区切りたい場合) は以下のようにすればOKです。

media.tsx
import media from 'styled-media-query';
import {
  ThemedStyledProps,
  InterpolationValue,
  FlattenInterpolation,
  css,
} from 'styled-components';

/* (... 中略) */

const rules: { [v: string]: GeneratorFunction<unknown, any> } = {
  pc: (...args) => media.greaterThan('medium')(...args),
  sp: (...args) => media.lessThan('medium')(...args),
  short: (...args) => css`
    @media screen and (max-height: 480px) {
      ${css(...args)}
    }
  `,
};

export default rules;

これで新たに media.short`...`が使えるようになりました!

おわりに / 展望

以上で、晴れて簡潔にレスポンシブスタイルが書けるようになりました。最高ですね。

しかし、そもそもComponentを render するかどうかから出し分けたい場合などは、この方法では足りません。
例えば

const media = useMedia();
const isPC = media.pc;

みたいに書けたら便利ですよね。これを実現する方法はまた今度書きたいと思います。

お読みいただきありがとうございました!

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

CBcloudにフロントエンドエンジニアとして入社して行ったこと

現在、私はCBcloud株式会社でエンジニアとして、フロントエンドに限らず業務に携わっています。
今回の記事では入社当時、フロントエンドエンジニアとしてやってきたことをつらつら書いていきます。

前職の話

前職は完全にフロントエンド実装のみを専門とする会社でした。
サイトリニューアルや店舗予約系のWEBサービスのフロントエンド実装を担当していました。
jQueryを使い、モーダル、カルーセル、スムーズスクロールのようなWebサイトでよくあるコンテンツを作っていました。
実務では最後までjQuery1本でした。JSフレームワークは社内・社外の技術勉強会を通して触れていました。

一つ言えるのは、前職でのJavaScript研修がなければ、今も確実に「JavaScriptよくわからん」の状態ですし、リファレンスもまともに読めず、他の言語を学ぶ際にも応用できなかったでしょう。

転職してCBcloudに入社した

どうして入ったのか(入れたのか)

2019年12月現在、社員数を着々と伸ばし続け60人を超える会社に成長しているCBcloudですが、
私が入社する直前の2017年4月時点では10人ほどの会社でした。

CBcloudのサービスである「軽town」(のちの「PickGo」)を開発しているエンジニアは1人の状態で、
フロントエンド専門のエンジニアはおらず、サービスを紹介するウェブサイトからサービスサイトまで頑張って実装していました。

そんなわけで、「バックエンドはよくわからないけどフロントエンドならできます」という私とはちょうどタイミングや利害が一致し、入社することになりました。
「今の基準で面談・面接を受けていたら入れるかどうか」というのはたまに思います。
ともあれタイミングと、自分の強み・弱みを相手にちゃんと話せるかどうかは大事だなと思います。

私と同じ時期にもう一人デザイナー兼エンジニアが入ったことで、
2017年5月末から、PickGoのエンジニアは3人となります。

PickGoとは

荷物を送りたい人と荷物を運ぶことの出来る軽貨物者の配送ドライバーをマッチングさせ、
届けたい場所に即時でモノを届けることを実現する配送マッチングサービスです。

おおきく分けてサービスが2つに分かれています。

  • 企業が配送依頼をするWebサービス(PickGo Business)
  • 配送依頼をドライバーが確認し、エントリー~配送までを管理するスマートフォンアプリ(PickGo Driver)

PickGoのサブサービス的な個所のフロントエンド実装

まだあまりJavaScriptは触っておらず、HTMLとCSS修正が主でした。
バックエンド実装やJavaScriptとフロントエンドをつなぐ部分は任せきりでした。
リリース作業も任せていたため、本当にフロントエンドだけやっていました。

私のスキルがどの程度かまだ理解されていなかったので、
様子を見ながら起用されていたと思います。
フロントエンド的には「CSSの使い方を修正するだけでも割とコミットできそう」と当たりをつけたりしてました。

ウェブサイト立ち上げ

法人向けサイト作成

入社当時、既に軽townドメインを使った状態でドライバー向けWEBサイトはリリース済みでしたが、
PickGoの法人向けのPRサイトがまだありませんでした。
「デザインはあるがフロントエンド実装できる人間がいない!」という状況下で、私が入社したため実装を任されました。

当時渡されたデザインがビジネスサイトにしてはポップ過ぎたため、CEOからの要望もあり、
構成はほぼそのままで、色調を抑えつつデザイン画から実装しなおしました。

https://www.pickgo.town/business

現在は完全リニューアルしており、当時の面影は微塵もありません。
ともあれpickgoドメインは継続して使っており、SEO的には少しでも長い間存続している方がいいので、
2017年時点でリリースできたのはよかったのかなと思います。

個人向けサイト作成

後述の個人向けサービス開始前の布石として、個人向けサイトをリリースしました。
こちらはほぼ出来上がっているHTML・CSSがあり、そこに必要な文言・画像・セクションを足したものをリリースした形となります。
作業自体はフロントエンドを触れる人間なら誰でも出来るもので、特別なことはありません。

リリース直前、キャッチコピーとなる言葉の添削があり、
いろいろ考えた末に、私の案である「5000円から引っ越しを始めよう!」で行くことになりました。

https://www.pickgo.town/personal

例によって完全リニューアルされており当時の面影はありません。

統合ページ作成+リダイレクト

  • ドライバー向け
  • 法人向け
  • 個人向け

それぞれ対象が異なる3つのサイトが出来上がったので、それをまとめるページも必要となったため、作成しました。
3つのサイトにリンクするためのページです。

https://www.pickgo.town/

例によって完全リニューアルされていますが、統合サイト → 各サービスサイトという流れはこの段階で確立しました。

ロゴ変更

入社してすぐ、「サービス名を変える」ということになり、軽townからPickGoにサービス名が変わることになります。
それに伴い、Webサービス全体のロゴを変えることになりました。
ロゴを変えるだけではなく、メイン色も緑から青に変わりました。

ログイン画面UI再実装

また、ビジネス側のログイン画面はこの時に再実装し、デザインを大幅に変えました。
要素数の少ない画面なので、当時のデザインを活かすよりも実装しやすいように完全に構造を変えました。
デザイン画は特になかったので、必要な要素を小綺麗にまとめた感じにしました。

この画面はまだ当時のデザインのままなので、ログイン画面は長く使われるし、一度リリースしたらめったに変更されないぞ!と思います。
(機能改修は何度か入っていますが、デザイン自体はそのまま保たれています)

アプリのUIデザイン変更

ドライバー向け一覧と詳細のデザインを変えました。
デザイン画があったのでその通りに実装した感じです。
UIデザインはここから後も少しずつ変わっていくことになります。
ここ以外もそうですが、とにかくCSSクラス単体での指定に置き換えていってます。

PickGo Personal

ロゴの変更やアプリの改修も一段落し、次が大型の案件となるPickGoの個人向けサービスの開発でした。

メイン機能となるのはチャット機能でしたが、私はそこにはほぼ触れず、他のエンジニア2人に任せていました。
その代わり、それ以外の画面すべてのフロントエンド実装をしました(といっても10画面程度ですが)。
土壇場でヘルプページに説明を足したりしました。
PickGo Personalは結果的に3ヶ月ほどでリリースした形でした。

個人向け=一般向けのサービスであるため、テレビで取り上げられることが多く、
アプリの画面が大きく映し出されるたびに、嬉しいやら恥ずかしいやら何とも言えない思いをしました。

採用

個人アプリのリリース後は年末まで採用活動に振り切っており、
開発した記憶があまりありません。1日中面談やソーシングをしていました。
前職ではエンジニア面談をする機会がなかったので初めての経験でした。

人を採用するって難しい。
なかなか採用内定が出ることがなく、私が良いと思っても他の人たちが首を縦に振るような人がなかなか現れませんでした。
スキルとマインドを両方見た上で判断するわけですが、
圧倒的なスキルを持つ人には合うポジションを用意することができなかったりするわけで……。

他の皆さんの頑張りもあり、2017年末までに1人、2018年1月からさらに2人のエンジニアを迎えることになりました。

まとめ

つらつら書いていきましたが、採用以外は

  • サイトを作った
  • UI作った

以上! ということになります。
前職だとそれだけでずっと働けましたが、CBcloudでは限界を感じることになります。
次の記事ではその辺りの話から続けて書いていければと思います。

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