DropboxのMarkdownをちょっと便利に使うツールをつくった

Githubwikiで十分足りるんだけど、ちょっと欲しかったのでつくった!

Dropboxに保存したmarkdownをGFMに対応して表示したり編集したりするツール

Markdown-box

コードはこちら。

github.com

JavaScriptDropboxAPIを叩いてるだけ。せっかくなのでReactで。

機能としては、以下の3つくらい。

  • GFM対応
  • ツリービュー
  • 編集時に画像添付

grails-react-boilerplate で React に入門した - 3日目 Babel, ESLint

前回、React Routerを使ったルーティング、サーバーとのデータ通信までできました。しかし、これまでECMAScript 5 (ES5) でJavaScriptを書いてきましたが、grails-react-boilerplateではECMAScript 2015 (ES2015) でJavaScriptが書かれています。

今回はBabelを使って、ES2015に対応できるようにしてみます。

引き続き、以下の記事とリポジトリを参考に進めます。

uehaj.hatenablog.com

github.com

Babel

以前は6to5と呼ばれていたトランスパイラ。JSXも標準でサポート。本稿ではwebpackのローダの一つとして使用

Babelの説明はこう書かれてます。トランスパイラとはコード変換ツールのようなものっていう理解でいいのでしょうか。Babelを使うとES2015のコードをES5に変換できます。ES2015だけでなく、JSXも変換してくれます。

Babelはプラグインを組み合わせて動作させるようです。そして、複数プラグインをセットにしたものをpresetと呼ぶようです。

Plugins · Babel

今回使いたいpresetは、es2015とreactです。

WebpackからBabelを使えるようにしましょう。Webpackのloaderとして、babel-loaderが使えます。

GitHub - babel/babel-loader: Webpack plugin for Babel

Babelのオプションで使用するpresetを選べます。webpack.config.jsにloader: 'babel?presets[]=react,presets[]=es2015'というように書いてもいいですが、.babelrcというファイルに書くこともできます。.babelrcを作成しましょう。

// .babelrc
{
  "presets": ["es2015", "react"]
}

npmでモジュールをインストールします。

$ npm install babel-core babel-preset-es2015 babel-preset-react babel-loader --save-dev

webpack.config.jsでloaderの設定をjsxからbabelへ変更します。

// react-app/hot.webpack.config.js
loaders: ['react-hot', 'babel'],

これで設定ができました!

ES2015の文法はWebで調べるとして、ReactでES2015を使う場合、React.createClassメソッドを使わずにclassとしてReactコンポーネントを定義できます。

Reusable Components | React

BookIndexPageコンポーネントをES2015で書き換えてみます。

// react-app/src/components/BookIndexPage.js
import React, {Component} from 'react';

import {BootstrapTable, TableHeaderColumn} from 'react-bootstrap-table';
import {Button} from 'react-bootstrap';
import 'react-bootstrap-table/css/react-bootstrap-table-all.min.css';

import * as ajax from '../ajax';
import BookNewDialog from './BookNewDialog';

export default class BookIndexPage extends Component {
  constructor(props) {
    super(props);
    this.state = {booklist: []};
  }
  createBook(creatingBook) {
    this.setState({showNewDialog: false});
    ajax.createBook(creatingBook, (_) => {
      this.reloadData();
    }, (err) => {
      console.log("error");
    });
  }
  reloadData() {
    ajax.getBooks((data) => {
      this.setState({ booklist: data });
    });
  }
  showNewDialog() {
    this.setState({showNewDialog: true});
  }
  hideNewDialog() {
    this.setState({showNewDialog: false});
  }
  componentDidMount() {
    ajax.getBooks((data) => {
      this.setState({ booklist: data });
    });
  }
  render() {
    return (
      <div>
        <h1>Books</h1>
        <Button onClick={this.showNewDialog.bind(this)}>New</Button>
        <BootstrapTable data={this.state.booklist}
                        hover condensed pagination deleteRow
                        selectRow={{
                            mode: 'checkbox',
                            bgColor: "rgb(238, 193, 213)",
                        }}
                        >
          <TableHeaderColumn dataField="id" dataSort={true} isKey={true} >ID</TableHeaderColumn>
          <TableHeaderColumn dataField="title" dataSort={true}>Title</TableHeaderColumn>
          <TableHeaderColumn dataField="price" dataSort={true}>Price</TableHeaderColumn>
        </BootstrapTable>
        <BookNewDialog show={this.state.showNewDialog}
               closeAction={this.hideNewDialog.bind(this)}
               submitButtonAction={this.createBook.bind(this)} />
      </div>
    );
  }
}

webpack-dev-serverを起動してアクセスすると、無事表示されます。

grails-react-boilerplateでは、stage-0というpresetも使っています。これは何でしょうか? 調べてみると、ECMAScriptでは新機能が仕様として認められるかどうかの確度を5段階のStageで表記するようです。

5段階のStage | ECMAScriptとは何か?

Stage0の段階の機能も使いたい場合は、Stage0のpresetも使えるように設定しておく必要があります。

$ npm install babel-preset-stage-0 --save-dev
// react-app/.babelrc
{
  "presets": ["es2015", "stage-0", "react"]
}

ESLint

ESLintはJavaScriptの静的コード解析ツール。早速インストールしてみます。

$ npm install eslint --save-dev

package.jsonにeslintを実行するscriptを追加。

// react-app/package.js
  "scripts": {
    ・・・
    "lint": "eslint src"
  },

これで実行できます。

$ npm run-script lint

たくさんエラーが出ました!ES2015で記述してるので、ESLintにES2015を使ってることを教えてあげないといけないです。ESLintの設定ファイルは .eslintrc です。

ecmaFeaturesでES2015の文法を個別に有効にすることができるようです。env"es6": trueを設定するとmodules以外が有効になるようです(ECMAScript 2015は以前はECMAScript 6という名称で呼ばれてました)。modulesについては個別にecmaFeatures"modules": trueを追加します。JSXも使っているので"jsx": trueも追加。


追記 2016/2/21

ESLintのバージョンが2.xになってから ecmaFeatures の記述を paserOptions に書くようになったみたいです。

"parserOptions": {
  "ecmaFeatures": {
    "jsx": true,
    "modules": true
  }
},

追記ここまで。


Documentation - ESLint - Pluggable JavaScript linter

// react-app/.eslintrc
{
  "env": {
    "es6": true
  },
  "ecmaFeatures": {
    "jsx": true,
    "modules": true
  },
  "rules": {
  }
}

これでnpm run-script lintするとエラーなく実行されます。

.eslintrcにルールを追加していくと色々チェックできるようになります。例えば、セミコロンの省略をチェックするようにするには"semi": 2を追加します。2という数値を指定するとチェックに引っかかった場合にエラーとします。1を指定すると警告です。

List of available rules - ESLint - Pluggable JavaScript linter

// react-app/.eslintrc
"rules": {
  "semi": 2
}

おしまい

今回は、BabelとESLintを導入してみました。ここまでくれば後はモリモリと写経できそうです!

grails-react-boilerplate で React に入門した - 2日目 React-Router, サーバー通信

前回、ホットリロードに対応して、React-Bootstrapを使ってナビゲーションバーを実装(見た目だけ)するところまでやりました。

f:id:bati11:20151231110319p:plain

今回は、ナビゲーションバーをクリックすることで画面を切り替えるようにします。

前回に引き続き、以下の記事とリポジトリを参考に進めます。

uehaj.hatenablog.com

github.com

React-Router

ナビゲーションバーの「Link1」「Link2」でそれぞれ画面を切り替えます。URLで表示するReactコンポーネントを切り替えることで対応します。これはReact-Routerを使うと実現できます。

$ npm install history react-router --save-dev

ReactDOM.renderの第1引数をReact-Routerのコンポーネントに書き換えます。


追記 2016/2/21

React Router 2.x系からhistoryの部分の書き方が変わったようです。

react-router/Histories.md at latest · reactjs/react-router · GitHub

以下のコードの var createBrowserHistory = require('history/lib/createBrowserHistory'); ではなく var browserHistory = require('react-router').browserHistory とします。

追記ここまで。


// react-app/src/index.js
var Router = require('react-router').Router;
var Route = require('react-router').Route;
var IndexRedirect = require('react-router').IndexRedirect;
var createBrowserHistory = require('history/lib/createBrowserHistory');

ReactDOM.render(
  <Router history={createBrowserHistory()}>
    <Route name="TOP" path="/" component={TopLevel}>
      <IndexRedirect from="*" to="link1" />
      <Route path="link1" component={Page1} />
      <Route path="link2" component={Page2} />
    </Route>
  </Router>,
  document.getElementById('root')
);

これで以下のようにルーティングされます。

TopLevelコンポーネントで子要素を表示するように変更します。{this.props.children}を使います。

// react-app/src/components/TopLevel.js
module.exports = React.createClass({
  render: function() {
    return (
      <div>
        <TopLevelNavBar />
        {this.props.children}
        <footer>footer</footer>
      </div>
    );
  }
});

適当にPage1コンポーネントを用意します。

// react-app/src/components/Page1.js
var React = require('react');

module.exports = React.createClass({
  render: function() {
    return (
      <div>
        Page1.
      </div>
    );
  }
});

同じようにPage2コンポーネントも用意します。

server.jsにあるwebpack-dev-serverの設定にhistoryApiFallback: trueを追加。

// react-app/server.js
new WebpackDevServer(webpack(config), {
  ・・・
  historyApiFallback: true
}).listen(3000, 'localhost', function (err, result) {
  ・・・

webpack-dev-serverを再起動してアクセス。ナビゲーションバーのリンクをクリックすると表示されるコンポーネントが切り替わります!

しかし、画面のリロードが発生してしまってます!これは望んでた動きじゃないです。リロードなしで表示されるコンポーネントを切り替えたい!!

これは、リンク要素をReact RouterのLinkコンポーネントで実装することで実現できます。しかし、ナビゲーションバーのリンク要素はReact-BootstrapのNavItemコンポーネントで実装しているため、Linkコンポーネントは使えません。こういう時に使うのがreact-router-bootstrap。

GitHub - react-bootstrap/react-router-bootstrap: Integration between React Router and React-Bootstrap

$ npm install react-router-bootstrap --save-dev

react-router-bootstrapのLinkContainerコンポーネントでNavItem コンポーネントを囲みます。

// react-app/src/components/TopLevel.js
var LinkContainer = require('react-router-bootstrap').LinkContainer;
・・・
<Nav>
  <LinkContainer to={"/link1"}><NavItem>Link1</NavItem></LinkContainer>
  <LinkContainer to={"/link2"}><NavItem>Link2</NavItem></LinkContainer>
</Nav>

これでブラウザのリロードなしでPage1コンポーネントとPage2コンポーネントの切り替えができるようになりました!

Dynamic Children と key

メニューのリンクの数はルーティングの設定から動的に取得できると嬉しいですね。this.props.route を使って取得できます。

この時Routeコンポーネントのpropsを使うと便利です。Routeコンポーネントnameというpropを追加します。

// react-app/src/index.js
<Route name="TOP" path="/" component={TopLevel}>
  <IndexRedirect from="*" to="link1" />
  <Route path="link1" name="page1" component={Page1} />
  <Route path="link2" name="page2" component={Page2} />
</Route>

TopLevelコンポーネントで子供のRouteコンポーネントの一覧をthis.propts.routeを使って取得します。

// react-app/src/components/TopLevel.js
var TopLevel = React.createClass({
        ・・・
        <TopLevelNavbar route={this.props.route} />
        ・・・
});

var TopLevelNavbar = React.createClass({
        ・・・
        <Nav>
          {this.props.route.childRoutes.map(function(item) {
            return (
              <LinkContainer to={"/"+item.path}><NavItem>{item.name}</NavItem></LinkContainer>
            )
          })}
        </Nav>
        ・・・
});

これでナビゲーションバーのリンクが増えた場合でも、Routeコンポーネントを増やすだけで対応できるようになりました。

しかし、コンソールを見てみると以下のような警告メッセージが出力されてます。

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `TopLevelNavBar`. See https://fb.me/react-warning-keys for more information.

動的にコンポーネントのリストを作るような時、keyというpropにユニークな値をつけておいた方が良いようです。こちらが参考になります。 React.jsの地味だけど重要なkeyについて - Qiita

ユニークなkeyを設定しましょう。item.pathならユニークになります。これで警告メッセージも消えます。

// react-app/src/components/TopLevel.js
{this.props.route.childRoutes.map(function(item) {
  return (
    <LinkContainer key={item.path} to={"/"+item.path}><NavItem>{item.name}</NavItem></LinkContainer>
  )
})}

データ一覧表示

次はサーバーからデータを取得して表示します。

grails-react-boilerplateでは、Grailsでサーバーアプリケーションが作られています。起動するには以下のようにします。

$ ./grailsw
grails> run-app

ではまずは、BookIndexPageコンポーネントをつくって、それをルーティングに登録してみましょう。

// react-app/src/components/BookIndexPage.js
var React = require('react');

module.exports = React.createClass({
  render: function() {
    return (
      <div>
        <h1>BookIndexPage</h1>
      </div>
    );
  }
});
// react-app/src/index.js
var BookIndexPage = require('./components/BookIndexPage');

ReactDOM.render(
  <Router history={createBrowserHistory()}>
    <Route name="TOP" path="/" component={TopLevel}>
      <IndexRedirect from="*" to="link1" />
      <Route path="book" name="book-index" component={BookIndexPage} />
      <Route path="link1" name="page1" component={Page1} />
      <Route path="link2" name="page2" component={Page2} />
    </Route>
  </Router>,
  document.getElementById('root')
);

BookIndexPageコンポーネントができました。この中でBookListコンポーネントをつくってそこにサーバーから読み込んだデータを表示したいと思います。

react-bootstrap-tableを使ってテーブル要素をつくります。

React Bootstrap Table

$ npm install react-bootstrap-table --save-dev

データはstateで管理します。ひとまず、固定のデータで。

// react-app/src/components/BookIndexPage.js
var BootstrapTable = require('react-bootstrap-table').BootstrapTable;
var TableHeaderColumn = require('react-bootstrap-table').TableHeaderColumn;
require('react-bootstrap-table/css/react-bootstrap-table-all.min.css');

module.exports = React.createClass({
  getInitialState: function() {
    var data = [
      {"id": 1, "title": "hoge", "price": 200},
      {"id": 2, "title": "fuga", "price": 500}
    ];
    return {booklist: data};
  },
  render: function() {
    return (
      <div>
        <h1>Books</h1>
        <BootstrapTable data={this.state.booklist}
                        hover condensed pagination deleteRow
                        selectRow={{
                            mode: 'checkbox',
                            bgColor: "rgb(238, 193, 213)",
                        }}
                        >
          <TableHeaderColumn dataField="id" dataSort={true} isKey={true} >ID</TableHeaderColumn>
          <TableHeaderColumn dataField="title" dataSort={true}>Title</TableHeaderColumn>
          <TableHeaderColumn dataField="price" dataSort={true}>Price</TableHeaderColumn>
        </BootstrapTable>
      </div>
    );
  }
});

こんな感じになります。

f:id:bati11:20151231130941p:plain

では、サーバーから取得したデータを一覧表示する様にしましょう。サーバーとの通信処理は、ajax.jsという別ファイルに書きます(このへんの設計をFluxにするかどうかを検討するのかな?)。

// react-app/src/ajax.js
var $ = require('jquery');

var urlBase = '/api/';

exports.getBooks = function (callback, callbackError) {
  $.ajax({
    type: 'GET',
    url: urlBase + 'books.json?max=100',
    contentType: 'application/json',
    dataType: 'json',
    cache: false,
    success: function(data) {
      callback(data)
    },
    error: function(xhr, status, err) {
      console.error(xhr, status, err.toString())
      if (callbackError !== undefined) {
        callbackError(err)
      }
    }
  });
}

公式サイトのチュートリアルにも出てきたように、BookIndexPageコンポーネントに componentDidMount メソッドを追加して、サーバーからデータを取得します。

// react-app/src/components/BookIndexPage.js
var ajax = require('../ajax');

module.exports = React.createClass({
  getInitialState: function() {
    return {booklist: []};
  },
  componentDidMount: function() {
    var that = this;
    ajax.getBooks(function(data) {
      that.setState({ booklist: data });
    });
  },
  ・・・

server.jsにproxyの設定を追加して、ローカルで動いているGrailsアプリケーションのAPIを呼ぶようにします。

// react-app/server.js
new WebpackDevServer(webpack(config), {
  ・・・
  proxy: {
    '/api/*': "http://localhost:8080",
  },
}).listen(3000, 'localhost', function (err, result) {

server.jsを再起動して、アクセスするとサーバーから取得したデータが表示されます!

f:id:bati11:20151231132405p:plain

データ登録

データの登録フローをつくります。下の流れのようにします。

  • Newボタンをクリック -> ダイアログが表示される
  • ダイアログ内のフォームに入力して投稿 -> サーバーにデータが登録され、一覧に入力したデータが表示される

ボタンはReact-BootstrapのButtonコンポーネント、ダイアログはReact-BootstrapのModalコンポーネントを使います。では、BookNewDialogコンポーネントをつくりましょう。

// react-app/src/components/BookNewDialog.js
var React = require('react');

var Modal = require('react-bootstrap').Modal;
var Input = require('react-bootstrap').Input;
var Button = require('react-bootstrap').Button;

module.exports = React.createClass({
  callbackSubmitButtonAction: function() {
    this.props.submitButtonAction({
      title: this.refs.title.getValue(),
      price: this.refs.price.getValue()
    });
  },
  getInitialState: function() {
    return {book: null};
  },
  render: function() {
    return (
      <Modal show={this.props.show} onHide={this.props.closeAction}>
        <Modal.Header closeButton>
          <Modal.Title>New Book</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <form ref='form' className="form-horizontal">
            <Input ref="title" type="text" label="Title:" labelClassName="key col-xs-2" wrapperClassName="col-xs-10" defaultValue={this.state.book && this.state.book.title} />
            <Input ref="price" type="text" label="Price:" labelClassName="key col-xs-2" wrapperClassName="col-xs-10" defaultValue={this.state.book && this.state.book.price} />
          </form>
        </Modal.Body>
        <Modal.Footer>
          <Button bsStyle="primary" onClick={this.props.closeAction}>Close</Button>
          <Button bsStyle="success" onClick={this.callbackSubmitButtonAction}>Create</Button>
        </Modal.Footer>
      </Modal>
    );
  }
});

コンポーネントから受け取ったsubmitButtonActionやcloseActionをonClick時のアクションとして指定します。フォームの値はstateを使って管理します。

BookListコンポーネントのJSXを変更して、BookNewDialogコンポーネントを追加します。

// react-app/src/components/BookNewDialog.js
<Button onClick={this.showNewDialog}>New</Button>
<BootstrapTable data={this.state.booklist}
・・・
</BootstrapTable>
<BookNewDialog show={this.state.showNewDialog}
               closeAction={this.hideNewDialog}
               submitButtonAction={this.createBook} />

使用してるメソッドをつくっていきます。

// react-app/src/components/BookIndexPage.js
module.exports = React.createClass({
  createBook: function(creatingBook) {
    this.setState({showNewDialog: false});
    console.log(creatingBook);
  },
  showNewDialog: function() {
    this.setState({showNewDialog: true});
  },
  hideNewDialog: function() {
    this.setState({showNewDialog: false});
  },
  ・・・

ここまででダイアログを表示して、入力された値をcreateBookメソッドで受け取るところまでできました(ダイアログを表示するかどうかのstateを変えるのってFlux的にやるならどうなるんでしょうね)。

f:id:bati11:20151231133852p:plain

あとは、createBookメソッドでサーバーにPOSTすればいいですね。ajax.jsにメソッドを追加します。

// react-app/src/ajax.js
exports.createBook = function (book, callback, callbackError) {
  $.ajax({
    type: 'POST',
    url: urlBase + `books.json`,
    contentType: 'application/json',
    dataType: 'json',
    data: JSON.stringify(book),
    cache: false,
    success: (data) => {
      if (callback !== undefined) {
        callback(data)
      }
    },
    error: (xhr, status, err) => {
      console.error(xhr, status, err.toString())
      if (callbackError !== undefined) {
        callbackError(err)
      }
    }
  });
};

BookListコンポーネントから、ajaxモジュールのメソッドを呼び出すように変更します。thisが・・・。

// react-app/src/component/BookList.js
module.exports = React.createClass({
  createBook: function(creatingBook) {
    this.setState({showNewDialog: false});
    var that = this;
    ajax.createBook(creatingBook, function(){
      that.reloadData();
    }, function(err) {
      console.log("error");
    });
  },
reloadData: function() {
  var that = this;
  ajax.getBooks(function(data) {
    that.setState({ booklist: data });
  });
},
  ・・・

これでデータをサーバーに登録できて、登録したデータがすぐに一覧に追加されます。

おしまい

今回は、React-Router、Dynamic Children と key、サーバーとのHTTP通信をやりました。

grails-react-boilerplateではBabelを使ってES2015でJavaScriptを書けるように対応しています。次回、Babelを使うようにするのとESLintを入れてみます。