Node.js
reactjs
material-ui

ReactでテールスピンRPGを作るっ!Day#03 ストーリーを表示してみる

これまでのあらすじ

神の啓示を受けて「テールスピンRPG」を作ることを決めたノアnoah_40w.pngが神の御使いロジカlogica_40w.pngの助けを借りて開発に着手!Material-UIを使って楽してデザインを適用したサイトに新たな要素を追加することに・・・

Cardコンポーネントを使ってストーリーを表示する

「ちょっとページが寂しいので何かしらコンテンツを投入したいなぁ。「テールスピン」RPGの最大の特徴はストーリーを書いたり共有したりできることなので、1件のストーリーを表示してみることはできないかな。ダミーのデータで構わないので。」

「もちろんできるケロ。普通にHTML要素をrenderメソッド内に書けばOKケロ。
ただ、せっかくMaterial-UIを適用したんだから、使えそうなコンポーネントがあるか探してみるケロ。例えば・・・これなんかどうケロか?」

「『Card』コンポーネント?ヘッダや画像、タイトル、本文からなるカード型のコンポーネントか。1件の記事を表示するにはちょうどいいかもしれないね。
じゃ、ちょっと入れてみるよ。
まずimport文を追加して・・・。」

import {Card, CardHeader, CardTitle, CardText} from 'material-ui/Card';

「renderメソッドを書き変える・・・。」

render() {
    return (
      <MuiThemeProvider muiTheme={muiTheme}>
        <div>
          <AppBar title="TOLDOT"/>
          <Card>
            <CardHeader title="ノアの記" subtitle="2017.7.22"/>
            <CardTitle title="ストーリーを表示してみる"/>
            <CardText>
              Cardコンポーネントを使って1件のストーリーを表示してみました。
            </CardText>
          </Card>
        </div>
      </MuiThemeProvider>
    );
  }

「保存・・・と。」

ss0301.png

「おぉ、なるほど。こうなるのね。フォントサイズや色なんかもメリハリがあっていい感じ。」

「一つ一つの要素のスタイルの調整もできるケロが、とりあえずこれでOKとするケロ。
でもってこれをベースに何点か手を加えていくケロ!」

「・・・というと?」

this.stateで状態を管理する

「まず、このままだと永遠に同じストーリーしか表示されないケロね。状況によって違うストーリーを表示させるにはどうすればいいケロか?」

「えーっと、固定の文言が入っている部分を変数に置き換える?」

「その通りケロ。Reactでは『状況によって変化する情報』は『state』というインスタンス変数で管理することになっているケロ。インスタンス変数なのでstateにアクセスする際はthisをつけて『this.state』となるケロ。」

「・・・えと、『インスタンス変数』って何だっけ?」

「オブジェクト自身が持つ変数で、普通はオブジェクトの性質などを表すデータになるケロ。
一般的には『プロパティ』という呼び方をされるケロが、ここで使うとDOM要素のプロパティと混同してややこしくなるので、ここではインスタンス変数と呼ぶことにするケロ。」

「そ、そうなんだね・・・。で、具体的にはどうやって使うの?」

「まずコンストラクタで初期値をセットするケロ。this.stateに状態を表すデータを要素に持つオブジェクトで初期化するケロ。」

class App extends Component {
  // コンストラクタ
  constructor(props) {
    super(props); // 親クラスのコンストラクタの呼び出し

    this.state = {
      storyAbout: 'ノアの記',
      storyDate: '2017.7.22',
      storyTitle: 'ストーリーを表示する',
      storyBody: 'コンストラクタで初期値を設定してみました。'
    };
  }

「ちなみにコンストラクタとはオブジェクトが作成される時に自動的に呼ばれる特殊なメソッドケロ。必要なければ定義しなくてもいいケロが、インスタンス変数に初期値を設定したい場合などには『constructor』という名前で定義するケロ。処理の先頭で親クラスのコンストラクタを呼び出す必要があるので注意するケロ。」

「そういうもんだ、と思っておくね。」

「そしたら、renderメソッドの記述を書き換えるケロ。」

「えと、こうかな?」

render() {
    return (
      <MuiThemeProvider muiTheme={muiTheme}>
        <div>
          <AppBar title="TOLDOT"/>
          <Card>
            <CardHeader title={this.state.storyAbout} subtitle={this.state.storyDate}/>
            <CardTitle title={this.state.storyTitle}/>
            <CardText>
              {this.state.storyBody}
            </CardText>
          </Card>
        </div>
      </MuiThemeProvider>
    );
  }

「いいケロね。表示が変わっているのが確認できるケロか?」

「ホントだ。ストーリーの本文が変わったね。」

ボタンで状態を変えてみる

「ボタンを1個追加して、ボタンを押したら状態が変わり、それに伴って表示も更新されることを確認してみるケロ!」

「わかった。ボタンもMaterial-UIのコンポーネントを使えばいいね?」

「わかってきたケロね。『Raised Button』コンポーネントを使ってみるケロ。」

「OK。
import RaisedButton from 'material-ui/RaisedButton';
を追加して、renderメソッドにRaisedButtonタグを追加・・・。」

render() {
    return (
      <MuiThemeProvider muiTheme={muiTheme}>
        <div>
          <AppBar title="TOLDOT"/>
          <Card>
            <CardHeader title={this.state.storyAbout} subtitle={this.state.storyDate}/>
            <CardTitle title={this.state.storyTitle}/>
            <CardText>
              {this.state.storyBody}
            </CardText>
          </Card>
          <RaisedButton label="状態を変更"/>
        </div>
      </MuiThemeProvider>
    );
  }

「出たけど、ちょっと分かりにくいなぁ・・・。」

ss0302.png

「うん。ちょっと色と位置を調整してみるケロ。」

render() {
    const style = {
      display: 'block',
      width: '120px',
      margin: '20px auto'
    };

    return (
      <MuiThemeProvider muiTheme={muiTheme}>
        <div>
          <AppBar title="TOLDOT"/>
          <Card>
            <CardHeader title={this.state.storyAbout} subtitle={this.state.storyDate}/>
            <CardTitle title={this.state.storyTitle}/>
            <CardText>
              {this.state.storyBody}
            </CardText>
          </Card>
          <RaisedButton label="状態を変更" secondary={true} style={style}/>
        </div>
      </MuiThemeProvider>
    );
  }

「どうケロか?」

ss0303.png

「あ、わかりやすくなったよ。随分。
でもスタイルの指定の仕方が独特だね・・・。」

「普通にCSSファイルに記述して適用することもできるケロが、コンポーネントのプロパティとしても設定できるケロ。その場合は『style』オブジェクトを作って設定したようにCSSをオブジェクト形式で定義する必要があるケロ。」

「わかった。ちなみに『const』って何だっけ?」

「変数宣言する際のキーワードケロ。一旦値を設定したらプログラムの最後まで変更されない変数の場合にはconst宣言するのがいいケロ。『定数』と呼ばれるタイプのデータになるケロ。」

「そうなんだね。注意するよ。
ところでこのボタンクリックしても何も起こらないけど・・・。」

「まだイベントハンドラを作ってないケロからね。『ボタンが押された』というイベントに呼応して実行される処理を定義するのがイベントハンドラケロ。こういう感じで定義するケロ。」

class App extends Component {
   :
  handleButtonTap = (e) => {
    // ボタンが押されたときの処理を記述
  };

「・・・これって何?どういう構文なんだろうか・・・。」

「『アロー関数』と呼ばれるもので、無名関数の省略記法になるケロ。」

「『無名関数』って?」

「まとめて説明するケロ。」

class X {
  // 通常関数(メソッド)の定義
  method1(a, b) {
     return a + b;
  }

  // 無名関数(メソッド)の定義
  method2 = function(a, b) {
    return a + b;
  };

  // アロー関数(メソッド)の定義
  method3 = (a, b) => {
    return a + b;
  }
}

「関数やメソッドの定義の方法はいろいろあるということだね・・・。違いはあるの?」

「基本的には書き方の違いで、例にあげたような処理だとどれも同じだケロ。
ただ処理中に『this』を書く時、それが何を指すかがアロー関数とそれ以外とでは変わってくるケロ。
イベントハンドラとして使う場合、アロー関数記法で定義する必要があるケロ。」

「そうなんだね。とりあえず『イベントハンドラの定義はアロー関数で書く』と覚えておくよ。」

「じゃ、イベントハンドラが用意できたので呼び出す処理を書くケロ。ボタンコンポーネントの『onClick』プロパティの値にイベントハンドラを指定するケロ!」

<RaisedButton label="状態を変更" secondary={true} style={style}
  onClick={this.handleButtonTap}/>

「これでボタンと処理がつながったケロ。じゃノア『ボタンがタップされたらストーリーの内容を変更する。』処理をイベントハンドラに書いてみるケロ。」

「OK!こうかな。」

handleButtonTap = (e) => {
  this.state.eventBody = 'ボタンが押されたよ。';
};

「じゃあボタンをクリックしてみるケロ。」

「うん。あれっ?何も変わらないな。ロジカ、イベントハンドラが呼ばれていないんじゃないの?」

「確認用のコードを入れてみるケロ。」

handleButtonTap = (e) => {
  console.log('Call handleButtonTap');
  this.state.storyBody = 'ボタンが押されたよ。';
};

「ディベロッパーツールの『Console』タブに『Call handleButtonTap』と表示されれば、イベントハンドラは呼ばれていることがわかるケロ。」

「やってみるね。あ、表示されてるね。イベントハンドラは確かに呼ばれているか・・・。」

「ConsoleタブにWarningが表示されているケロ。なんて書かれているケロか?」

「え〜と、『stateの内容を直接変えちゃダメダメ。setState()を使うべし。』かな。」

「その通りケロ。this.stateに直接代入するだけじゃ何も起こらないケロ。this.setState()を使って値を更新することにより、変更された値でreder()が呼ばれ、再描画されるケロ。
こういう風に書くケロ。」

handleButtonTap = (e) => {
this.setState({storyBody: 'ボタンが押されたよ。'});
};

「ホントだ、変わった!なるほどこう書くのね。」

「stateを変更するときは『setState()』を使う、これがReactの基本のキの一つケロ。」

「うっかり直接代入しちゃいそうだね。覚えておくよ。」

「じゃついでにsetStateの落とし穴、もう1つ伝えておくケロ。」

「まだあるんだね。」

「まず、ストーリー関連のデータをオブジェクトを使って1つにまとめるケロ。分かりやすくなって冗長さも減るケロ。」

「えと、どうすればいい?」

「こんな感じケロ。」

constructor(props) {
   :
  this.state = {
    story: {
      about: 'ノアの記',
      date: '2017.7.22',
      title: 'ストーリーを表示する',
      body: 'コンストラクタで初期値を設定してみました。'
    }
  };
}
 :
render() {
   :
  const story = this.state.story;

  return (
   :
    <Card>
      <CardHeader title={story.about} subtitle={story.Date}/>
        <CardTitle title={story.title}/>
        <CardText>
         {story.body}
        </CardText>
    </Card>
   :
 );
}

「ほんとだ。スッキリしたね。」

「イベントハンドラの処理を書き変えてみるケロ。」

「えと、こうかな?」

handleButtonTap = (e) => {
  this.setState({story.body: 'ボタンが押されたよ。'});
};

「文法エラーか・・・こういう書き方はできないんだね・・・。
じゃぁ一旦stateからstoryオブジェクトを持ってきて・・・こうすればいいかな。」

handleButtonTap = (e) => {
  const story = this.state.story;
  story.body = 'ボタンが押されたよ。';
  this.setState({story: story});
};

「あ、変わった。成功!」

「おめでとうケロ。」

「あれ?自己ツッコミだけど、『const story』って宣言しているのに、『story.body=』で後から値変更していますが、これはエラーにはならないの?」

「オブジェクトの場合、要素の値が変更されてもオブジェクト自身が入れ替わっているわけではないのでエラーにはならないケロ。確かにそれはそれでいいのかという気もするケロが・・・。
その辺は気にしても仕方がないところなので流すケロ。
今回のまとめケロ!」

import React, { Component } from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import AppBar from 'material-ui/AppBar';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import {Card, CardHeader, CardTitle, CardText} from 'material-ui/Card';
import RaisedButton from 'material-ui/RaisedButton';

const muiTheme = getMuiTheme({
  palette : {
    primary1Color: '#656733',
    primary2Color: '#90AD66',
    primary3Color: '#D5EAD8',

    accent1Color: '#B71C1C',
    accent2Color: '#aaaaaa', // トグルのオフの色
    accent3Color: '#EDF2C5',
    textColor: '#333333',
    alternateTextColor: '#ffffff',
  },
});

class App extends Component {
  // コンストラクタ
  constructor(props) {
    super(props); // 親クラスのコンストラクタの呼び出し

    this.state = {
      story: {
        about: 'ノアの記',
        date: '2017.7.22',
        title: 'ストーリーを表示する',
        body: 'コンストラクタで初期値を設定してみました。'
      }
    };
  }

  handleButtonTap = (e) => {
    const story = this.state.story;
    story.body = 'ボタンが押されたよ。';
    this.setState({story: story});
  };

  render() {
    const style = {
      display: 'block',
      width: '120px',
      margin: '20px auto'
    };
    const story = this.state.story;

    return (
      <MuiThemeProvider muiTheme={muiTheme}>
        <div>
          <AppBar title="TOLDOT"/>
          <Card>
            <CardHeader title={story.about} subtitle={story.date}/>
            <CardTitle title={story.title}/>
            <CardText>
              {story.body}
            </CardText>
          </Card>
          <RaisedButton label="状態を変更" secondary={true} style={style}
            onClick={this.handleButtonTap}/>
        </div>
      </MuiThemeProvider>
    );
  }
}

export default App;

次回予告

次回はストーリーデータをデータベースに登録してみるケロ。お楽しみに!