20200325のCSSに関する記事は10件です。

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

超初心者のための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はECMAScript 5に変換
    "target": "es5",
    // TSのモジュールはES Modulesとして出力
    "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>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React Sample App</title>
    <script defer src="js/main.js"></script>
</head>
<body>
    <div id="app"></div>
</body>
</html>

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

<script defer src="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 * as React from 'react';
import * as ReactDOM from 'react-dom';
//Testコンポーネントをインポートしてmain.tsx内で使用できるようにしています。
import Test from './screens/Test';

//React.Componentは上でReactをインポートしたことによって使用できます。
//これはReactのコンポーネントをAppという名前で作成するためのものです。
//class 任意の名前 extends React.Componentで作成します。
class App extends React.Component {

//Reactで画面に表示させたいときは以下のようにrender,returnを使います。
render() {
    return (
      <div className="container">
        <Test />//JSX構文
      </div>
    );
  }
}
//ReactDomも上でReact.Domをインポートしたことによって使用できます。
//このコードはindex.html内のid="app"をもつ要素内にAppコンポーネントをレンダリングするという意味です。
ReactDOM.render(<App/>, document.querySelector('#app'));
Test.tsx
import * as React from 'react';

class Test extends React.Component {
    render () {
        return (
            <h1>テスト</h1>
        );
    }
}
//このコードを書くことにより他のファイルからTestコンポーネントを使用できるようになります。
export default Test; 

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 * as React from 'react';
import * as ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import GameStart from './screens/GameStart';
//import Field from './screens/Field';
//import TurtleWin from "./screens/Turtle_win";
//import RabbitWin from "./screens/Rabbit_win";

class App extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <Switch>
           <Route exact path="/" component={GameStart} />
           {/*<Route exact path="/field/" component={Field} />*/}
           {/*<Route exact path="/Turtle_win/" component={TurtleWin} />*/}
           {/*<Route exact path="/Rabbit_win/" component={RabbitWin} />*/}
        </Switch>
      </BrowserRouter>
    );
  }
}


ReactDOM.render(<App/>, document.querySelector('#app'));

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

      <BrowserRouter>
        <Switch>
          <Route exact path="/" 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 * as React from "react";
import { Link } from 'react-router-dom';


class GameStart extends React.Component {
    render() {
        return (
            //classの代わりにclassNameでクラス名を指定
            <div className="l-start">
                <h1 className="p-start__title -view">ウサギとカメ</h1>
                <button className="p-start__button -view"><Link to="/field" className="p-start__link">Game Start</Link></button>
            </div>

            );
    }
}

export default GameStart;

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 * as React from "react"; 
import HP from "./HP";

interface FieldState {
    TurtleHP: number;
    RabbitHP: number;
} 

class Field extends React.Component<{},FieldState> {
    constructor(props:{}) {
        super(props);
        this.state = {
            TurtleHP: 5,
            RabbitHP: 5,
        }
    }

    render () {
        interface CharacterHP {
            width:string;
        }
        let TurtleHP:CharacterHP = {
            width:`${this.state.TurtleHP}rem`,
        }
        let  RabbitHP:CharacterHP = {
            width:`${this.state.RabbitHP}rem`,
        } 
        return (
            <div className="l-field">
                <div className="p-field">
                    <div className="p-field__wrapper">
                        <div className="p-field__character-box -turtle">
                            <img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
                            <HP CharacterHP = {TurtleHP} />
                            <button className="p-field__button -view">たたかう</button>
                        </div>
                        <div className="p-field__character-box -rabbit">
                            <HP CharacterHP = {RabbitHP} />
                            <img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

export default Field;
HP.tsx
import * as React from "react"

interface HPProps {
    CharacterHP: {},
}

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

export default HP;

ここからは細かく解説していきます。
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は静的型付け言語の一種で変数などに型を宣言する必要があります。
宣言の仕方は

const name: string = "yamabaku";
const age: number = 24;

のように宣言します。

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

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

interface FieldState {
    TurtleHP: number;
    RabbitHP: number;
} 

class Field extends React.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 () {
        interface CharacterHP {
            width:string;
        }
        let TurtleHP:CharacterHP = {
            width:`${this.state.TurtleHP}rem`,
        }
        let  RabbitHP:CharacterHP = {
            width:`${this.state.RabbitHP}rem`,
        } 

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

次にreturn部分です。

        return (
            <div className="l-field">
                <div className="p-field">
                    <div className="p-field__wrapper">
                        <div className="p-field__character-box -turtle">
                            <img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
                            <HP CharacterHP = {TurtleHP} />
                            <button className="p-field__button -view">たたかう</button>
                        </div>
                        <div className="p-field__character-box -rabbit">
                            <HP CharacterHP = {RabbitHP} />
                            <img src={`${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
interface HPProps {
    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というように指定することでそのデータを扱うことができます。

class HP extends React.Component<HPProps,{}> {
    render (){
        return(
            <div className="l-hp">
                <p>HP:</p>
                <div className="p-hp__box">
                    <div className="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 * as React from "react"; 
import HP from "./HP";
import Comment from "./Comment";

interface FieldState {
    TurtleHP: number;
    RabbitHP: number;
    //追加
    Damage: number;
    name: string;
    ShowFlag: boolean;
    ClickFlag: boolean;
} 

class Field extends React.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});
            const Damage:number[] = [10,15,20,25];
            let TurtleAttack = Damage[Math.floor(Math.random()* Damage.length)];
            this.setState({Damage: TurtleAttack});
            this.setState({name: 'カメイ・ウェザー'})
            let RestHP = 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{
        const Damage:number[] = [15,15,15,15,15,20,20,20,20,20,100000000];
        let RabbitAttack = Damage[Math.floor(Math.random()* Damage.length)];
        this.setState({Damage: RabbitAttack});
        this.setState({name: 'バニー・パッキャオ'})
        let RestHP = 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 () {
        interface CharacterHP {
            width:string;
        }
        let TurtleHP:CharacterHP = {
            width:`${this.state.TurtleHP}rem`,
        }
        let  RabbitHP:CharacterHP = {
            width:`${this.state.RabbitHP}rem`,
        } 
        //追加
        let ShowDamage = this.state.ShowFlag ? <Comment Damage = {this.state.Damage} name = {this.state.name} /> : '';
        return (
            <div className="l-field">
                <div className="p-field">
                    <div className="p-field__wrapper">
                        <div className="p-field__character-box -turtle">
                            <img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
                            <HP CharacterHP = {TurtleHP} />
                            {/* onClick追加 */}
                            <button className="p-field__button -view" onClick = { () => {this.TurtleAttack(); this.setState({ShowFlag:true});}}>たたかう</button>
                        </div>
                        <div className="p-field__character-box -rabbit">
                            <HP CharacterHP = {RabbitHP} />
                            <img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
                        </div>
                    </div>
                </div>
                {/* 追加 */}
                {ShowDamage}
            </div>
        );
    }
}

export default Field;

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

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

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

<button className="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});

            //カメが与えるダメージの配列を作成します。
            const Damage:number[] = [10,15,20,25];

            //上の配列からランダムに値を取り出し、カメがウサギに与えるダメージを決めます。
            let TurtleAttack = 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にしています。
            //ダメージの値は一桁より二桁の方が見栄えがいいと思ったのでこのように計算しています。
            let RestHP = 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 () {
        //省略
        let ShowDamage = this.state.ShowFlag ? <Comment Damage = {this.state.Damage} name = {this.state.name} /> : '';
        return (
            <div className="l-field">
                <div className="p-field">
                    <div className="p-field__wrapper">
                        <div className="p-field__character-box -turtle">
                            <img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
                            <HP CharacterHP = {TurtleHP} />
                            <button className="p-field__button -view" onClick = { () => {this.TurtleAttack(); this.setState({ShowFlag:true});}}>たたかう</button>
                        </div>
                        <div className="p-field__character-box -rabbit">
                            <HP CharacterHP = {RabbitHP} />
                            <img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
                        </div>
                    </div>
                </div>
                {ShowDamage}
            </div>
        );
    }

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

let ShowDamage = this.state.ShowFlag ? <Comment Damage = {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
class TurtleWin extends React.Component {
    render() {
        return(
            <div className="l-winner">
                <div className="p-winner">
                    <p className="p-winner__text -view">カメイ・ウェザーの勝ち!!</p>
                </div>
            </div>
        );
    }
}

export default TurtleWin;
Rabbit_win.tsx
import * as React from "react";


class RabbitWin extends React.Component {
    render() {
        return(
            <div className="l-winner">
                <div className="p-winner">
                    <p className="p-winner__text -view">バニー・パッキャオ<br/>の勝ち!!</p>
                </div>
            </div>
        );
    }
}

export default RabbitWin;

この画面への遷移は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を学び始めた人は高い確率で同じエラーが出たりするのではないかと思います。
そのような人たちにこの記事を参考にしていただければ幸いです。
もしこの記事でわからないことがあれば質問して下さい。
出来る限り答えたいと思います。

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

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

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

※本稿は弊社ホームページに記載のものを転載しています。
図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デザイナーとして成長意欲があり、向上心をもって取り組める人が向ている職業といえるでしょう。

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

画像のアスペクト比を維持しながら縮小させるmixin

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

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

使い方

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

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

scss
@include img_ratio(padding-top, 300, 200);

になります。

コンパイル後は

css
padding-top: 66.66667%;

こうなりました。

少しだけ解説

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

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

sass:math

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

もっと簡単だった

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

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

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

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

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

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

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

使い方

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

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

scss
@include img_ratio(padding-top, 300, 200);

になります。

コンパイル後は

css
padding-top: 66.66667%;

こうなりました。

少しだけ解説

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

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

sass:math

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

もっと簡単だった

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

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

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

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

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

backgroundの機能で簡単にグラフを作りました。

CodePen

See the Pen make graphs by using background by ryu38 (@ryu38) on CodePen.

参考

「背景色を2色で分割表示する方法」
https://qiita.com/C-3PHobbit/items/1b53d5dd792111ed5a78

「CSSアニメーションで動く背景に!CSSアニメーションバックグラウンド12選」
https://webdesignfacts.net/entry/css-background-animation/

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

チーム開発

チーム開発で違うリポジトリで作業してしまった、その修正で使った記事
違う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
の作業、たったの一二週間なのに忘れて、すぐにやり方が出てこない!!焦る!
クレジットカード登録、変更画面作業中(フロントエンド)
まだどの作業もスムーズにできない、一つ一つ確認、復習しながら進めるため遅い!
チームに迷惑かけないようにしたいが、ものすごく足ひっぱってる。

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

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

100日チャレンジの272日目

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

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

z-indexが効かない

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

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

一般的な対処法をしてもz-indexが効かない(ように見える)

正確に言うと効いてるんだけど効いてないように見えるという話。

z-indexが効かないときの一般的な対処法

  • z-index以外の要因で重ね合わせコンテクストの変更が行われている
  • 親要素のz-indexに影響を受けている
  • positionがstatic

概ねこのあたりだと思うのですが、私は上記いずれを試してもz-indexの効果が確認されず、重ね合わせた時にz-indexの低い要素が高い要素の上に位置していました(るように見えました。)

原因

background-colorの初期値はtransparentなのですが、この値は実は透過色で要素が重なり合った時に下位の要素が視認できてしまいます。
私はデフォルトの背景色は白色だと思っていたので微妙にハマってしまいました。

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

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

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

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

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

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

今回の要件

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

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

items_controller.rb
def new
    @item = Item.new
    @category_parent =  Category.where("ancestry is null")
  end

  def create
    @item = Item.new(item_params)
    if @item.save
      redirect_to root_path 
    else
      render :new
    end
  end
 private

  def item_params
    params.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をイベント元に指定

    //ファイルオブジェクトを取得する
    let files = e.target.files;
    $.each(files, function(index, file) {
      let reader = new FileReader();

      //画像でない場合は処理終了
      if(file.type.indexOf("image") < 0){
        alert("画像ファイルを指定してください。");
        return false;
      }
      //アップロードした画像を設定する
      reader.onload = (function(file){
        return function(e){
          let imageLength = $('#output-box').children('li').length;
          // 表示されているプレビューの数を数える

          let labelLength = $("#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(){
    let targetImageId = $(this).data('image-id');
    // イベント元のカスタムデータ属性の値を取得
    $(`#upload-image${targetImageId}`).remove();
    //プレビューを削除
    $(`[for=item_images${targetImageId}]`).remove();
    //削除したプレビューに関連したinputを削除

    let imageLength = $('#output-box').children('li').length;
    // 表示されているプレビューの数を数える
    if (imageLength ==9) {
      let labelLength = $("#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>`);
    };
  });

バリデーション

item.rb(モデル)
  validate :images_presence
  #バリデーションを呼び出す

  def images_presence
    if images.attached?
      # inputに保持されているimagesがあるかを確認
      if images.length > 10
        errors.add(:image, '10枚まで投稿できます')
      end
    else
      errors.add(:image, '画像がありません')
    end
  end

ActiveStorageは基本のバリデーションが使えません。
なので自分で作りました。

最後に

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

ただし、一つ問題があります。
複数同時選択を行い、プレビューを削除した時に不具合が発生する事です。
と言うのも、同時選択をすると一つのinputに複数の画像の情報が保持されるます。
そしてinputから情報を全て抜き取る事は出来ません。
つまり、あるinputを削除すると複数画像のデータが消えてしまう・・・と言う事です。

対策としては

  • 同時選択を出来ないようにする
    この場合、name属性にも手を加える必要があります。

  • 先にDBに一旦保存してしまう
    ちょっと難しそう

それらは今後試すことにします。

今回は以上です

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