Quantcast
Channel: CSSタグが付けられた新着記事 - Qiita
Viewing all 8773 articles
Browse latest View live

CSS一部の要素だけを変更したい時

$
0
0

全体の要素を変更してしまうと思わぬところに影響を及ぼすことがあります。
なので、一部だけに適応した時は以下のような例で対応できる時もある。
- こちらのCSSが全体の要素としてかかっている時、htmlで対応させる。

.mutual-dependence-content{box-sizing:border-box;display:flex;}
<divclass="mutual-dependence-content"style="box-sizing: initial;">

initalは規定値という意味。
参考:initialとは
参考:box-sizingとは


React版ReactivesearchアプリをiPhone縦でも見やすくする

$
0
0

はじめに

PC用に作ったReactアプリを、CSSの@media(メディアクエリ)を使って、iPhone縦のときも見やすくします。ReactでなくてもHTMLアプリであれば使えます。

事前準備

React版Reactivesearch v3を使ってゼロから最速でElasticsearchフロントアプリを作る」で作ったコードをサンプルに説明します。

手順

CSSファイルに@media記述を付け加えるだけです。

CityRank.cssの修正

アプリのCSSで全体幅を指定しているmain-classセレクタの下に、横幅が768px以下の画面のときにmain-classセレクタを上書きする定義を@mediaを使って記述します。

CityRank.css
.main-class{width:480px;margin-top:10px;}@mediascreenand(width<=768px){.main-class{width:360px;margin-top:10px;}}

リンク

CSSでメディアクエリ(Media Queries)の基本的な書き方、記述の意味を理解し、「何となく使う」を卒業する。

【初心者向け】はじめてのGulpでのSassトランスパイル

$
0
0

Gulp手順

1. Node.js導入

①Node.js導入
公式サイトより安定版パッケージをダウンロードしインストールする
https://nodejs.org/ja/
node-v12.16.1.pkg(2020/02/24時点)

②Node.jsの存在確認

node -v

③デスクトップにテスト用のGulpディレクトリを作成

cd desktop
mkdir sample-gulp
cd sample-gulp

2. Gulp実行

①npmパッケージマネジャーにて初期化

npm init -y

②Gulpプラグインをインストール(※1 Mac OSでエラー)
Sassトランスパイルを実現したいので今回B手順を実施。

A.gulpをインストールする場合

npm install -D gulp

B.gulpとgulp-sassをインストールする場合

npm install -D gulp gulp-sass

※1 Mac OSでエラー
■課題
Catalina macOS 10.15にnpm installが失敗し次のエラーが出る(2020/02/24時点)
No Xcode or CLT version detected!

■対策
「Xcodeをインストールして、Command Line ToolsをXcode同梱版に切り替えてみた。」を参考にした。
https://qiita.com/UTA6966/items/6f8b1fd21c2dc9591488

手順①App StoreでXcodeをインストール
手順②ターミナルにてnode-gypをインストール

$ sudo xcode-select --switch /Applications/Xcode.app

$ npm install node-gyp

③トランスパイル対象フォルダを用意
・cssフォルダを作成
desktop/sample-gulp/css

・SCSSファイルを用意
style.scssファイルを作成
desktop/sample-gulp/css/style.scss

// ネストのテスト
div {
  p {
    font-weight: bold;
  }
}

// 変数のテスト
$fontColor: #525252;

h1 {
  color: $fontColor;
}

④タスクランナー処理記述
・gulpfile.js処理の記述
プロジェクトファイル直下にgulpfile.jsというファイルを作成

gulpfile.js

// gulpプラグインの読み込み
const gulp = require("gulp");
// Sassをコンパイルするプラグインの読み込み
const sass = require("gulp-sass");

// style.scssをタスクを作成する
gulp.task("default", function() {
  // style.scssファイルを取得
  return (
    gulp
      .src("css/style.scss")
      // Sassのコンパイルを実行
      .pipe(sass())
      // cssフォルダー以下に保存
      .pipe(gulp.dest("css"))
  );
});

⑤タスク実行(トランスパイル)

npx gulp

参考サイト

絶対つまずかないGulp 4入門(2019年版)インストールとSassを使うまでの手順
https://ics.media/entry/3290/

macOS 10.15 Catalinaをクリーンインストール後のnode-gypインストール
https://qiita.com/UTA6966/items/6f8b1fd21c2dc9591488

【デザイナー向け】gulpでかんたん画像圧縮
https://qiita.com/MikaShirahama/items/ab91624709510c496e53

[CSS/jsのみ]tableの行/列ヘッダーを固定する

$
0
0

はじめに

最近Webアプリ周辺の技術を学び始めた者です。普段は製造業で設計業務を担当しておりますが、社内システム構築用にいろいろと勉強しています。主に使うのは下記。

  • 言語 : python/C#
  • Web framework : django

記事に書き出すことで自身の理解も深まると考え、今回初投稿をさせて頂きます。

動機

休日に妻と共同でアプリ開発をしています(この開発記録もつけられたらなーと思っています)。
tableを多用するのですが、その際列ヘッダーと行ヘッダーを固定してtbodyのデータセルだけスクロールできないかなと考え、いろいろ調べていました。
便利なプラグインもたくさんありましたが、

  • 複数ヘッダーを固定できるものが限られていた
  • なるべく既存のtableに変更を加えたくない

という理由で手を出せず。position:stickyというステキなオプションがあるので、どうにかこれを使ってできないかと思い、やってみました。
なおこちらのstackoverflowの質問を参考にしました→Table with fixed header and fixed column on pure css

html

下記のようなtableと、wrapperとなるdivを用意します。class名はbootstrapを意識しています。

sample.html
<divclass="table-wrapper"><tableclass="table text-nowrap sticky-table table-borderless"><theadclass="thead-light"><trclass="fixed-header-0"><thclass="fixed-column-0">日付</th><th>1/1</th><th>1/2</th><th>1/3</th><th>1/4</th><th>1/5</th><th>1/6</th><th>1/7</th></tr><trclass="fixed-header-1"><thclass="fixed-column-0">曜日</th><th></th><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><thclass="fixed-column-0 table-light">AM</th><td></td><td></td><td></td><td>×</td><td>×</td><td></td><td></td></tr><tr><thclass="fixed-column-0 table-light">PM</th><td>×</td><td></td><td></td><td>×</td><td></td><td>×</td><td></td></tr><tr><thclass="fixed-column-0 table-light"></th><td></td><td></td><td></td><td></td><td></td><td></td><td>×</td></tr></tbody></table></div>

image.png

ゴールは上記tableの「日付」「曜日」行を固定しつつ、「AM」「PM」「夜」も常に表示することです。

wrapperにはclass=table-wrapper、tableにはclass=sticky-tableを指定します。また、固定したい行ヘッダーとなる各trには上から順にclass=fixed-header-n(nは0始まりの番号)を指定し、固定したい列ヘッダーとなる各thには左から順にclass=fixed-column-m(mは0始まりの番号)を指定します。
tableの左上の場所は行列方向に拘束したいので、tr.fixed-header-nおよびth.fixed-column-mの両方の指定が必要です。

ちなみにヘッダーにtable-lightやthead-lightで色を付けているのは、透明のままだとヘッダー固定したときに他のセルと重なってしまうからです。

css/js

下記のようなcssとjsを作成します。

sticky-table.css
/*ラッパー*/div.table-wrapper{overflow:scroll;max-height:200px;/*任意*/max-width:400px;/*任意*/}/*行ヘッダーを固定する。topの値はjsで動的に指定*/table.sticky-tabletheadtr[class*="fixed-header-"]th{position:-webkit-sticky;/* for Safari */position:sticky;/* tbody tdより手前に表示する */z-index:1;}/*行ヘッダーと列ヘッダーが重なる部分を固定する。top,leftの値はjsで動的に指定*/table.sticky-tabletheadtr[class*="fixed-header-"]th[class*="fixed-column-"]{/* 全てのセルより手前に表示する */z-index:2;}/*列ヘッダーを固定する。leftの値はjsで動的に指定*/table.sticky-tabletbodyth[class*="fixed-column-"]{position:-webkit-sticky;/* for Safari */position:sticky;/* tbody tdより手前に表示する */z-index:1;}

コメントを添えていますが、

div.table-wrapper{overflow:scroll;max-height:200px;/*任意*/max-width:400px;/*任意*/}

はwrapperの挙動です。max-height/max-widthは任意の値に設定してください。

続いて下記のような.jsを作成します。結局jqueryで書いてしまった。

sticky-table.js
//行ヘッダーに対しtopを設定height=0;for(vari=0;i<fixed_header_num;i++){$(".fixed-header-"+i+" th").css('top',height);height+=$(".fixed-header-"+i+" th").outerHeight();}//列ヘッダーに対しleftを設定width=0;for(varj=0;j<fixed_column_num;j++){$("th.fixed-column-"+j).css('left',width);width+=$("th.fixed-column-"+j).outerWidth(true);}

jsでは固定したい各ヘッダーに対し、「どこまでの位置に達したら上/左方向への移動を拘束するか」の値となるtop/leftの値を動的に設定しています。一番上のヘッダーはtop=0でよいのですが、二番目以降のヘッダーは自身の上にあるヘッダーの累積高さ分の値を設定しています。列ヘッダーも同様。

fixed_header_num, fixed_column_numはそれぞれ、固定したい行/列ヘッダーの数なのですが、これらは使用シーンに合わせて変わると思うので、グローバルで宣言することにします。

使用例

下記のツリー構造を仮定します。

sticky-table/
 ├ sample.html
 └ static/
   ├ css/
   │ └ sticky-table.css
   └ js/
      └ sticky-table.js
sample.html
<html><head><metaname="viewport"content="width=device-width,initial-scale=1"><metacharset="utf-8"/><title>ヘッダー固定</title><!--bootstrap--><linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"crossorigin="anonymous"><!--sticky-table--><linkrel="stylesheet"href="static/css/sticky-table.css"></head><body><!--https://stackoverflow.com/questions/15811653/table-with-fixed-header-and-fixed-column-on-pure-css--><divclass="container"><divclass="table-wrapper"><tableclass="table text-nowrap sticky-table table-borderless"><theadclass="thead-light"><trclass="fixed-header-0"><thclass="fixed-column-0">日付</th><th>1/1</th><th>1/2</th><th>1/3</th><th>1/4</th><th>1/5</th><th>1/6</th><th>1/7</th></tr><trclass="fixed-header-1"><thclass="fixed-column-0">曜日</th><th></th><th></th><th></th><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><thclass="fixed-column-0 table-light">AM</th><td></td><td></td><td></td><td>×</td><td>×</td><td></td><td></td></tr><tr><thclass="fixed-column-0 table-light">PM</th><td>×</td><td></td><td></td><td>×</td><td></td><td>×</td><td></td></tr><tr><thclass="fixed-column-0 table-light"></th><td></td><td></td><td></td><td></td><td></td><td></td><td>×</td></tr></tbody></table></div></div><!--jquery+bootstrap--><script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"crossorigin="anonymous"></script><!--sticky-table--><script type="text/javascript">//固定するヘッダーの数varfixed_header_num=2;//固定するカラムの数varfixed_column_num=1;</script><script src="static/js/sticky-table.js"></script></body></html>

image.png

これで任意の数の行/列ヘッダーをcss/jsのみで固定することができます。

課題

Chromeでは動作確認しましたが、IEだとpolyfillが必要みたいです。
stickyfill

また、globalで変数指定が必要だったり、洗練されてないイメージもあるので、もっとうまいやり方がありましたらご教示頂ければ幸いです。

参考

Table with fixed header and fixed column on pure css
stickyfill

メンテナンスページのHTMLの書き方

$
0
0

Webサービスをメンテナンスモードに入れるためのサーバー設定の記事は数多くあるが、肝心の「メンテナンス告知ページのHTMLをどう作るか」という部分があまり語られないので、そのことを中心にまとめた。

前提:ウェブサーバーをメンテナンスモードにするには

メンテナンスページを除いた全てのページでHTTPステータスコード503を返すようにする。
正しくステータスコードを返さないと格好悪いことになってしまうので気をつける。

設定例は「Apache メンテナンス」とか「nginx メンテナンス」とかでググると山ほど出てくるので割愛。
AWSを利用している場合はALBやCloudFrontでメンテナンスページ(Sorry Page)の機能がIaaS側にあるのでそれを利用すると良い。
DNSレベルでサーバーを切り替えてメンテナンスページを表示する方法(僕は心の中で乙武法と呼んでいる)は、DNS切り替えのタイミングがきっちりしないというのと、切り替え先で503を返すなど手間なので、本当に緊急な時以外はあまり採用したくない。

メンテナンスページの作り方

メンテナンスページは複雑なことをしようと思うとトラブルのもとなので極力シンプルな実装を心がける。

1ファイルで完結させる
CSSや画像を別ファイルで用意するとApacheやnginxの設定がややこしくなるし、ALBのSorry Pageのような複数ファイルに対応していない場合に利用できないので、CSSはhead内に書くかインラインで、画像はbase64で埋め込み1ファイルにまとめる。
ログファイル的にも画像やCSSの200アクセスがなくなるので状況を把握しやすくなる(と思う)。
(一応faviconもbase64で埋め込むことが可能だが、IE11, Edgeでは表示されないようなので無理して入れることはない)

HTML5(プログレッシブ・エンハンスメント)
サービス自体がIE11を未だサポートしていたりする場合はメンテナンスページも対応せざるを得ないのでプログレッシブ・エンハンスメントで実装する。
ただ複雑なことをやるわけでもないので普通に書けばIE11でも正常に表示されるはず。

日本語UTF-8対応
今どきめったなことでは文字化けなんてしないけれども、お行儀よくlangとcharsetの指定はしたい。

幅640pxくらいでレスポンシブ
スマホであれば幅はママでいいが、さすがにPCで幅の制限をかけないと少し間抜けになってしまうので多少狭めで。

Analyticsは入れなくてもいいのでは
判断が分かれるかもしれないが、個人的には意図的に起こしている503ページのアクセスは統計に入れたくないというのと、メンテナンス中にプライバシーポリシーページが見られないことやGDPRのことを考えると入れなくて良いと思う。

連絡手段を提示する
メンテナンスに入っているということは問い合わせフォームも見られないため、ユーザーからすると連絡手段が見えなくなってしまう。
メンテナンス中にユーザーから問い合わせ電話が入ったという話も聞くので、電話に対応できるリソースがないのであればメールアドレスなりGoogleフォームなりを明記したほうが良い。

いつからいつまでのメンテナンスか
Backlogの503ページにTwitterが埋め込まれていて、メンテナンス状況がリアルタイムで把握できてなかなかいいやり方だなと思ったので、定期メンテナンスなどが必要な場合は検討したい。
ただ基本的に意図的に入れたメンテナンスであればシンプルに終了時刻を目立たせてあげる方がユーザーにはわかりやすいと思うので、Twitterは単にリンクなどで見せるだけでも良いような気もする。

HTMLの例

簡易的な表示確認しかしていないので擬似コード程度のものとして見てほしい。

<!DOCTYPE html><htmllang="ja"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>ただいまメンテナンス中です | EXAMPLE.COM</title><style>body{margin:0;padding:0;background:#eee;}#conainer{margin:0auto;padding:20px;max-width:600px;background:#fff;}h1{font-size:2em;}.warn{padding:1em;background:#ff7;}.note{font-size:0.8em;}#footer{text-align:center;}</style></head><body><divid="conainer"><imgsrc="data:image/png;base64,略"alt="EXAMPLE.COM"><h1>ただいまメンテナンス中です</h1><p>システムアップデートのためサービスを停止しています。<br>ユーザーの皆様にはご不便をおかけしますが、メンテナンス終了まで今しばらくお待ち下さい。<br></p><h2>メンテナンス期間</h2><pclass="warn">
        2020年1月1日(水) 00:00 〜 <strong>12月31日(木) 23:59</strong><br></p><pclass="note">実施時間は前後する可能性があります。<br>最新の情報は<ahref="https://twitter.com/{username}">Twitter</a>にて更新しています。<br></p><h2>お問い合わせ
    </h2><p>メンテナンスに関するお問い合わせは<ahref="mailto:maintenance@example.com">maintenance@example.com</a>までお願いします。
    </p><hr><divid="footer">&copy; example.com
    </div></div></body></html>

selectをカスタマイズするやーつ

初心者が見落としてしまったメディアクエリの書き順

$
0
0

こんにちは。今回はSASSでメディアクエリを使い始めた頃に詰まってしまった所をお伝えしたいと思います。

index.html
...(省略)
  <body><h1>hogehoge</h1></body>
...(省略)

このhtmlに対して、以下のように設定するとmdサイズのときの設定(color:blue)が反映されません。何故でしょうか?

style.scss
$breakpoints:("sm":"screen and (min-width: 375px)","md":"screen and (min-width: 425px)")!default;@mixinmq($breakpoint:md){@media#{map-get($breakpoints,$breakpoint)}{@content;}}h1{color:blue;@includemq(md){// 「425px以上はcolor: red」color:red;}@includemq(sm){// 「375px以上はcolor: green」color:green;}}

そう、@include mq(md)の後に@include mq(sm)を設定しているからです。超基本なことですが、(詳細度が同じだったら)CSSのルールは後に書いた方が反映されます。なので、@include mq(md)@include mq(sm)の順番を入れ替えましょう。そうするとちゃんと意図した通りに反映されます。

style.scss
$breakpoints:("sm":"screen and (min-width: 375px)","md":"screen and (min-width: 425px)")!default;@mixinmq($breakpoint:md){@media#{map-get($breakpoints,$breakpoint)}{@content;}}h1{color:blue;@includemq(sm){// 「375px以上はcolor: green」color:green;}@includemq(md){// 「425px以上はcolor: red」color:red;}}

商品出品画面を作る

$
0
0

メルカリの商品出品画面を参考にコピーを作る

参考画面メルカリ

使用する機能

  • ActiveStorage(画像投稿)リンク
  • ancestry(多階層カテゴリー)
  • active_hash(静的データ作成)

とりあえず出来たコード

new.html.haml
.sell%header.sell-header=link_toroot_pathdo=image_tag'mercari_top_logo.svg',alt: 'mercari',height: '49',width: '185'-#メイン部分
%main%section.sell-container=form_withmodel: @itemdo|f|-# 画像部分
.sell-container__content.sell-title%h3.sell-title__text出品画像
              %span.sell-title__require必須
          .sell-container__content__max-sheet最大10枚までアップロードできます
          .sell-container__content__upload.sell-container__content__upload__items.sell-container__content__upload__items__box%ul#output-box%div#image-input{tabindex:"0"}=f.label:images,for: "item_images0",class: 'sell-container__content__upload__items__box__label',data: {label_id: 0}do=f.file_field:images,multiple: true,class: "sell-container__content__upload__items__box__input",id: "item_images0",style: 'display: none;'%pre%i.fas.fa-camera.fa-lgドラッグアンドドロップ
                        またはクリックしてファイルをアップロード
          .error-messages#error-image-#商品名部分
.sell-container__content.sell-title%h3.sell-title__text商品名
              %span.sell-title__require必須
          =f.text_field:name,{class:'sell-container__content__name',required: "required",placeholder: '商品名(必須 40文字まで)'}.error-messages#error-name.sell-title%h3.sell-title__text商品の説明
              %span.sell-title__require必須
          =f.text_area:text,{class: 'sell-container__content__description',required: "required",rows: '7',maxlength: '1000',placeholder: text_placeholder}-# placeholderでtems_helperを呼び出す
.sell-container__content__word-count%span#word-count
              0
            &#47;1000
          .error-messages#error-text-# 詳細部分
.sell-container__content%h3.sell-sub-head商品の詳細
          .sell-container__content__details.sell-title%h3.sell-title__textカテゴリー
                %span.sell-title__require必須
            .sell-collection_select=f.label:category_id,{class: 'sell-collection_select__label'}do=f.collection_select:category_id,@category_parent,:id,:name,{prompt: "選択して下さい"},{class: 'sell-collection_select__input',id: 'category-select',required: "required"}%i.fas.fa-chevron-down.error-messages#error-category.sell-title%h3.sell-title__text商品の状態
                %span.sell-title__require必須
            .sell-collection_select=f.label:condition_id,{class: 'sell-collection_select__label'}do=f.collection_select:condition_id,Condition.all,:id,:condition,{prompt: '選択して下さい'},{class: 'sell-collection_select__input',id: 'condition-select',required: "required"}%i.fas.fa-chevron-down.error-messages#error-condition-# 配送部分
.sell-container__content%h3.sell-sub-head%p配送について
            =link_to'/delivery',target: '_blank',class: 'sell-sub-head__guides-link'do%i.far.fa-question-circle.sell-container__content__delivery.sell-title%h3.sell-title__text配送料の負担
                %span.sell-title__require必須
            .sell-collection_select=f.label:deliverycost_id,{class: 'sell-collection_select__label'}do=f.collection_select:deliverycost_id,Deliverycost.all,:id,:payer,{prompt: '選択して下さい'},{class: 'sell-collection_select__input',id: 'deliverycost-select',required: "required"}%i.fas.fa-chevron-down.error-messages#error-deliverycost.sell-title%h3.sell-title__text発送元の地域
                %span.sell-title__require必須
            .sell-collection_select=f.label:pref_id,class: 'sell-collection_select__label'do=f.collection_select:pref_id,Pref.all,:id,:name,{prompt: '選択して下さい'},{class: 'sell-collection_select__input',id: 'pref-select',required: "required"}%i.fas.fa-chevron-down.error-messages#error-pref.sell-title%h3.sell-title__text発送までの日数
                %span.sell-title__require必須
            .sell-collection_select=f.label:delivery_days_id,class: 'sell-collection_select__label'do=f.collection_select:delivery_days_id,DeliveryDays.all,:id,:days,{prompt: '選択して下さい'},{class: 'sell-collection_select__input',id: 'delivery_days-select',required: "required"}%i.fas.fa-chevron-down.error-messages#error-delivery_days-# 価格部分
.sell-container__content%h3.sell-sub-head%p販売価格(300〜9,999,999)
            =link_to'/price',target: '_blank',class: 'sell-sub-head__guides-link'do%i.far.fa-question-circle.sell-container__content__price.sell-title%h3.sell-title__text販売価格
                %span.sell-title__require必須
            .sell-container__content__price__form=f.label:price,class: 'sell-container__content__price__form__label'do¥
                =f.number_field:price,{placeholder: '0',value: '',autocomplete:"off",class: 'sell-container__content__price__form__box',required: "required"}.error-messages#error-price.sell-container__content__commission.sell-container__content__commission__left販売手数料 (10%)
            .sell-container__content__commission__right.sell-container__content__profit.sell-container__content__profit__left販売利益
            .sell-container__content__profit__right.submit-btn=f.submit'出品する',class: 'submit-btn__sell-btn'=link_to'もどる',root_path,class: 'submit-btn__return-btn'.attention-box%p禁止されている
              =link_to'行為','/prohibited_conduct',target: '_blank'および
              =link_to'出品物','/prohibited_item',target: '_blank'を必ずご確認ください。
              =link_to'偽ブランド品','/counterfeit_goods',target: '_blank'=link_to'盗品物','/stolen_goods',target: '_blank'などの販売は犯罪であり、法律により処罰される可能性があります。また、出品をもちまして
              =link_to'加盟店規約','/seller_terms',target: '_blank'に同意したことになります。

  %footer.sell-footer%nav%ul.clearfix%li=link_to'#'doプライバシーポリシー
        %li=link_to'#'doメルカリ利用規約
        %li=link_to'#'do特定商取引に関する表記
    =link_toroot_path,class: 'footer__logo'do=image_tag'logo-gray.svg',alt: 'mercari',height: '65',width: '80'%p%small&copy; Mercari, Inc.
items_new.scss
a{color:inherit;text-decoration:none;box-sizing:border-box;}img{vertical-align:middle;box-sizing:border-box;}.error-messages{color:#ff0211;font-size:14px;line-height:1.4em;margin:16px0;box-sizing:border-box;}.sell-title{align-items:center;margin:0!important;box-sizing:border-box;&__text{font-size:14px;font-weight:600;line-height:1.4em;}&__require{margin-left:8px;font-size:12px;padding:04px;background-color:#ff0211;color:#fff;border-radius:2px;display:inline-block;font-style:normal;font-weight:600;line-height:1.4em;margin:0;}}.sell-sub-head{box-sizing:border-box;color:rgb(136,136,136);font-size:14px;font-weight:600;line-height:1.4em;margin-bottom:24px;display:flex;&__guides-link{color:rgb(0,149,238);margin-left:4px;}}.sell-collection_select{box-sizing:border-box;margin-top:16px;&__label{display:inline-block;position:relative;width:100%;.fas.fa-chevron-down{box-sizing:border-box;pointer-events:none;position:absolute;right:16px;top:40%;color:rgb(136,136,136);height:48px;}}&__input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:1pxsolid#ccc;border-radius:4px;box-sizing:border-box;color:#222;font-size:16px;height:48px;line-height:1;margin:0;outline:none;padding:056px016px;width:100%;}}// ここより上は繰り返し使用するパーツ.sell{box-sizing:border-box;position:relative;color:rgb(51,51,51);background-color:rgb(245,245,245);font-family:Arial,游ゴシック体,YuGothic,メイリオ,Meiryo,sans-serif;font-size:14px;line-height:1;box-sizing:border-box;.sell-header{box-sizing:border-box;height:128px;align-items:center;display:flex;justify-content:center;}.sell-container{box-sizing:border-box;max-width:700px;width:100%;margin:0pxauto;background-color:rgb(255,255,255);// 画像部分&__content{height:auto;padding:40px;border-bottom:1px;border-bottom-color:#efefef;border-bottom-style:solid;&__max-sheet{margin-top:16px;}&__upload{margin-top:16px;display:flex;flex-wrap:wrap;&__items{height:auto;width:100%;&__box{height:auto;align-content:center;align-items:center;cursor:pointer;display:flex;flex-wrap:wrap;justify-content:center;position:relative;border-width:1px;#output-box{box-sizing:border-box;display:flex;flex-wrap:wrap;width:100%;height:auto;.preview-image{box-sizing:border-box;height:150px;width:20%;padding:0px4px;margin-top:8px;&__figure{margin:0auto;height:118px;background-color:rgb(245,245,245);img{box-sizing:border-box;width:100%;height:100%;object-fit:contain;}}&__button{border-top-width:1px;border-top-color:rgb(204,204,204);border-top-style:solid;background-color:rgb(245,245,245);justify-content:space-around;display:flex;align-items:center;height:32px;color:rgb(0,149,238);}}#image-input{box-sizing:border-box;height:100%;-webkit-flex:1;flex:1;margin-top:8px;.sell-container__content__upload__items__box__label{box-sizing:border-box;background-color:rgb(245,245,245);height:150px;border-width:1px;border-style:dashed;border-color:rgb(204,204,204);text-align:center;display:flex;align-items:center;justify-content:center;i{box-sizing:border-box;margin-bottom:8px;}}}}}}}}// 商品名部分&__content{&__name{margin-top:16px;border:1pxsolid#ccc;border-radius:4px;box-sizing:border-box;height:48px;padding:016px;width:100%;}&__description{margin-top:16px;border:1pxsolid#ccc;border-radius:4px;box-sizing:border-box;padding:16px;width:100%;font-size:16px;display:block;}&__word-count{text-align:right;color:#888;font-size:12px;line-height:1.4em;}}// 詳細は共通パーツのみ// 配送は共通パーツのみ// 価格部分&__content{&__price{-webkit-box-align:center;align-items:center;box-sizing:content-box;display:flex;height:46px;justify-content:space-between;&form__label{font-size:14px;}&__form__box{border:1pxsolid#ccc;border-radius:4px;height:48px;margin-left:8px;padding:016px;width:300px;align-items:center;display:inline-flex;text-align:right;}}#error-price{box-sizing:border-box;text-align:right;}&__commission{display:flex;justify-content:space-between;height:70px;padding:12px0px;align-items:center;border-bottom:1px;border-bottom-color:#efefef;border-bottom-style:solid;}&__profit{display:flex;justify-content:space-between;height:70px;padding:12px0px;align-items:center;}}.submit-btn{box-sizing:border-box;margin:0auto;width:360px;margin-bottom:32px;&__sell-btn{background-color:#ea352d;color:#fff;margin-bottom:24px;width:100%;font-size:17px;height:48px;font-weight:600;border-radius:4px;}&__return-btn{background-color:#ccc;color:#222;width:100%;font-size:17px;height:48px;font-weight:600;padding:14px0;border-radius:4px;display:inline-block;text-align:center;}}.attention-box{box-sizing:border-box;font-size:12px;line-height:1.4em;word-break:keep-all;a{box-sizing:border-box;color:#0095ee;}}}.sell-footer{box-sizing:border-box;padding:40px0px;text-align:center;nav{box-sizing:border-box;.clearfix{box-sizing:border-box;display:inline-block;font-size:12px;display:flex;justify-content:center;margin-bottom:40px;li{box-sizing:border-box;margin:0px8px;}}}}}
items_new.js
$(document).on('turbolinks:load',function(){// 画像が選択された時プレビュー表示、inputの親要素のulをイベント元に指定$('#image-input').on('change',function(e){//ファイルオブジェクトを取得するletfiles=e.target.files;$.each(files,function(index,file){letreader=newFileReader();//画像でない場合は処理終了if(file.type.indexOf("image")<0){alert("画像ファイルを指定してください。");returnfalse;}//アップロードした画像を設定するreader.onload=(function(file){returnfunction(e){letimageLength=$('#output-box').children('li').length;// 表示されているプレビューの数を数えるletlabelLength=$("#image-input>label").eq(-1).data('label-id');// #image-inputの子要素labelの中から最後の要素のカスタムデータidを取得// プレビュー表示$('#image-input').before(`<li class="preview-image" id="upload-image${labelLength}" data-image-id="${labelLength}">
                                      <figure class="preview-image__figure">
                                        <img src='${e.target.result}' title='${file.name}' >
                                      </figure>
                                      <div class="preview-image__button">
                                        <a class="preview-image__button__edit" href="">編集</a>
                                        <a class="preview-image__button__delete" data-image-id="${labelLength}">削除</a>
                                      </div>
                                    </li>`);$("#image-input>label").eq(-1).css('display','none');// 入力されたlabelを見えなくするif(imageLength<9){// 表示されているプレビューが9以下なら、新たにinputを生成する$("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                        <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                        <i class="fas fa-camera fa-lg"></i>
                                      </label>`);};};})(file);reader.readAsDataURL(file);});});//削除ボタンが押された時$(document).on('click','.preview-image__button__delete',function(){lettargetImageId=$(this).data('image-id');// イベント元のカスタムデータ属性の値を取得$(`#upload-image${targetImageId}`).remove();//プレビューを削除$(`[for=item_images${targetImageId}]`).remove();//削除したプレビューに関連したinputを削除letimageLength=$('#output-box').children('li').length;// 表示されているプレビューの数を数えるif(imageLength==9){letlabelLength=$("#image-input>label").eq(-1).data('label-id');// 表示されているプレビューが9なら,#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得$("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                  <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                  <i class="fas fa-camera fa-lg"></i>
                                </label>`);};});// f.text_areaの文字数カウント$("textarea").keyup(function(){lettxtcount=$(this).val().length;$("#word-count").text(txtcount);});//販売価格入力時の手数料計算$('#item_price').keyup(function(){letprice=$(this).val();if(price>=300&&price<=9999999){letfee=Math.floor(price*0.1);// 小数点以下切り捨てletprofit=(price-fee);$('.sell-container__content__commission__right').text('¥'+fee.toLocaleString());// 対象要素の文字列書き換える$('.sell-container__content__profit__right').text('¥'+profit.toLocaleString());}else{$('.sell-container__content__commission__right').html('');$('.sell-container__content__profit__right').html('');}});// 各フォームの入力チェック$(function(){//画像$('#image-input').on('focus',function(){$('#error-image').text('');$('#image-input').on('blur',function(){$('#error-image').text('');letimageLength=$('#output-box').children('li').length;if(imageLength==''){$('#error-image').text('画像がありません');}elseif(imageLength>10){$('#error-image').text('画像を10枚以下にして下さい');}else{$('#error-image').text('');}});});//送信しようとした時$('form').on('submit',function(){letimageLength=$('#output-box').children('li').length;if(imageLength==''){$('body, html').animate({scrollTop:0},500);$('#error-image').text('画像がありません');}elseif(imageLength>10){$('body, html').animate({scrollTop:0},500);$('#error-image').text('画像を10枚以下にして下さい');}else{returnfalse;}});//画像を削除した時$(document).on('click','.preview-image__button__delete',function(){letimageLength=$('#output-box').children('li').length;if(imageLength==''){$('#error-image').text('画像がありません');}elseif(imageLength>10){$('#error-image').text('画像を10枚以下にして下さい');}else{$('#error-image').text('');}});//商品名$('.sell-container__content__name').on('blur',function(){letvalue=$(this).val();if(value==""){$('#error-name').text('入力してください');$(this).css('border-color','red');}else{$('#error-name').text('');$(this).css('border-color','rgb(204, 204, 204)');}});//商品説明$('.sell-container__content__description').on('blur',function(){letvalue=$(this).val();if(value==""){$('#error-text').text('入力してください');$(this).css('border-color','red');}else{$('#error-text').text('');$(this).css('border-color','rgb(204, 204, 204)');}});//カテゴリー$('#category-select').on('blur',function(){letvalue=$(this).val();if(value==""){$('#error-category').text('選択して下さい');$(this).css('border-color','red');}else{$('#error-category').text('');$(this).css('border-color','rgb(204, 204, 204)');}});//状態$('#condition-select').on('blur',function(){letvalue=$(this).val();if(value==""){$('#error-condition').text('選択して下さい');$(this).css('border-color','red');}else{$('#error-condition').text('');$(this).css('border-color','rgb(204, 204, 204)');}});//送料負担$('#deliverycost-select').on('blur',function(){letvalue=$(this).val();if(value==""){$('#error-deliverycost').text('選択して下さい');$(this).css('border-color','red');}else{$('#error-deliverycost').text('');$(this).css('border-color','rgb(204, 204, 204)');}});//発送元$('#pref-select').on('blur',function(){letvalue=$(this).val();if(value==""){$('#error-pref').text('選択して下さい');$(this).css('border-color','red');}else{$('#error-pref').text('');$(this).css('border-color','rgb(204, 204, 204)');}});//発送までの日数$('#delivery_days-select').on('blur',function(){letvalue=$(this).val();if(value==""){$('#error-delivery_days').text('選択して下さい');$(this).css('border-color','red');}else{$('#error-delivery_days').text('');$(this).css('border-color','rgb(204, 204, 204)');}});//価格$('.sell-container__content__price__form__box').on('blur',function(){letvalue=$(this).val();if(value<300||value>9999999){$('#error-price').text('300以上9999999以下で入力してください');$(this).css('border-color','red');}else{$('#error-price').text('');$(this).css('border-color','rgb(204, 204, 204)');}});});});
items.controller.rb
defnew@item=Item.new@category_parent=Category.where("ancestry is null")enddefcreate@item=Item.new(item_params)if@item.saveredirect_toroot_pathelserender:newendendprivatedefitem_paramsparams.require(:item).permit(:name,:text,:category_id,:condition_id,:deliverycost_id,:pref_id,:delivery_days_id,:price,images: []).merge(user_id: current_user.id,boughtflg_id:"1")endend
item.rb(モデル)
classItem<ApplicationRecordextendActiveHash::Associations::ActiveRecordExtensionsbelongs_to_active_hash:conditionbelongs_to_active_hash:prefbelongs_to_active_hash:deliverycostbelongs_to_active_hash:delivery_daysbelongs_to_active_hash:boughtflg# 上記active_hashのアソシエーションvalidate:images_presencevalidates:name,:text,:category_id,:condition_id,:deliverycost_id,:pref_id,:delivery_days_id,:boughtflg_id,presence: truevalidates:price,presence: true,inclusion: 300..9999999has_many_attached:imagesbelongs_to:user,foreign_key: 'user_id' # optional: true後で消す belongs_toのnotnull制約解放のため使用しているbelongs_to:category#imageのバリデーションdefimages_presenceifimages.attached?# inputに保持されているimagesがあるかを確認ifimages.length>10errors.add(:image,'10枚まで投稿できます')endelseerrors.add(:image,'画像がありません')endendend

カテゴリーの2階層以降がまだ未実装でした・・・

とりあえず、後から実装するとして、先ずは画像投稿から


商品出品画面を作る②ActiveStorage(複数画像投稿)

$
0
0

ActiveStorageを使用して複数枚の画像を投稿する

先ずはここを見てくださいRailsガイド、導入方法とか書いてあります。
S3に接続するときは注意が必要ですね。

そもそもActiveStorageは何がいいのか?

ActiveStorageの特徴としては画像をitemモデルとして扱えることでしょう。
どう言う事かと言うと、itemとimageが1:多の関係だとすると、テーブルを2個用意しないといけません。
そしてアソシエーションを組んで〜と言う処理をします。
削除や変更の時に処理が若干複雑になってしまいます。
そんな時、ActiveStorageならitemテーブルのみを対象に処理をすれば良いわけです。

今回の要件

  • 複数同時選択による複数枚の画像投稿
  • 複数回選択による複数枚の画像投稿
  • プレビュー表示
  • 投稿枚数管理
  • プレビューの削除
imet.rb
has_many_attached:images

ActiveStorageで複数画像を扱う為にモデルに記述

items_controller.rb
defnew@item=Item.new@category_parent=Category.where("ancestry is null")enddefcreate@item=Item.new(item_params)if@item.saveredirect_toroot_pathelserender:newendendprivatedefitem_paramsparams.require(:item).permit(:name,:text,:category_id,:condition_id,:deliverycost_id,:pref_id,:delivery_days_id,:price,images: []).merge(user_id: current_user.id,boughtflg_id:"1")end

def item_paramsの images:[]がポイントです。
こうする事で、複数画像を受け取ることが出来ます。

new.html.haml
-# 画像部分
.sell-container__content.sell-title%h3.sell-title__text出品画像
      %span.sell-title__require必須
  .sell-container__content__max-sheet最大10枚までアップロードできます
  .sell-container__content__upload.sell-container__content__upload__items.sell-container__content__upload__items__box%ul#output-box     ここにプレビューが入ります
          %div#image-input{tabindex:"0"}=f.label:images,for: "item_images0",class: 'sell-container__content__upload__items__box__label',data: {label_id: 0}do=f.file_field:images,multiple: true,class: "sell-container__content__upload__items__box__input",id: "item_images0",style: 'display: none;'ここに新しinputが入ります
              %pre%i.fas.fa-camera.fa-lgドラッグアンドドロップ
                またはクリックしてファイルをアップロード
  .error-messages#error-imageここにエラーメッセージが入ります

= f.file_fieldの multiple:trueがポイントです
name属性がname="item[images][]"になっていると思います。
これにより、選択した画像を配列とし保持できるようになり、複数枚を同時に選択できるようになります。
image.png

item_new.js
$(document).on('turbolinks:load',function(){$('#image-input').on('change',function(e){// 画像が選択された時プレビュー表示、inputの親要素のdivをイベント元に指定//ファイルオブジェクトを取得するletfiles=e.target.files;$.each(files,function(index,file){letreader=newFileReader();//画像でない場合は処理終了if(file.type.indexOf("image")<0){alert("画像ファイルを指定してください。");returnfalse;}//アップロードした画像を設定するreader.onload=(function(file){returnfunction(e){letimageLength=$('#output-box').children('li').length;// 表示されているプレビューの数を数えるletlabelLength=$("#image-input>label").eq(-1).data('label-id');// #image-inputの子要素labelの中から最後の要素のカスタムデータidを取得// プレビュー表示$('#image-input').before(`<li class="preview-image" id="upload-image${labelLength}" data-image-id="${labelLength}">
                                      <figure class="preview-image__figure">
                                        <img src='${e.target.result}' title='${file.name}' >
                                      </figure>
                                      <div class="preview-image__button">
                                        <a class="preview-image__button__edit" href="">編集</a>
                                        <a class="preview-image__button__delete" data-image-id="${labelLength}">削除</a>
                                      </div>
                                    </li>`);$("#image-input>label").eq(-1).css('display','none');// 入力されたlabelを見えなくするif(imageLength<9){// 表示されているプレビューが9以下なら、新たにinputを生成する$("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                        <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                        <i class="fas fa-camera fa-lg"></i>
                                      </label>`);};};})(file);reader.readAsDataURL(file);});});

今回の1番の要所です
流れとしては
1. 画像を選択し、inputに保持させる
2. 保持された画像からファイルオブジェクトを取得する
3. プレビューを表示する(イベント元のイベント元のカスタムデータIDの番号を付加する)
4. 表示されているプレビューの数を数え、9以下なら次のinputを生成する(イベント元のカスタムデータIDの次の番号を付加する)

ポイント

  • プレビューとinputはカスタムデータIDの番号で紐づけます。
    data-image-id="${labelLength}"data-label-id="0"の部分です。

  • input要素はidで区別します。
    id="item_images${labelLength+1}"となっている部分です。
    これは#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得指定ます。
    つまり、イベント元のカスタムデータIDの番号に+1しています。

  • labelとinputは、labelのfor属性inputのidで紐づける。
    for="item_images${labelLength+1}"id="item_images${labelLength+1}"の部分です。
    image.png
    ul要素の下のli要素がプレビューです。
    その下のdiv要素の下がinputが囲われたlabelです。

プレビューを削除する

item_new.js
//削除ボタンが押された時$(document).on('click','.preview-image__button__delete',function(){lettargetImageId=$(this).data('image-id');// イベント元のカスタムデータ属性の値を取得$(`#upload-image${targetImageId}`).remove();//プレビューを削除$(`[for=item_images${targetImageId}]`).remove();//削除したプレビューに関連したinputを削除letimageLength=$('#output-box').children('li').length;// 表示されているプレビューの数を数えるif(imageLength==9){letlabelLength=$("#image-input>label").eq(-1).data('label-id');// 表示されているプレビューが9枚なら,#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得$("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                  <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                  <i class="fas fa-camera fa-lg"></i>
                                </label>`);};});

後はCSSでデコレーションすればOKですね。

ただし、一つ問題があります。
複数同時選択を行うと不具合が発生する事です。
と言うのも、同時選択をすると一つのinputに複数の画像の情報が保持されるからです。
そうすると、プレビューを削除した時に、正常に動作しなくなってしまいます。
対策としては同時選択を出来ないようにする方法があります。
この場合、name属性にも手を加える必要があります。
それは今後試すことにします。

今回は以上です

z-indexが効かない

$
0
0

z-indexがまともに指定されていてもbackgroundcolorが指定されていないと透過する

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

$
0
0

100日チャレンジの272日目

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

チーム開発

$
0
0

チーム開発で違うリポジトリで作業してしまった、その修正で使った記事
違うgitのURLにプロダクトをpushしたい参考記事
https://ja.stackoverflow.com/questions/42216/%E9%81%95%E3%81%86git%E3%81%AEurl%E3%81%AB%E3%83%97%E3%83%AD%E3%83%80%E3%82%AF%E3%83%88%E3%82%92push%E3%81%97%E3%81%9F%E3%81%84

一二週間ぶりに
rails g controller name 
rails g model name
rails db:migrate
rails routes
の作業、たったの一二週間なのに忘れて、すぐにやり方が出てこない!!焦る!
クレジットカード登録、変更画面作業中(フロントエンド)
まだどの作業もスムーズにできない、一つ一つ確認、復習しながら進めるため遅い!
チームに迷惑かけないようにしたいが、ものすごく足ひっぱってる。

【CSS】backgroundを使って棒グラフをつくる(アニメーション付き)

画像のアスペクト比を維持しながら縮小させるmixin(だけどmixinじゃなくても大丈夫だった)

$
0
0

タグで囲った画像(もしくは背景画像)を、ブラウザの幅縮めてもうまい具合に縮小させてくれるアレです。

scss
@mixinimg_ratio($property,$width,$height){#{$property}:percentage($height/$width);}

使い方

いつもみたいに@includeでよみこんで使いましょう

たとえば、300x200の画像のアスペクト比をpadding-topに出したい場合は

scss
@includeimg_ratio(padding-top,300,200);

になります。

コンパイル後は

css
padding-top:66.66667%;

こうなりました。

少しだけ解説

+ %とか#{%}とかつけといたらいけるだろう、と思ってたらできませんでした。😂
ので、いろいろと調べてみたわけなんですけども・・・

percentageをつけると、単位のない数値をパーセントに変換してくれます。
公式サイトでは以下のページに解説がありました。

sass:math

個人的によくつかうのでmixin化しました。
ここまちがってるぜ!的な指摘あればぜひぜひ・・・😂

もっと簡単だった

あとからご指摘いただいて、以下のようにすればmixin使わなくてもできるやん、ってなりました😆

scss
padding-top:percentage(200/300);

でも私、すぐ忘れてしまうので・・・(考え方とか、手順とか、なぜそうなるのかの部分)
念の為、記事はそのまま残しておくのです・・・😂

Webデザイナーの仕事内容や働き方について

$
0
0

※本稿は弊社ホームページに記載のものを転載しています。
図1b.png

Webデザイナーは、クリエイティブな職種として近年注目を集めています。そんなWebデザイナーの仕事内容は広く言うとWebサイトの制作に携わることです。

通常Webサイトを制作するにはチームで行うことが一般的です。制作に関わる職種は、Webサイトの仕様を決めるWebディレクター、サイトを構築するコーダー、文章を作成するライターなどさまざまです。

その中でWebデザイナーは、デザインソフトを駆使してWebサイトの見た目の部分を作ります。制作チームの一端を担うWebデザイナーですが、最近では在宅やインハウス等、働き方も多様化してきています。

Webデザイナーはパソコンとインターネット環境があれば仕事ができる性質上、ある程度のスキルがあれば場所を選ばずに働けることも人気の理由の一つです。

Webデザイナーの仕事内容

Webデザイナーの主な仕事内容はPhotoshopやillustratorなどのデザインソフトを使って、Webサイトの全体像やボタンなどの見た目の部分を作ることです。会社によってWebディレクターやコーダーと兼務している場合もあり、どこまでが業務範囲かという明確な決まりはありません。WebデザイナーからWebディレクターになり、管理業務のみ行う人もいれば、企画からサイト作成まで全てを一人で行う人もいます。そこで一般的にコーダーを兼務している場合のWebデザイナーの仕事内容をそれぞれの項目ごとに確認していきましょう。

Webサイトの要件整理

まずはじめに、Webサイトを作りたいクライアントの目的をヒアリングし要件を整理します。クライアントの目的やターゲットによってWebサイトの仕様やデザインが変わるため、しっかりとヒアリングを行う必要があります。サイトの目的、要件をコンセプトに落とし込みます。サイトコンセプトを企画書にまとめ、クライアントの合意を得ます。

Webサイトの構成とレイアウト(ワイヤーフレーム)

コンセプトが決まったら、Webサイトの設計図となるワイヤーフレームを作ります。ワイヤーフレームとは骨組みという意味です。このときにサイト内のページ数、コンテンツの位置や数、大きさなどを決めます。構成とレイアウトはサイトの使いやすさや、集客にも影響する重要な作業です。

Webサイトのデザインを作る

Photoshopやillustratorを使って、Webサイトの実際の見た目を作っていきます。このときにサイト全体のカラーや、ボタンのデザインなどを決めていきます。まずデザインカンプを作ります。デザインカンプとは、サイト制作に取り掛かる前のデザイン案のことです。デザインカンプでOKが出れば、実際にサイト制作に取り掛かります。デザイン作成はミリ単位の調節が求められる繊細な作業です。

Webサイトのコーディング

Webサイトの見た目ができたらコーディングを行います。Webサイトは見た目を作るだけではインターネット上に表示されません。コーディングによってインターネット上に認識させることでWebサイトを観覧できるようになります。コーディングにはHTMLとCSSを使います。HTMLはマークアップ言語と呼ばれ、文字情報を表示するために使います。CSSはサイト内の色や画像、見た目を定義します。Webサイトに動きや、システムを組み込むにはJavaScriptなどのプログラミング言語が必要です。

Webデザイナーの働き方

図2.png

Webデザイナーといっても、様々な働き方があります。働き方にはインハウス、制作会社、SES会社、フリーランスなどがあり、雇用形態や仕事内容なども変わってきます。

インハウス

企業に所属し、自社のWebサイトやサービスを担当するWebデザイナーです。業務の幅が広く、パンフレット作成などいろいろな経験ができることが魅力です。企業によっては、Web担当者が他におらず、全てをやらなければいけない場合があります。デザインの幅は少ないので、いろいろなデザインを担当したい人には向きません。

制作会社

クライアントから依頼を受けてWebサイトを制作します。新規サイト制作や、既存サイトのリニューアルなどを担当します。新規のWebサイト制作の場合、クライアントや案件ごとにデザインをゼロから考えることも多く、デザインセンスが求められます。クライアントの都合で納期が決まるため、仕事に追われることもありますが、様々なデザインを経験したい人には向いています。

SES会社

SES(システムエンジニアリングサービス)会社は、自社で契約しているエンジニアを、外部の会社に客先常駐として派遣する会社のことで、最近ではWebデザイナーにも波及しています。常駐先は一般企業や制作会社などで、大手で働ける機会もあります。未経験でも採用されやすいことがメリットです。業務内容が単純作業になりがちで、スキルアップが難しいというデメリットもあります。

個人で独立して仕事を請負うWebデザイナーです。仕事の請負先は、企業や個人など多岐にわたります。フリーランスは自分で仕事を獲得する営業力が必要ですが、最近では営業代行サービスなども増えてきています。フリーランスは自分の裁量で仕事量などを決められることが魅力です。反面、トラブル対応などすべて自分でやらなければならないため、ある程度の実力と対応力が求められます。

Webデザイナーのしごと内容まとめ

図3.png

今回はWebデザイナーの仕事内容や働き方について紹介しました。Webデザイナーは単にデザインするだけでなく、Webサイト制作のさまざまな業務に携わる仕事だということがわかっていただけたと思います。Webサイト制作は技術の移り変わりが早く、デザインのトレンドも変化します。Webデザイナーとして成長意欲があり、向上心をもって取り組める人が向ている職業といえるでしょう。


超初心者のためのReactチュートリアル

$
0
0

超初心者のためのReactチュートリアル

この記事はReactを学び始めたReact超初心者の私が今からReact学習を始める超初心者の方のために書いたものです。
学習していく中で躓いた所を挙げ、その解決方法を書いていきます。

この記事の対象となる方

1.Reactを学び始めた方
2.TypeScriptを使ってReactを学びたい方
3.Reactでコンポーネント間のデータの受け渡しを学びたい方
4.Reactで簡単なアプリを作りたい方
5.Reactでコンポーネントの表示をボタンなどで切り替えたい方

TypeScriptとReactを使った簡単なアプリを作りながら、私がReactで躓いてしまった所を紹介し、その問題を解決していきます!
※最低限Reactチュートリアルに目を通しておくと理解しやすいと思います。

では、はじめましょう!

事前準備

まずは開発環境を整えます。
以下の項目を実施していきましょう。

1.npmのインストール
Node.jsをインストールするとnpmが使えるようになるので以下からインストールしてください。Reactの環境構築に必須なので必ずインストールして下さい。
Node.jsインストール

2.React&TypeScriptの開発環境を構築
ReactはHTMLやCSSなどとは違い、ブラウザとエディターがあれば開発できる訳ではありません。いろいろなファイルを用意しておかないと使うことができません。TypeScriptも同様です。ブラウザはReactやTypeSctriptを直接認識することができません。なので、ブラウザが認識してくれるように書いたコードを変換する必要があります。このように、あるプログラミング言語から違うプログラミング言語に変換することをトランスパイルというらしいです。

このトランスパイルを行いブラウザでReactで作ったアプリを表示するための環境を作ります。
ここからnpmを多用するので必ずインストールしておいて下さい。
では、環境構築をはじめましょう!

まず、環境構築したいディレクトリ内にpackage.jsonというファイルを作成して下さい。そして、以下の内容をファイル内に書いて下さい。

package.json
{"scripts":{"start":"webpack-dev-server --hot --inline --watch-content-base --content-base ./dist --open --history-api-fallback","build":"webpack","watch":"webpack -w","gulp":"gulp sass:watch"},"devDependencies":{"@types/react-router-dom":"^5.1.3","gulp":"^4.0.2","gulp-sass":"^4.0.2","gulp-sass-glob":"^1.1.0","ts-loader":"^5.4.3","typescript":"^3.4.4","webpack":"^4.30.0","webpack-cli":"^3.3.1","webpack-dev-server":"^3.10.3"},"dependencies":{"@types/react":"^16.8.14","@types/react-dom":"^16.8.4","react":"^16.8.6","react-dom":"^16.8.6","react-router":"^5.1.2","react-router-dom":"^5.1.2"},"private":true}

その後ターミナルを開き、このpackage.jsonがあるディレクトリに移動し、

npm install

を実行して下さい。これでディレクトリ内に必要なモジュールがインストールされます。インストール後に

run `npm audit fix` to fix them, or `npm audit`for details

のように表示された場合、使用しているモジュールに脆弱性のあるものが含まれているということなので、

npm audit fix

を実行して脆弱性のあるモジュールをバージョンアップして下さい。

次に同じディレクトリ内にtsconfig.jsとwebpack.config.jsを作成して下さい。

tsconfig.json
{"compilerOptions":{"sourceMap":true,//TSはECMAScript5に変換"target":"es5",//TSのモジュールはESModulesとして出力"module":"es2015",//JSXの書式を有効に設定"jsx":"react","moduleResolution":"node","lib":["es2020","dom"]}}
webpack.config.js
module.exports={// モード値を production に設定すると最適化された状態で、// development に設定するとソースマップ有効でJSファイルが出力されるmode:"production",// メインとなるJavaScriptファイル(エントリーポイント)entry:"./src/main.tsx",// ファイルの出力設定output:{//  出力ファイルのディレクトリ名publicPath:"/js/",path:`${__dirname}/dist/js`,// 出力ファイル名filename:"main.js",},module:{rules:[{// 拡張子 .ts もしくは .tsx の場合test:/\.tsx?$/,// TypeScript をコンパイルするuse:"ts-loader"}]},// import 文で .ts や .tsx ファイルを解決するためresolve:{extensions:[".ts",".tsx",".js",".json"]},performance:{maxEntrypointSize:500000,maxAssetSize:500000,},};

最後にdistディレクトリとsrcディレクトリを作成して下さい。
また、dist内にはjsディレクトリを作成して下さい。
distはhtml/css/jsファイルを格納する場所です。
srcはTypeScriptのファイルを格納する場所です。
現在のディレクトリの構成が以下のようになっていれば環境構築は完了です。

root
├── dist
│   └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
├── tsconfig.json
└── webpack.config.js

この構成の詳しい内容は割愛しますが、これでReactとTypeScriptを使った最低限の開発環境が完成しました。
この構築は以下のサイトの構築を参考にし、少しだけ手を加えています。
引用元:最新版TypeScript+webpack 4の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き)

Reactが動くことを確認しよう!

環境構築が完了したので、簡単なコードを書いてReactがブラウザで動くことを確認しましょう。

まず、以下のindex.htmlをdistディレクトリ内に作成します。
main.jsはTypeScriptをトランスパイルしたファイルです。
今はまだTypeScriptファイルがないため、main.jsはありません。

index.html
<!DOCTYPE html><htmllang="ja"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>React Sample App</title><script defersrc="js/main.js"></script></head><body><divid="app"></div></body></html>

はい!、ここで私の躓きポイントです!!!

<script defersrc="js/main.js"></script>

ここでdeferが指定されていると思います。本環境でReactをブラウザで表示させようとするとdeferなしではmain.jsが読み込まれずReactのコンポーネントがレンダリングされませんでした。ちゃんと確認できている訳ではありませんがdeferなしの場合、レンタリングするための領域<divid="app></div>"が読み込まれるより前にmain.jsが実行されたため、レンダリングされなかったのだと思います。deferはHTMLが全て読み込まれた後にjsを実行するため、この問題を解決できたのだと思います。
この後、TypeScriptファイルを作成してReactが動くことを確認するので、その時にdeferを外してみて下さい。恐らく、ブラウザに何も表示されないと思います。

ここからはTypeScriptでReactを使っていきます!
main.tsxとtest.tsxを作成して動作確認します。
main.tsxはscrディレクトリに保存して下さい。
test.tsxはscrディレクトリ内にscreensというディレクトリを作成してそこに保存して下さい。

ここまででディレクトリは以下の構成になっているはずです。

root
├── dist
│   ├── index.html
│   └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── main.tsx
│   └── screens
│       └── Test.tsx
├── tsconfig.json
└── webpack.config.js

保存した後に、ターミナルで以下のコードを入力し実行して下さい。

npm start

実行後、ブラウザにテストと表示されると成功です!

main.tsx
//reactとreact-domの機能を使うためにインポートします import*asReactfrom'react';import*asReactDOMfrom'react-dom';//Testコンポーネントをインポートしてmain.tsx内で使用できるようにしています。importTestfrom'./screens/Test';//React.Componentは上でReactをインポートしたことによって使用できます。//これはReactのコンポーネントをAppという名前で作成するためのものです。//class 任意の名前 extends React.Componentで作成します。classAppextendsReact.Component{//Reactで画面に表示させたいときは以下のようにrender,returnを使います。render(){return(<divclassName="container"><Test/>//JSX構文</div>
);}}//ReactDomも上でReact.Domをインポートしたことによって使用できます。//このコードはindex.html内のid="app"をもつ要素内にAppコンポーネントをレンダリングするという意味です。ReactDOM.render(<App/>,document.querySelector('#app'));
Test.tsx
import*asReactfrom'react';classTestextendsReact.Component{render(){return(<h1>テスト</h1>
);}}//このコードを書くことにより他のファイルからTestコンポーネントを使用できるようになります。exportdefaultTest;

tsxについて

拡張子.tsxはTypeScriptとJSXという構文を同時に使う時に用いる拡張子です。
JSXを使うときは拡張子を.tsxにして下さい。
JSXについてはReactチュートリアルで確認して下さい。

ウサギとカメゲームを作ろう!

テストで動作確認できたので実際にアプリを作っていきます。
今回はウサギとカメが闘うゲームを作ります。

構成は
ゲームスタート画面
バトルフィールド画面
結果画面 
の3つの画面を作っていきます。

完成したアプリはこのようになっています。
ウサギとカメ
このアプリを作りながらReactを学んでいきましょう。

reactではブラウザをリロードせずに簡単に画面の切り替えを行えます。
その機能を使って3つの画面を作っていきます。
今回はHTML、CSSの解説はしないのでGitHubからCSSファイルをクローンもしくはコピーしておいて下さい。
CSSファイル
また、画像ファイルもダウンロードしておいて下さい。
画像ファイル

状態を持たないコンポーネントもあり、そのときは関数コンポーネントを使用した方がいいと思うのですが、
今回はClassコンポーネントで統一します。

スタート画面

最初にスタート画面を作っていきます。
機能としてはタイトルを表示してGameStartボタンを押すとバトルフィールド画面に遷移するという簡単なものです。

早速実装していきましょう!
まず、動作確認で作成したmain.tsxを以下のように書き換えます。
先に実装するコンポーネントをmain.tsxに追加しておきます。

main.tsx
import*asReactfrom'react';import*asReactDOMfrom'react-dom';import{BrowserRouter,Route,Switch}from'react-router-dom';importGameStartfrom'./screens/gamestart';classAppextendsReact.Component{render(){return(<BrowserRouter><Switch><Routeexactpath="/"component={GameStart}/>
<Routeexactpath="/field/"component={Field}/>
<Routeexactpath="/Turtle_win/"component={TurtleWin}/>
<Routeexactpath="/Rabbit_win/"component={RabbitWin}/>
</Switch>
</BrowserRouter>
);}}ReactDOM.render(<App/>,document.querySelector('#app'));

<BrowserRouter>,<Switch>,<Route>が新しく出てきたと思います。
これはReactで画面の切り替えを実装する際に使います。

<BrowserRouter><Switch><Routeexactpath="/"component={GameStart}/>
</Switch>
</BrowserRouter>

この部分はURLが"/"となったときcomponent GameStartを表示するよという意味です。
この環境ではlocalhost:8080で動作確認しているので、
localhost:8080/のときGameStartを表示します。

react-router-domで画面を切り替える際に注意点が一つあります。
react-router-domの画面切り替えではデフォルトの状態だと直接URLを指定するとページが取得できません。
例えばlocalhost:8080/field/
のように直接fieldに遷移しようとした時などです。
しかし、この環境ではローカル環境のサーバー設定で
この問題を回避しています。

package.json
{"scripts":{"start":"webpack-dev-server --hot --inline --watch-content-base --content-base ./dist --open --history-api-fallback",

package.json内の--history-api-fallbackの部分です。
サーバー側でフォールバックの設定をしてあげることで
localhost:8080/field/のように直接URLを指定してもそのページ移動できます。
これはローカルでの設定なので、もし本番の環境に移行した場合はそのサーバーでフォールバックを設定して下さい。
今回はすでに設定してあるのでこのような仕様があるといことを覚えておいて下さい。

次にGameStartコンポーネントを作成していきます。
GameStart.tsxはscreensディレクトリに保存して下さい。

GameStart.tsx
import*asReactfrom"react";import{Link}from'react-router-dom';classGameStartextendsReact.Component{render(){return(//classの代わりにclassNameでクラス名を指定<divclassName="l-start"><h1className="p-start__title -view">ウサギとカメ</h1>
<buttonclassName="p-start__button -view"><Linkto="/field"className="p-start__link">GameStart</Link></button></div>
);}}exportdefaultGameStart;

import { Link } from 'react-router-dom';でimportしているLinkは画面を遷移させたい時に使います。
ブラウザ上ではaタグとして表示されます。
使い方は、要素をLinkでかこい、to="遷移先"で遷移するURLを指定します。
今回はボタンを押すことでバトルフィールドに遷移したいので、to="/field"を指定しています。
次にclassNameについて説明します。
要素にクラス名をつける場合、class="クラス名"で指定すると思うのですが、Reactでclassは別のところで使用しているため、
クラス名のためにclassが使用できません。ですから、Reactでクラス名を指定する際はclassNameを使います。

ファイルを保存したら以下のコマンドを実行して下さい。
スタート画面が表示されると思います。

npm start

バトルフィールド画面

バトルフィールドはGameStartコンポーネントからから遷移します。
移動後ウサギとカメのバトルが始まりましすが、この画面は3つのコンポーネントでできています。
1.Fiiedコンポーネント
2.HPコンポーネント
3.Commentコンポーネント

この3つの関係は
Fieldコンポーネントが親コンポーネントでHP、Commentコンポーネントがその子コンポーネントになっています。
Fieldコンポーネントで計算した値などを子コンポーネントに渡して画面の表示を切り替えていきます。

では、まずFieldコンポーネントとHPコンポーネントを作成します。
以下のコードからTypeScriptならではの書き方が出てきますが一旦スルーして下さい。
すぐに説明します。

Field.tsx
import*asReactfrom"react";importHPfrom"./HP";interfaceFieldState{TurtleHP:number;RabbitHP:number;}classFieldextendsReact.Component<{},FieldState>{constructor(props:{}){super(props);this.state={TurtleHP:5,RabbitHP:5,}}render(){interfaceCharacterHP{width:string;}letTurtleHP:CharacterHP={width:`${this.state.TurtleHP}rem`,}letRabbitHP:CharacterHP={width:`${this.state.RabbitHP}rem`,}return(<divclassName="l-field"><divclassName="p-field"><divclassName="p-field__wrapper"><divclassName="p-field__character-box -turtle"><imgsrc={`${window.location.origin}/images/turtle.png`}alt="キャラクターの画像"className="p-field__character -turtle"/><HPCharacterHP={TurtleHP}/>
<buttonclassName="p-field__button -view">たたかう</button>
</div>
<divclassName="p-field__character-box -rabbit"><HPCharacterHP={RabbitHP}/>
<imgsrc={`${window.location.origin}/images/rabbit.png`}alt="キャラクターの画像"className="p-field__character -rabbit"/></div>
</div>
</div>
</div>
);}}exportdefaultField;
HP.tsx
import*asReactfrom"react"interfaceHPProps{CharacterHP:{},}classHPextendsReact.Component<HPProps,{}>{render(){return(<divclassName="l-hp"><p>HP:</p>
<divclassName="p-hp__box"><divclassName="p-hp__bar -view"style={this.props.CharacterHP}></div>
</div>
</div>
);}}exportdefaultHP;

ここからは細かく解説していきます。
FieldコンポーネントはカメとウサギのHPを保存しています。
このように何か値を保存しておきたい場合はstateを使います。

stateを使う場合はクラスコンポーネントの中にコンストラクタを作成します。

constructor(props:{}){super(props);this.state={TurtleHP:5,RabbitHP:5,}}

コンストラクタを使用するときはsuper(props)から始めるのがお約束となっています。
その後にthis.state=...とつなげていくことでstateを使用できます。
ここではカメとウサギのHPを5としてstateに保存しています。
このHPの値を更新していきキャラクターがダメージ受けるたびにHPバーが減っていく機能を実装します。
書き方は

constructor(props:{}){super(props);this.state={任意の名前:任意の値(文字列や数値など),}}

という風に書きます。
ここでTypeScriptの型について軽く説明します。
TypeScriptは静的型付け言語の一種で変数などに型を宣言する必要があります。
宣言の仕方は

constname:string="yamabaku";constage:number=24;

のように宣言します。

コンストラクタの部分ではpropsの後にprops:{}という風に型を宣言しています。
propsは親コンポーネントからデータを受け取る際に使用するのですが、Fieldコンポーネントは親からデータを受け取る必要がないので空のオブジェクトを型として宣言しています。

次にstateの型宣言について説明します。
stateの型を宣言する場合はinterfaceを使います。

interfaceFieldState{TurtleHP:number;RabbitHP:number;}classFieldextendsReact.Component<{},FieldState>{constructor(props:{}){super(props);this.state={TurtleHP:5,RabbitHP:5,}}

クラスを作成する前にinterfaceで型を宣言し、
class Field extends React.Component<{},FieldState>
のように書くことでクラス内で宣言した型を使用できます。
{}の部分はpropsのinterfaceで使用しますが今回はpropsがないため
空のオブジェクトを入れています。
型については以下の記事を参考にしました。
参考:TypeScriptの型入門

次にrender部分を解説していきます。

render(){interfaceCharacterHP{width:string;}letTurtleHP:CharacterHP={width:`${this.state.TurtleHP}rem`,}letRabbitHP:CharacterHP={width:`${this.state.RabbitHP}rem`,}

ここのコードはHPコンポーネントに送るためのオブジェクトを作成しています。
Fieldコンポーネントのstateを使用するときはthis.state.TurtleaHP
というように使います。
カメとウサギがダメージを受けたときここのHPが変化し、その変化したHPをHPコンポーネントに送ることで
HPバーの表示を変化させます。

次にreturn部分です。

return(<divclassName="l-field"><divclassName="p-field"><divclassName="p-field__wrapper"><divclassName="p-field__character-box -turtle"><imgsrc={`${window.location.origin}/images/turtle.png`}alt="キャラクターの画像"className="p-field__character -turtle"/><HPCharacterHP={TurtleHP}/>
<buttonclassName="p-field__button -view">たたかう</button>
</div>
<divclassName="p-field__character-box -rabbit"><HPCharacterHP={RabbitHP}/>
<imgsrc={`${window.location.origin}/images/rabbit.png`}alt="キャラクターの画像"className="p-field__character -rabbit"/></div>
</div>
</div>
</div>
);

まずimagタグのpathのですが、Reactを本番環境(実際のサーバー上で動作させる)ときに普通にpathを指定するだけでは読み込んでくれません。
しかし、以下のように指定してあげると読み込んでくれます。
src={${window.location.origin}/images/turtle.png}
ここはローカル環境でのみ動作させる場合は関係ないのですが、もし本番環境でimgタグがうまく動作していないと思ったらここを思い出して下さい。
他にも画像をimportを使ってコンポーネントに読み込んでからimgタグのsrcを指定する方法もありますので調べてみて下さい。

次にReactのとても便利な機能である、コンポーネント間のデータの受け渡しを解説します。
このコードでデータの受け渡しを行っているのは以下の部分です。
<HP CharacterHP = {TurtleHP} />
ここはHPコンポーネントをレンダリングする時にHPコンポーネントにTurtleHPというデータを渡してレンダリングしてねという意味です。
このCharacterHPがHPコンポーネントのpropsになります。propsは先ほども説明したように親から子に渡されたデータです。ここではFieldが親でHPが子ですね。

ここでも私が躓いたポイントがあります。

<HP CharacterHP = {TurtleHP} />
という風に記述し、子コンポーネントにデータを渡す際は、その子コンポーネントで以下のようにinterfaceを使ってpropsの型を宣言しておく必要があります。

HP.tsx
interfaceHPProps{CharacterHP:{},}

こうしておかないとField.tsxをコンパイルする際にデータを送り先がないよということでエラーが出てしまいます。

例えば上のinterfaceを削除してコンパイルすると

 TS2769: No overload matches this call.
  Overload 1 of 2, '(props: Readonly<{}>): HP', gave the following error.
    Type '{ CharacterHP: CharacterHP; }' is not assignable to type'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
      Property 'CharacterHP' does not exist on type'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
  Overload 2 of 2, '(props: {}, context?: any): HP', gave the following error.
    Type '{ CharacterHP: CharacterHP; }' is not assignable to type'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
      Property 'CharacterHP' does not exist on type'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.

ERROR in /Users/username/Desktop/Turtle-VS-Rabbit/src/screens/Field.tsx
./src/screens/Field.tsx
[tsl] ERROR in /Users/username/Desktop/Turtle-VS-Rabbit/src/screens/Field.tsx(38,33)
      TS2769: No overload matches this call.
  Overload 1 of 2, '(props: Readonly<{}>): HP', gave the following error.
    Type '{ CharacterHP: CharacterHP; }' is not assignable to type'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
      Property 'CharacterHP' does not exist on type'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
  Overload 2 of 2, '(props: {}, context?: any): HP', gave the following error.
    Type '{ CharacterHP: CharacterHP; }' is not assignable to type'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
      Property 'CharacterHP' does not exist on type'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.

のようなエラーが出ます。
このようなエラーが出た場合はデータを渡す子コンポーネントでpropsの型を宣言しているか確認してみて下さい。

ここで一旦HPコンポーネントの解説に移りましょう。
HPコンポーネントは先に説明したようにFieldコンポーネントからCharacterHPを受け取ります。
その値は以下のstyleの値として使用します。
propsを使う際もstateと同様にthis.props.CharacterHPというように指定することでそのデータを扱うことができます。

classHPextendsReact.Component<HPProps,{}>{render(){return(<divclassName="l-hp"><p>HP:</p>
<divclassName="p-hp__box"><divclassName="p-hp__bar -view"style={this.props.CharacterHP}></div>
</div>
</div>
);}}

HPバーのstyle属性をpropsで変動させることによりHPバーが減っていく動きを作っています。
Reactではstateやpropsが変化したことを感知して自動でレンダリングし直してくれます。
ここではその機能を使っています。
ReactのコンポーネントのCSSをどのように当てるかは以下の記事が大変参考になりました。
参考:Reactのコンポーネントのスタイリングをどうやるか

それではFieldコンポーネントに戻ってHPの値を変化させる機能を実装しましょう。
以下のコードではキャラクターの残りHPと攻撃したキャラクターの名前を更新し、残りHPが0になったらResult画面へ遷移する関数、攻撃したキャラクターとそのダメージを表示するCommentコンポーネントを追加しています。
以下で細かく解説していきます。

Field.tsx
import*asReactfrom"react";importHPfrom"./HP";importCommentfrom"./Comment";interfaceFieldState{TurtleHP:number;RabbitHP:number;//追加Damage:number;name:string;ShowFlag:boolean;ClickFlag:boolean;}classFieldextendsReact.Component<{},FieldState>{constructor(props:{}){super(props);this.state={TurtleHP:5,RabbitHP:5,//追加Damage:null,name:'',ShowFlag:false,ClickFlag:true,}this.RabbitAttack=this.RabbitAttack.bind(this)}//カメが与えるダメージを決めウサギの残りHPを計算する関数TurtleAttack():void{if(this.state.ClickFlag){this.setState({ClickFlag:false});constDamage:number[]=[10,15,20,25];letTurtleAttack=Damage[Math.floor(Math.random()*Damage.length)];this.setState({Damage:TurtleAttack});this.setState({name:'カメイ・ウェザー'})letRestHP=this.state.RabbitHP-TurtleAttack/20;if(RestHP>0){this.setState({RabbitHP:RestHP});}else{this.setState({RabbitHP:0});location.href="/Turtle_win"}setTimeout(this.RabbitAttack,800);}else{this.setState({ClickFlag:false});}}//ウサギが与えるダメージを決めカメの残りHPを計算する関数RabbitAttack():void{constDamage:number[]=[15,15,15,15,15,20,20,20,20,20,100000000];letRabbitAttack=Damage[Math.floor(Math.random()*Damage.length)];this.setState({Damage:RabbitAttack});this.setState({name:'バニー・パッキャオ'})letRestHP=this.state.TurtleHP-RabbitAttack/20;if(RestHP>0){this.setState({TurtleHP:RestHP});}else{this.setState({TurtleHP:0});setTimeout(()=>{location.href="/Rabbit_win"},500);}this.setState({ClickFlag:true});}render(){interfaceCharacterHP{width:string;}letTurtleHP:CharacterHP={width:`${this.state.TurtleHP}rem`,}letRabbitHP:CharacterHP={width:`${this.state.RabbitHP}rem`,}//追加letShowDamage=this.state.ShowFlag?<CommentDamage={this.state.Damage}name={this.state.name}/> : '';
return(<divclassName="l-field"><divclassName="p-field"><divclassName="p-field__wrapper"><divclassName="p-field__character-box -turtle"><imgsrc={`${window.location.origin}/images/turtle.png`}alt="キャラクターの画像"className="p-field__character -turtle"/><HPCharacterHP={TurtleHP}/>
{/* onClick追加 */}<buttonclassName="p-field__button -view"onClick={()=>{this.TurtleAttack();this.setState({ShowFlag:true});}}>たたかう</button>
</div>
<divclassName="p-field__character-box -rabbit"><HPCharacterHP={RabbitHP}/>
<imgsrc={`${window.location.origin}/images/rabbit.png`}alt="キャラクターの画像"className="p-field__character -rabbit"/></div>
</div>
</div>
{/* 追加 */}{ShowDamage}</div>
);}}exportdefaultField;

まず、interfaceで宣言するstateが増えているのがわかると思います。
以下の目的で追加しています。

interfaceFieldState{TurtleHP:number;RabbitHP:number;Damage:number;//相手に与えるダメージを保存name:string;//攻撃したキャラクターの名前を保存ShowFlag:boolean;//Commentコンポーネントの表示、非表示を制御ClickFlag:boolean;//ボタンクリックの連打を防ぐためのものです}

次に追加した関数、TurtleAttack (),RabbitAttack ()について解説します。
この関数はrender内のbuttonがクリックされた時に呼び出されます。

<buttonclassName="p-field__button -view"onClick={()=>{this.TurtleAttack();this.setState({ShowFlag:true});}}>たたかう</button>

このボタンはクッリクした時にTurtleAttack()を呼び出し、this.setState({ShowFlag:true}により
ShowFlagを更新します。ShowFlagはCommentコンポーネントで解説します。

まずボタンを押したときの流れを確認します
ボタンクリック→TurtleAttack()→ダメージ、残りHP計算→RabbitAttack()→ダメージ、残りHP計算...→結果画面に遷移という処理になっています。
結果画面は最後に実装します。

まず、関数にコメントを追加し解説していきます。

その前に1つ説明しておきます。
stateの値を更新するためには必ず
this.setState({stateの名前: 更新する値})
というようにstateを更新します。
これを使って以下の関数では値を更新していきます。
では関数をみていきましょう。

TurtleAttack():void{//ClickFlagを設けこのフラグがtrueの間のみクリックを受け付けます。//これによりボタン連打による誤動作を防ぎます。if(this.state.ClickFlag){//ここでClickFlagをfalseとすることでこの関数の実行中はボタンのクリックを受け付けませんthis.setState({ClickFlag:false});//カメが与えるダメージの配列を作成します。constDamage:number[]=[10,15,20,25];//上の配列からランダムに値を取り出し、カメがウサギに与えるダメージを決めます。letTurtleAttack=Damage[Math.floor(Math.random()*Damage.length)];//そのダメージをと攻撃する側のキャラクターの名前を保存します。//この2つの値はCommentコンポーネントに送り、攻撃時のコメントとして表示します。this.setState({Damage:TurtleAttack});this.setState({name:'カメイ・ウェザー'})//ランダムで取り出したダメージを現在のHPから引いて残りのHPを計算しています。//20で割っているのはHPを100と設定しているからです。HPバーの初期値は5rem(50px)です//widthを10remとすると大きすぎるので、20で割って5remにしています。//ダメージの値は一桁より二桁の方が見栄えがいいと思ったのでこのように計算しています。letRestHP=this.state.RabbitHP-TurtleAttack/20;if(RestHP>0){//残りHPが0以上の場合は上で計算した値をそのまま現在のHPとして保存します。this.setState({RabbitHP:RestHP});}else{//残りHPが0以下になった場合はHPを0とし結果画面に遷移させます。this.setState({RabbitHP:0});location.href="/Turtle_win"}//カメの与えたダメージを0.8秒間表示してウサギの攻撃に移ります。//ウサギの攻撃もカメと同様に計算し、最後にClickFlagをtrueにしてボタン操作を再受付けします//ウサギの攻撃はClickFlagをtrueにする以外ほぼ同じなので説明は省きます。setTimeout(this.RabbitAttack,800);}else{this.setState({ClickFlag:false});}}

TurtleAttack ()は上記のようになっています。
ここでまた私が躓いたポイントです。
TurtleAttack ()のsetTimeoutのなかでthis.RabbitAttack()を呼び出しています。
ここはそのままだとうまく動作しません。thisの意味が変わってしまっているからです。
ですから、constractor内で以下のようにthisを固定してあげる必要があります。

constructor(props:{}){super(props);this.state={TurtleHP:5,RabbitHP:5,Damage:null,name:'',ShowFlag:false,ClickFlag:true,}this.RabbitAttack=this.RabbitAttack.bind(this)}

これでネストが深いところでもthisで関数が使えます。
ここは以下のサイトがとても参考になりました。
React のクラスコンポーネントの bind は何を解決しているのか

これで関数の実装は完了です。
次にCommentコンポーネントについてみていきます。
説明済みの部分は省略しています。

render(){//省略letShowDamage=this.state.ShowFlag?<CommentDamage={this.state.Damage}name={this.state.name}/> : '';
return(<divclassName="l-field"><divclassName="p-field"><divclassName="p-field__wrapper"><divclassName="p-field__character-box -turtle"><imgsrc={`${window.location.origin}/images/turtle.png`}alt="キャラクターの画像"className="p-field__character -turtle"/><HPCharacterHP={TurtleHP}/>
<buttonclassName="p-field__button -view"onClick={()=>{this.TurtleAttack();this.setState({ShowFlag:true});}}>たたかう</button>
</div>
<divclassName="p-field__character-box -rabbit"><HPCharacterHP={RabbitHP}/>
<imgsrc={`${window.location.origin}/images/rabbit.png`}alt="キャラクターの画像"className="p-field__character -rabbit"/></div>
</div>
</div>
{ShowDamage}</div>
);}

まず、ShowDamageについて解説します。

letShowDamage=this.state.ShowFlag?<CommentDamage={this.state.Damage}name={this.state.name}/> : '';

ここはShowFlagがtrueのときCommentコンポーネントを表示し、falseのときは何も表示しないという意味です。
これはReactチュートリアルにも出て来るので確認してみて下さい。

buttonをクリックするとShowFlagをfalseからtrueに切り替えます。
この{ShowDamage}をrender内に入れておくことでボタンを押すと表示されるコメント欄を実装できます。

以上でバトルフィールドの実装は終了です。

結果画面の実装

最後に結果画面を実装します。

以下のTurtle_win.tsx,Rabbit_win.tsxをscreens内に作成して下さい。

Turtle_win.tsx
classTurtleWinextendsReact.Component{render(){return(<divclassName="l-winner"><divclassName="p-winner"><pclassName="p-winner__text -view">カメイ・ウェザーの勝ち!!</p>
</div>
</div>
);}}exportdefaultTurtleWin;
Rabbit_win.tsx
import*asReactfrom"react";classRabbitWinextendsReact.Component{render(){return(<divclassName="l-winner"><divclassName="p-winner"><pclassName="p-winner__text -view">バニー・パッキャオ<br/>の勝ち!!</p>
</div>
</div>
);}}exportdefaultRabbitWin;

この画面への遷移はFieldに実装してあります。

関数、TurtleAttack (),RabbitAttack ()内の

location.href="/Turtle_win"location.href="/Rabbit_win"

でキャラクターのHPが0になった時、結果画面に遷移します。

以上で完成です!
お疲れ様でした。

ここまでのディレクトリ構成を確認しておきましょう。

root
├── dist
│   ├── css
│   │   └── style.css
│   ├── images
│   │   ├── rabbit.png
│   │   └── turtle.png
│   ├── index.html
│   └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── main.tsx
│   └── screens
│       ├── Comment.tsx
│       ├── Field.tsx
│       ├── GameStart.tsx
│       ├── HP.tsx
│       ├── Rabbit_win.tsx
│       ├── Test.tsx
│       └── Turtle_win.tsx
├── tsconfig.json
└── webpack.config.js

確認できたらターミナルで以下のコマンドを実行して下さい。

npm start

これで正常に動けば完了です!

最後に

この記事は私のReact学習の復習もかねて作成しました。
今回作ったものは本当に基礎的なものかと思いますが、Reactを学びはじめた私にとってはよくわからないエラーがでたりして大変でした。
Reactを学び始めた人は高い確率で同じエラーが出たりするのではないかと思います。
そのような人たちにこの記事を参考にしていただければ幸いです。
もしこの記事でわからないことがあれば質問して下さい。
出来る限り答えたいと思います。

以上です。
最後までみて頂きありがとうございます。

Web開発の基本知識(メモ用)

$
0
0

背景

フロントサイドのソースを見たら、なんじゃこれ?のこれをメモする目的

jQuery

  • $から始まる
  • ready(function(){})関数

内側の関数は:HTMLの読み込みが終わった後に、実行される。

$(document).ready(function(){  ここに処理記述});

readyが省略可能

上記の例の省略形
$(function(){  ここに処理記述});
  • 各種セレクター

1.要素セレクター:$("li").css("color", "blue");

要素セレクター
<ul><li>要素</li></ul>

2.IDセレクター:$("#myID").css("color", "blue");

IDセレクター
<ul><liid="myID">IDセレクターの場合、"#"を使う</li></ul>

3.クラスセレクター:$(".myClass").css("color", "blue");

クラスセレクター
<ul><liclass="myClass">クラスセレクターの場合、"."を使う</li></ul>

4.子孫セレクター:$(".myClass strong").css("color", "blue");

クラスセレクター
<ul><liclass="myClass"><strong>子孫セレクター</strong>の場合、"space"を使う</li></ul>

5.ユニバーサルセレクター:$(".li *").css("color", "blue");

ユニバーサルセレクター
<ul><li><strong>ユニバーサルセレクター</strong>の場合、"*"を使う</li><li><span>ユニバーサルセレクター</span>の場合、"*"を使う</li></ul>

6.グループセレクター:$("#myId1", #myId3).css("color", "blue");

グループセレクター
<ul><liid="myId1">IDセレクターの場合、","を使う</li><liid="myId2">IDセレクターの場合、","を使う</li><liid="myId3">IDセレクターの場合、","を使う</li></ul>

まとめ

セレクターサンプル備考
要素$("li").css("color", "blue");
ID$("#myID").css("color", "blue");"#"を使う
クラス$(".myClass").css("color", "blue");"."を使う
子孫$(".myClass strong").css("color", "blue");spaceを使う
ユニバーサル$(".li *").css("color", "blue");"*"を使う
グループ$("#myId1", #myId3).css("color", "blue");","を使う

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

$
0
0

100日チャレンジの273日目

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

【HTML】ウィンドウの大きさでレイアウトがグチャる...【CSS】

$
0
0

ウィンドウの大きさ変更でレイアウトがグチャる...

そんな時は、widthで、コンテンツの入ったcontainerを固定しちゃいましょう!

これにより、ウィンドウの幅の変更に対応出来ます

【完全初心者】Web系エンジニアを目指す ~LP模写に挑戦(随時更新)~

$
0
0

【自己紹介(やや長い)】

Web系プログラマーに憧れて
去年の2月に入社したつもりが、
3か月の研修後Web系ではない開発現場に出させてもらう。
(銀行の運用補助ツールのシステム開発)

もがきつつ何とか半年、ExcelVBA,javascript,javaの案件を
基礎もないままサポート修行or担当(上司達には大変お世話になりました)。

半年後、妊娠して退社。

つわりやら妊娠時期ごとに常に付きまとう体調不良、
引っ越し、出産準備や結婚、旦那との生活で色々あったこの一年、
他のアルバイトをしながら
あ、私、Web系プログラマーになりたいんだったと思い出す。

どんな仕事でも、働けるって、本当に幸せなことだと思うなあ。
特に将来性の高い仕事で成長を実感できるのは楽しいこと。


出産後は赤ちゃんの傍で
ノマドWebプログラマーとして働きたいので
これからWeb系エンジニアになるための勉強を始めます
(出産まであと1ヵ月切ったけど…笑)

とりあえず、かなりご無沙汰であったHTML,CSSの復習からスタート。

目標

「LP案件」をバンバン取れるようになりたい。

やったこと(2020.3.25~2020.3.26現在)

・Photoshop無料期間登録
・Photoshopデータからコーディングする方法の動画を見る
・HTML,CSSの動画を見ながら手を動かす
(会社の研修中に社員さん達と作った教材動画 コソコソ)
・時々GoogleCromeで模写参考サイトを覗く。

Viewing all 8773 articles
Browse latest View live