dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

[Apex]レコードが承認プロセスでロック中かどうか判定する

メモ。
レコードの ID を元に、そのレコードが現在承認プロセスの最中で、かつロックされているかどうか Apex で判定したい。

ここを参考にできた。
apex - Check if a record is in approval process - Salesforce Stack Exchange

Approval.isLocked(id)

を使う。

参考:Approval クラス | Apex 開発者ガイド | Salesforce Developers

isLocked は id だけでなく id のリストだったり SObject またはそのリストも引数として受け取れるらしい。

1個だけ注意点として、このメソッドは

作成 > ワークフローと承認プロセス > プロセスの自動化設定

より、「Apex でのレコードのロックおよびロック解除を有効化」に✔を入れておかないといけない。

f:id:dackdive:20170110225827p:plain

参考:SFDC:Apexトリガによる承認申請時のレコードロック解除について - tyoshikawa1106のブログ

設定しておかないと、以下のエラーが出る。

System.NoAccessException: Apex approval lock/unlock api preference not enabled.

[Salesforce]カスタムオブジェクトのメタデータをCSVに変換する

動機

開発時、特定のカスタムオブジェクトの項目一覧をさっと確認したい。
基本的にメタデータを git 管理しているので、src/objects/MyObj__c.object のようなローカルの XML ファイルをパースして
人が読める形式に加工できれば十分。

Node 界隈のパッケージとか使えば簡単にできるんじゃないかなと思って試してみたらできたので、メモ。


作る

想定しているディレクトリ構成は以下。

├── src
│   ├── package.xml
│   └── objects
│       └── MyObj__c.object
├── build.xml
├── package.json
└── index.js

必要なパッケージをインストールする。

$ npm install -D fs-extra
$ npm install -D xml2js
$ npm install -D json2csv

コードは以下のようになった。

const fs = require('fs-extra');
const xml2js  = require('xml2js');
const json2csv = require('json2csv');
const csv2md = require('csv2md');

const parser = new xml2js.Parser();
const PATH_TO_TARGET = 'src/objects/MyObj__c.object';
const CSV_FIELDS = [
  'label',
  'fullName',
  'type',
  'required',
  // 'externalId',
  // 'caseSensitive',
  // 'length',
  // 'trackTrending',
  // 'unique',
];
const dataList = [];

fs.readFile(__dirname + '/' + PATH_TO_TARGET, function(err, data) {
  parser.parseString(data, function (err, result) {
    // console.dir(result);

    for (const field of result.CustomObject.fields) {
      const data = {};
      // console.log(field);
      for (const csvField of CSV_FIELDS) {
        data[csvField] = field[csvField] ? field[csvField][0] : null;
      }
      // console.log(data);
      dataList.push(data);
    }
    // console.log('Done');
  });

  const csv = json2csv({ data: dataList, fields: CSV_FIELDS });
  console.log(csv);
});

CSV_FIELDS で定義しているプロパティは以下を参照。
https://developer.salesforce.com/docs/atlas.ja-jp.204.0.api_meta.meta/api_meta/customobject.htm

項目によって出力されるものとそうでないものがあるので、とりあえず最低限必要なものだけにしてみた。

試してみる

こういうメタデータに対して

<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
    <fields>
        <fullName>Account__c</fullName>
        <deleteConstraint>SetNull</deleteConstraint>
        <externalId>false</externalId>
        <label>取引先</label>
        <referenceTo>Account</referenceTo>
        <relationshipLabel>Myobj</relationshipLabel>
        <relationshipName>Myobjs</relationshipName>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Lookup</type>
    </fields>
    <fields>
        <fullName>Checkbox__c</fullName>
        <externalId>false</externalId>
        <label>元チェックボックス</label>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Date</type>
    </fields>
    <fields>
        <fullName>Discount_Percentage__c</fullName>
        <externalId>false</externalId>
        <label>Discount_Percentage</label>
        <precision>3</precision>
        <required>false</required>
        <scale>0</scale>
        <trackTrending>false</trackTrending>
        <type>Number</type>
        <unique>false</unique>
    </fields>
    <fields>
        <fullName>Discount_Range__c</fullName>
        <externalId>false</externalId>
        <formula>IF((Discount_Percentage__c &gt;=5 &amp;&amp; Discount_Percentage__c&lt;=10) , &apos;Level-1&apos;,
IF((Discount_Percentage__c &gt;=10 &amp;&amp; Discount_Percentage__c&lt;=20), &apos;Level-2&apos;,
IF((Discount_Percentage__c &gt;=20 &amp;&amp; Discount_Percentage__c&lt;=30), &apos;Level-3&apos;,
IF((Discount_Percentage__c &gt;=30 &amp;&amp; Discount_Percentage__c&lt;=40), &apos;Level-4&apos;,



&apos;No Disount&apos;))))</formula>
        <formulaTreatBlanksAs>BlankAsZero</formulaTreatBlanksAs>
        <label>Discount_Range</label>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    <fields>
        <fullName>Lead__c</fullName>
        <deleteConstraint>SetNull</deleteConstraint>
        <externalId>false</externalId>
        <label>リード</label>
        <referenceTo>Lead</referenceTo>
        <relationshipLabel>Myobj</relationshipLabel>
        <relationshipName>Myobjs</relationshipName>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Lookup</type>
    </fields>
    <fields>
        <fullName>PicklistValues1__c</fullName>
        <externalId>false</externalId>
        <label>PicklistValues</label>
        <picklist>
            <picklistValues>
                <fullName>aaaaa</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>bbbbb</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>ccccc</fullName>
                <default>false</default>
            </picklistValues>
            <sorted>false</sorted>
        </picklist>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Picklist</type>
    </fields>
    <fields>
        <fullName>PicklistValues__c</fullName>
        <externalId>false</externalId>
        <label>PicklistValues</label>
        <picklist>
            <picklistValues>
                <fullName>aaaaaaaaaaa</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>bbbbbbbbbbb</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>cccccccccc</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>ddddddddddd</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>eeeeeeeeee</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>ffffffffffffffff</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>gggggggggg</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>hhhhhhhhh</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>iiiiiiiiiii</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>jjjjjjjjjjjj</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>kkkkkkkkkk</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>lllllllllll</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>mmmmmmmmmm</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>nnnnnnnnnnnnn</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>oooooooo</fullName>
                <default>false</default>
            </picklistValues>
            <sorted>false</sorted>
        </picklist>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Picklist</type>
    </fields>
    <fields>
        <fullName>Status__c</fullName>
        <externalId>false</externalId>
        <label>ステータス</label>
        <picklist>
            <picklistValues>
                <fullName>対応中</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>完了</fullName>
                <default>false</default>
            </picklistValues>
            <sorted>false</sorted>
        </picklist>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Picklist</type>
    </fields>
    <fields>
        <fullName>Text2__c</fullName>
        <externalId>false</externalId>
        <label>テキスト入力欄2</label>
        <length>100</length>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    <fields>
        <fullName>Text__c</fullName>
        <externalId>false</externalId>
        <label>テキスト入力欄</label>
        <length>255</length>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    <label>Myobj</label>
    <!-- 略 -->
</CustomObject>

結果がこう。

$ node index.js
"label","fullName","type","required"
"取引先","Account__c","Lookup","false"
"元チェックボックス","Checkbox__c","Date","false"
"Discount_Percentage","Discount_Percentage__c","Number","false"
"Discount_Range","Discount_Range__c","Text","false"
"リード","Lead__c","Lookup","false"
"PicklistValues","PicklistValues1__c","Picklist","false"
"PicklistValues","PicklistValues__c","Picklist","false"
"ステータス","Status__c","Picklist","false"
"テキスト入力欄2","Text2__c","Text","false"
"テキスト入力欄","Text__c","Text","false"


TODO

とりあえずで作ったので、ブラッシュアップしないといけない点は多々。

  • Name などの標準項目がメタデータとして出力されてないので調べる
    • retrieve する時の指定のしかたが悪いかも
  • src/objects 以下の全オブジェクトに対して実行できるようにしたい
  • CLI 化したい
    • 出力するプロパティや対象のオブジェクトは引数で指定できるようにしたい
  • 選択リスト(Picklist)などは選択肢の値もほしい
  • Markdown 形式にも対応したい
    • csv2md というのが使えるかと思ったけど、CLI としてしか使えない?

Markdown については、csv2md でもとりあえず以下のようにして変換することはできる。

$ npm i -D csv2md
$ node index.js | ./node_modules/csv2md/bin/csv2md
| label | fullName | type | required |
|---|---|---|---|
| 取引先 | Account__c | Lookup | false |
| 元チェックボックス | Checkbox__c | Date | false |
| Discount_Percentage | Discount_Percentage__c | Number | false |
| Discount_Range | Discount_Range__c | Text | false |
| リード | Lead__c | Lookup | false |
| PicklistValues | PicklistValues1__c | Picklist | false |
| PicklistValues | PicklistValues__c | Picklist | false |
| ステータス | Status__c | Picklist | false |
| テキスト入力欄2 | Text2__c | Text | false |
| テキスト入力欄 | Text__c | Text | false |

Material-UIでrefを使う

メモ。
以下のようなコンポーネント<input type="text"> 項目を Material-UI の TextField に置き換えようと思ったが
ref で参照しているのをそのまま TextField でも使えるのか迷った。

import React, { PropTypes } from 'react';

export default class AddTodoForm extends React.Component {
  handleSubmit(e) {
    e.preventDefault();
    const node = this.refs.input;
    const text = node.value.trim();
    if (!text) {
      return;
    }
    this.props.onSubmit(text);
    node.value = '';
  }

  render() {
    return (
      <div>
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <input type="text" ref="input" />
          <button type="submit">
            Add Todo
          </button>
        </form>
      </div>
    );
  }
}

AddTodoForm.propTypes = {
  onSubmit: PropTypes.func.isRequired,
};


先に結論

値の参照だけしたいときは

this.refs.xxx.getValue()

とし、DOM Node を取得する場合は

this.refs.xxx.getInputNode()

を使う。

両方ともドキュメントには載っておらず、前者は以下の Stack Overflow から
javascript - How get data from material-ui TextField, DropDownMenu components? - Stack Overflow

後者は以下のようにブラウザの開発者コンソールから無理やり見つけた。

f:id:dackdive:20161123195515p:plain


string ref を使う場合

 import React, { PropTypes } from 'react';
+import RaisedButton from 'material-ui/RaisedButton';
+import TextField from 'material-ui/TextField';

 export default class AddTodoForm extends React.Component {
   handleSubmit(e) {
     e.preventDefault();
-    const node = this.refs.input;
+    const node = this.refs.input.getInputNode();
     const text = node.value.trim();
     if (!text) {
       return;
@@ -16,10 +18,8 @@ export default class AddTodoForm extends React.Component {
     return (
       <div>
         <form onSubmit={(e) => this.handleSubmit(e)}>
-          <input ref="input" />
-          <button type="submit">
-            Add Todo
-          </button>
+          <TextField id="todo-title" ref="input" />
+          <RaisedButton label="Add Todo" onTouchTap={(e) => this.handleSubmit(e)} />
         </form>
       </div>
     );
diff --git a/src/containers/App.js b/src/containers/App.js
index 8068f95..4ee5acf 100644
--- a/src/containers/App.js
+++ b/src/containers/App.js
@@ -18,4 +18,3 @@ export default class App extends React.Component {
     );
   }
 }


arrow function による ref を使う場合

上述した string ref は現在推奨されていないらしい
(参考:http://reactjs.cn/react/docs/more-about-refs.html#the-ref-string-attribute
ので、arrow 関数を使った場合の書き方も載せておく。

https://facebook.github.io/react/docs/refs-and-the-dom.html

 import React, { PropTypes } from 'react';
+import RaisedButton from 'material-ui/RaisedButton';
+import TextField from 'material-ui/TextField';

 export default class AddTodoForm extends React.Component {
   handleSubmit(e) {
     e.preventDefault();
-    const node = this.refs.input;
+    const node = this.input.getInputNode();
     const text = node.value.trim();
     if (!text) {
       return;
@@ -16,10 +18,8 @@ export default class AddTodoForm extends React.Component {
     return (
       <div>
         <form onSubmit={(e) => this.handleSubmit(e)}>
-          <input ref="input" />
-          <button type="submit">
-            Add Todo
-          </button>
+          <TextField id="todo-title" ref={(input) => this.input = input} />
+          <RaisedButton label="Add Todo" onTouchTap={(e) => this.handleSubmit(e)} />
         </form>
       </div>
     );
     );
   }
 }

API Blueprintによるドキュメント開発環境【2016年冬】

ここから少しアップデートしたのでメモ。

リポジトリ


特徴

markdown -> html への変換には aglio、ローカルサーバーは --server オプションを使う

ローカルで markdown ファイルを html に変換するのに aglio を使うところは以前と同じ。
改めて調べてみると、--server というオプションでライブリロード機能つきのローカルサーバーの起動までやってくれるらしく、browserSync は不要だった。

また、ポート番号については README に記載されていないが、-p オプションで指定できた。
参考:Support for different port? · Issue #12 · danielgtaylor/aglio

$ ./node_modules/aglio/bin/aglio.js
Usage: node_modules/aglio/bin/aglio.js [options] -i infile [-o outfile -s]

オプション:
  -i, --input           Input file
  -o, --output          Output file
  -t, --theme           Theme name or layout file        [デフォルト: "default"]
  -f, --filter          Sanitize input from Windows    [真偽] [デフォルト: true]
  -s, --server          Start a local live preview server
  -h, --host            Address to bind local preview server to
                                                       [デフォルト: "127.0.0.1"]
  -p, --port            Port for local preview server         [デフォルト: 3000]
  -v, --version         Display version number               [デフォルト: false]
  -c, --compile         Compile the blueprint file           [デフォルト: false]
  -n, --include-path    Base directory for relative includes
  --verbose             Show verbose information and stack traces
                                                             [デフォルト: false]
  --theme-variables     Color scheme name or path to custom variables
                                                         [デフォルト: "default"]
  --theme-condense-nav  Condense navigation links      [真偽] [デフォルト: true]
  --theme-full-width    Use full window width         [真偽] [デフォルト: false]
  --theme-template      Template name or path to custom template
                                                         [デフォルト: "default"]
  --theme-style         Layout style name or path to custom stylesheet
  --theme-emoji         Enable support for emoticons   [真偽] [デフォルト: true]

例:
  node_modules/aglio/bin/aglio.js -i        Render to HTML
  example.apib -o output.html
  node_modules/aglio/bin/aglio.js -i        Start preview server
  example.apib -s
  node_modules/aglio/bin/aglio.js           Theme colors
  --theme-variables flatly -i example.apib
  -s
  node_modules/aglio/bin/aglio.js           Disable options
  --no-theme-condense-nav -i example.apib
  -s

See https://github.com/danielgtaylor/aglio#readme for more information

(コマンド例)

$ aglio -i src/index.md -p 8080 --server


複数ファイルの分割は <!-- include(xxx.md) --> で実現

参考:https://github.com/danielgtaylor/aglio#including-files

エントリーポイントとなるファイルで

FORMAT: 1A

<!-- include(xxx.md) -->

としておくと、別のファイルを読み込むことができる。
これで API 定義を複数のファイルに分割することができる。

これ自体は以前調べたときにも把握はしてたようだが、モックサーバー用ライブラリ api-mock の制限で断念したようだった。
が、次に示すようにモックサーバを api-mock から drakov というライブラリに変更したのでこの問題は解決した。


モックサーバには api-mock のかわりに drakov を使用

以前調べたときは api-mock を使おうとしていたらしいが、
今 Node のバージョン 7.1.0、npm のバージョン 3.10.9 でインストールしようとしたところエラーになってしまった。

$ npm i -g api-mock
/usr/local/bin/api-mock -> /usr/local/lib/node_modules/api-mock/bin/api-mock

> protagonist@1.2.6 install /usr/local/lib/node_modules/api-mock/node_modules/protagonist
> node-gyp rebuild

  CXX(target) Release/obj.target/libmarkdownparser/drafter/ext/snowcrash/ext/markdown-parser/src/ByteBuffer.o
clang: error: invalid deployment target for -stdlib=libc++ (requires OS X 10.7 or later)
make: *** [Release/obj.target/libmarkdownparser/drafter/ext/snowcrash/ext/markdown-parser/src/ByteBuffer.o] Error 1
gyp ERR! build error
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:276:23)
gyp ERR! stack     at emitTwo (events.js:106:13)
gyp ERR! stack     at ChildProcess.emit (events.js:191:7)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:215:12)
gyp ERR! System Darwin 14.5.0
gyp ERR! command "/usr/local/Cellar/node/7.1.0/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /usr/local/lib/node_modules/api-mock/node_modules/protagonist
gyp ERR! node -v v7.1.0
gyp ERR! node-gyp -v v3.4.0
gyp ERR! not ok

この Issue が関係している?Node のバージョン 6 系からうまくインストールできないらしい。
install failed at protagonist package · Issue #53 · localmed/api-mock

しかたないので
https://apiblueprint.org/tools.html#mock-servers
の一番上にあった drakov にした。

drakov は対象の markdown ファイルを指定する際

$ drakov -f "src/**/*.md"

というように正規表現で指定することができるので、ファイルが複数に分割されていても問題ない。
" でくくらないと期待通りに動かないので注意(シェル環境が zsh だったからかも)

また、--watch オプションをつけておくと markdown ファイルの変更を検知して自動的にリロードしてくれる。

(コマンド例)

$ drakov -f "src/**/*.md" -p 8081 --watch


TODO

結局いまだに dredd というツールを調べられていない。

React(v15)&ReduxでMaterial-UIを使ってみる

メモです。
基本的な使い方を試してみた。


リポジトリ

Redux の basic チュートリアル にそって作った Todo アプリをベースに、Material-UI を適用してみる。

https://github.com/zaki-yama/redux-todo-app/pull/2

(Material-UI 適用前のコードには base というタグをつけてます)

なお、公式 example リポジトリも存在するよう。

https://github.com/callemall/material-ui-webpack-example


インストール

$ npm install --save material-ui

でインストール。


導入手順

react-tap-event-plugin をインストールする

http://www.material-ui.com/#/get-started/installation によると、react-tap-event-plugin というパッケージが必要らしい。
Material-UI と同様に npm でインストールする。

$ npm install --save react-tap-event-plugin

このパッケージにより、Material-UI のすべてのコンポーネントは onTouchTap というイベントハンドラが使えるようになる。

マウスクリックやスマホタブレットのタッチ・タップイベントを共通のイベントハンドラで処理できるようになる、のかな?

また、上記ページには

This dependency is temporary and will go away once the official React version is released.

と書いており、試したときの React のバージョンは 15.3.2 だったので
メジャーバージョンが 1 以上だからこのプラグイン無しでも動くということかな?と思ったけど、結局インストールしないとうまく動かなかった。

react-tap-event-plugin をインストールした後は、以下のコードをアプリの先頭に記述する必要がある。

import injectTapEventPlugin from 'react-tap-event-plugin';

// Needed for onTouchTap
// http://stackoverflow.com/a/34015469/988941
injectTapEventPlugin();

よって Redux の場合、アプリのエントリーポイント(src/index.js)に以下のように追記するはず。

 import { render } from 'react-dom';
 import { Provider } from 'react-redux';
 import { createStore } from 'redux';
+import injectTapEventPlugin from 'react-tap-event-plugin';
 import todoApp from './reducers';
 import App from './containers/App';
  
+// Needed for onTouchTap
+// http://stackoverflow.com/a/34015469/988941
+injectTapEventPlugin();
+
 const store = createStore(todoApp);
  
 render(
   <Provider store={store}>
     <App />
   </Provider>,
   document.getElementById('root'),
 );


Roboto フォントを適用する

またまた
http://www.material-ui.com/#/get-started/installation#roboto-font
を読むと、

Material-UI was designed with the Roboto font in mind

とあるので、Roboto フォント を読み込むようにする。

html ファイルに

<!-- src/index.html -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">

と書き、CSS でも

/* src/main.css */
html {
  font-family: 'Roboto', sans-serif;
}

とする。
確認したところ html 側の記述だけでも Material-UI コンポーネントに Roboto フォントが適用されるが、
アプリ全体のフォントを統一するため CSS でも指定することにした。

なお、話は逸れるが webpack で CSS を扱う方法はいくつか考えられる。

├── dist .................. ビルド後のファイルを置く場所
├── src
│   ├── actions
│   ├── components
│   ├── constants
│   ├── containers
│   ├── index.html
│   ├── index.js
│   ├── main.css
│   └── reducers
└── webpack.config.js

というディレクトリ構成の場合、

1) CSSCSS のままで html から読み込む場合

html 側は

<link rel="stylesheet" type="text/css" href="main.css">

とし、webpack.config.js では file-loader を使って dist ディレクトリに CSS ファイルもコピーされるようにする。

   context: __dirname + '/src',
   entry: {
     javascript: './index.js',
-    html: './index.html'
+    html: './index.html',
+    css: './main.css',
   },
   output: {
     path: __dirname + '/dist',
@@ -27,6 +28,10 @@ module.exports = {
       {
         test: /\.html$/,
         loader: 'file?name=[path][name].[ext]'
+      },
+      {
+        test: /\.css$/,
+        loader: 'file?name=[path][name].[ext]'
       }
     ]
   }

2) JavaScript にバンドルする場合

CSSJavaScript 内で import してしまうという場合、html の代わりに src/index.js で

import './main.css';

とし、webpack.config.js では css-loader と style-loader を使うようにする。

       {
         test: /\.html$/,
         loader: 'file?name=[path][name].[ext]'
-      }
+      },
+      {
+        test: /\.css$/,
+        loader: 'style!css',
+      },
     ]

どちらでもいいですが、2) の方が後々 SCSS とかにも応用できるので良さげ。


<MuiThemeProvider> でアプリケーション全体をラップする

コンポーネントを使う前の最後の準備として、アプリケーション全体を <MuiThemeProvider> というコンポーネントでラップする。
エントリーポイントになっている src/index.js でやってもいいと思うし、コンポーネントの root になっている containers/App.js でもいいはず。

containers/App.js

 import React from 'react';
+import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
+
 import AddTodo from './AddTodo';
 import VisibleTodoList from './VisibleTodoList';
 import FilterLinkList from '../components/FilterLinkList';

 export default class App extends React.Component {
   render() {
     return (
-      <div>
-        <AddTodo />
-        <VisibleTodoList />
-        <FilterLinkList />
-      </div>
+      <MuiThemeProvider>
+        <div>
+          <AddTodo />
+          <VisibleTodoList />
+          <FilterLinkList />
+        </div>
+      </MuiThemeProvider>
     );
   }
 }


Material-UI コンポーネントを使う

ここまでできるとようやく Material-UI のコンポーネントを使うことができる。

今回は簡単な例で、Todo の入力フォームとボタンをそれぞれ TextFieldRaisedButton に置き換えてみる。

元のコードは以下。

components/AddTodoForm.js

import React from 'react';

export default class AddTodoForm extends React.Component {
  handleSubmit(e) {
    e.preventDefault();
    const node = this.refs.input;
    const text = node.value.trim();
    if (!text) {
      return;
    }
    this.props.onSubmit(text);
    node.value = '';
  }

  render() {
    return (
      <div>
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <input ref="input" />
          <button type="submit">
            Add Todo
          </button>
        </form>
      </div>
    );
  }
}

これを以下のようにする。

import React from 'react';
import RaisedButton from 'material-ui/RaisedButton';
import TextField from 'material-ui/TextField';

export default class AddTodoForm extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      input: '',
    };
  }

  handleChange(e) {
    this.setState({
      input: e.target.value,
    });
  }

  handleSubmit(e) {
    e.preventDefault();
    const text = this.state.input.trim();
    if (!text) {
      return;
    }
    this.props.onSubmit(text);
    this.setState({
      input: '',
    });
  }

  render() {
    return (
      <div>
        <form onSubmit={(e) => this.handleSubmit(e)}>
          <TextField
            id="todo-title"
            value={this.state.input}
            onChange={(e) => this.handleChange(e)}
          />
          <RaisedButton
            label="Add Todo"
            onTouchTap={(e) => this.handleSubmit(e)}
          />
        </form>
      </div>
    );
  }
}

フォームに入力されたテキストを管理するのに ref を使っていたのを local state で管理するようになったため全体のコード量が増えた。
ref のまま Material-UI のコンポーネントを管理することもできるみたいなんだけど、ドキュメントに載っていない方法なのでやめた。
あとで別の記事にでもメモする。

この変更によってアプリの UI は以下のように変更される。

before

f:id:dackdive:20161116014935p:plain

after

f:id:dackdive:20161116014944p:plain

ちょっとだけそれっぽい UI になった。


おわりに

ということで React/Redux なアプリに Material-UI を導入することができた。
意外と事前準備としてやることが多く、またそれがドキュメントからは微妙にわかりづらかったりして思った以上に手こずった。


トラブルシューティング的な

ボタンを使うと Unknown prop `onTouchTap` on <button> tag. が出る

→react-tap-event-plugin が入っていない。


リファレンス

eslint-config-airbnbのルール内容を確認する

ESLint はあまり深く考えず eslint-config-airbnb を導入していて、そんなに問題はないんだけども
たまにこちらが期待したルールが無効になってたり、逆にそんなに守らなくてもいいと思われるルールが有効になってたりして、ふと

「あれ、xxx っていうルールは今どういう設定になってるんだろう?」

と思うときがある。

で、eslint-config-airbnb の中身は実際どういうルールセットになっているのか確認してみた。


方法1:GitHub 上で直接確認する

この2つの rules を確認すれば良いと思われる。

が、複数のファイルに分かれていて、目的のルールにたどりつくのがめんどくさい。
git clone してローカルで grep するとか。


方法2:ESLint の --print-config オプションを使う

http://eslint.org/docs/user-guide/command-line-interface#print-config
によると ESLint には --print-config というオプションがあるらしく、

$ eslint --print-config file.js

というように通常の lint 実行時と同じく、対象ファイルを指定して実行する。

これを実行すると、対象ファイルに対して実際に lint チェックを行うかわりに
このファイルに対してどういった設定で lint チェックを行うか、設定(ルールセット)の方が出力される。

$ eslint --print-config index.js
{
  "globals": {
    "__dirname": false,
    "__filename": false,
    "arguments": false,
    "Buffer": false,
    "clearImmediate": false,
    ...
  "rules": {
    "accessor-pairs": 0,
    "array-callback-return": 2,
    ...
}

これを使えばいまどんなルールセットになっているのか知ることができる。

私は ESLint をグローバルインストールしていないので

$ ./node_modules/eslint/bin/eslint.js --print-config index.js | vim -R -

パスが面倒だけど↑のような感じで結果を Vim を開き、目的のルールを検索して設定内容を確認している。

--print-config は現在の設定内容を出力するので、当然のことながら .eslintrc に記述した内容は結果に反映される。
そういう意味ではオリジナルの eslint-config-airbnb のルールを確認する方法とはいえない。