react-blessedを使ってみる

## 概要

[react-blessed](https://github.com/Yomguithereal/react-blessed)は、Reactのblessed.js向けのrendererです。

blessed.jsのウィジェットを、Reactのコンポーネントを使用して描画することができます。

ターミナル上で動作するアプリを作成する際に便利そうだったため、使ってみました。

## 動作環境

* Ubuntu 18.04
* Node.js 8.11.4
* blessed 0.18.1
* react-blessed 0.3.0
* Babel 7

## インストール

```
$ yarn add blessed react react-blessed

$ yarn add @babel/cli @babel/core @babel/plugin-transform-react-jsx @babel/plugin-transform-modules-commonjs @babel/plugin-proposal-class-properties --dev
```

## Hello world

### ビルド設定

.babelrcに必要な設定を記述します

```json:.babelrc
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx"],
    ["@babel/plugin-transform-modules-commonjs"],
    ["@babel/plugin-proposal-class-properties"]
  ]
}
```

### ソースコード

```jsx:src/index.js
import React, { Component } from 'react';
import blessed from 'blessed';
import { render } from 'react-blessed';

class App extends Component {
  render() {
    // blessed.jsのBoxを描画する
    return (
      <box
        width='100%'
        height='10%'
        border={{ type: 'line' }}
        style={{ border: { fg: '#888' } }}>
        Hello, world
      </box>
    );
  }
}

const screen = blessed.screen({
  autopadding: true,
  smartCSR: true,
  title: 'Test',
  fullUnicode: true,
});

// Escapeキーで終了
screen.key(['escape'], (ch, key) => {
  process.exit(0);
});

render(<App></App>, screen);
```

### ビルド

ビルドする際は、package.jsonにスクリプトを記述しておくとよいと思います。

ウォッチしたい場合は、`--watch`をつけます。

```json:package.json
  "scripts": {
    "build": "babel src --out-dir lib"
  }
```

```
$ yarn run build
```

### 実行

```
$ node ./lib/index.js
```

Hello, worldが画面に出力されると思います。

Escapeキーを押すと、終了します。

## tags

blessed.jsの[タグ](https://github.com/chjj/blessed#content--tags)を利用する際は、要素に対してtags属性を指定します。

その際に、タグを含むテキストを子として指定するのではなく、

content属性に指定する必要があります。

```jsx
class App extends Component {
  render() {
    // <text tags>{'{green-fg}{bold}Hello, world{/bold}{/green-fg}'</text>
    // ↑だと、タグが解釈されない
    return (
      <text
        tags
        content={'{green-fg}{bold}Hello, world{/bold}{/green-fg}'}>
      </text>
    );
  }
}
```

## refs

react-dom同様、ref属性が使えます。

```javascript
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      selectedUser: ''
    };
  }

  componentDidMount() {
    // コンポーネントに対する参照
    this.refs.userList.focus();
  }

  onUserSelected(item) {
    this.setState({ selectedUser: item.content }); 
  }

  render() {
    return (
      <box height='40%' border={{ type: 'line' }}>
        <box height='10%'>{'selected: ' + this.state.selectedUser}</box>
        <box top='10%' height='20%'>
          <UserList
            ref='userList'
            onUserSelected={this.onUserSelected.bind(this)}
            users={['hoge', 'fuga', 'piyo']}>
          </UserList>
        </box>
      </box>
    );
  }
}

class UserList extends Component {
  focus() {
    // blessed.jsのNodeに対する参照
    this.refs.list.focus();
  }

  render() {
    const { users, onUserSelected } = this.props;

    return (
      <list
        ref='list'
        onSelect={onUserSelected}
        items={['hoge', 'fuga', 'piyo']}
        style={{
          selected: {
            bg: '#373b41',
            fg: '#c5c8c6'
          }
        }}
        vi
        keys>
      </list>
    );
  }
}
```

`ref`属性を指定した要素は、コンポーネントの`refs`プロパティから参照することができます。

## prop-types

```jsx
import PropTypes from 'prop-types';

class UserList extends Component {
  static propTypes = {
    users: PropTypes.func.isRequired,
    onUserSelected: PropTypes.func.isRequired
  };

  ...
}
```

## neo-blessed

blessed.js自体は、現在、あまりメンテナンスされていないようです。

そのため、neo-blessedというfork版が存在します。

react-blessedからneo-blessedを利用する際は、以下の設定を記述します。

```jsx
import blessed from 'neo-blessed';
import {createBlessedRenderer} from 'react-blessed';

const render = createBlessedRenderer(blessed);

...

render(<App></App>, screen);
```

## ユニットテスト

Enzymeやreact-testing-library等のライブラリは、現在のところ対応していません。

[react-test-renderer](https://reactjs.org/docs/test-renderer.html)を使うとよいのだろうか?

```jsx
import test from 'tape';
import ShallowRenderer from 'react-test-renderer/shallow';
import ChatMessage from './ChatMessage';

test('/components/ChatMessage', assert => {
  const renderer = new ShallowRenderer();

  const output = renderer.render(
    <ChatMessage
      top={0}
      height={20}
      width='100%'
      content='{bold}hoge{/bold}'>
    </ChatMessage>
  );

  assert.equal(
    output.props.content,
    '{bold}hoge{/bold}',
    'should have content'
  );
  assert.end();
});
```

## 参考

* [react-blessed - CUIアプリケーションでもReact！](https://www.moongift.jp/2015/09/react-blessed-cui%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A7%E3%82%82react%EF%BC%81/)
* [blessed](https://github.com/chjj/blessed)
* [react-blessed](https://github.com/Yomguithereal/react-blessed)
* [neo-blessed](https://github.com/embark-framework/neo-blessed)