dackdive's blog

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

[Vim]NeomakeでFlowを実行したときにexit code 64が表示されたときのメモ

Vim の Lint チェックに Neomake を使っていて、実行時に

Neomake: flow: completed with exit code 64.

と表示されてしまったときのメモ。
Flow のバージョンは 0.37.4。


原因と解決策

https://github.com/neomake/neomake/blob/master/doc/neomake.txt#L319-L327
によると g:neomake_verbose という変数があるので、.vimrc

g:neomake_verbose=3

を追加して Neomake を再度実行する。

:messages

でメッセージを表示したところ、以下のようになっていた。

Neomake [0.850]: [#2] stderr: flow: ['flow: unknown option ''--old-output-format''.', 'Usage: flow [COMMAND] ', '', 'Valid values for COMMAND:', '  ast             Print the AST', '  autocomple
te    Queries autocompletion information', '  check           Does a full Flow check and prints the results', '  check-contents  Run typechecker on contents from stdin', '  coverage        Show
s coverage information for a given file', '  find-module     Resolves a module reference to a file', '  find-refs       Gets the reference locations of a variable or property', '  force-recheck
   Forces the server to recheck a given list of files', '  gen-flow-files  EXPERIMENTAL: Generate minimal .js.flow files for publishing to npm.', '  get-def         Gets the definition location
 of a variable or property', '  get-importers   Gets a list of all importers for one or more given modules', '  get-imports     Get names of all modules imported by one or more given modules',
'  init            Initializes a directory to be used as a flow root directory', '  ls              Lists files visible to Flow', '  port            Shows ported type annotations for given file
s', '  server          Runs a Flow server in the foreground', '  start           Starts a Flow server', '  status          (default) Shows current Flow errors by asking the Flow server', '  sto
p            Stops a Flow server', '  suggest         Shows type annotation suggestions for given files', '  type-at-pos     Shows the type at a given file and position', '  version         Pri
nt version information', '', 'Default values if unspecified:', '  COMMAND^Istatus', '', 'Status command options:', '  --color                           Display terminal output in color. never,
always, auto (default: auto)', '  --from                            Specify client (for use by editor plugins)', '  --help                            This list of options', '  --ignore-version
                 Ignore the version constraint in .flowconfig', '  --json                            Output results in JSON format', '  --no-auto-start                   If the server is not ru
nning, do not start it; just exit', '  --one-line                        Escapes newlines so that each error prints on one line', '  --pretty                          Pretty-print JSON output (
implies --json)', '  --quiet                           Suppress output about server startup', '  --retries                         Set the number of retries. (default: 3)', '  --retry-if-init
                 retry if the server is initializing (default: true)', '  --sharedmemory-dep-table-pow      The exponent for the size of the shared memory dependency table. The default is 17, i
mplying a size of 2^17 bytes', '  --sharedmemory-dirs               Directory in which to store shared memory heap (default: /dev/shm/)', '  --sharedmemory-hash-table-pow     The exponent for t
he size of the shared memory hash table. The default is 19, implying a size of 2^19 bytes', '  --sharedmemory-log-level          The logging level for shared memory statistics. 0=none, 1=some',
 '  --sharedmemory-minimum-available  Flow will only use a filesystem for shared memory if it has at least these many bytes available (default: 536870912 - which is 512MB)', '  --show-all-error
s                 Print all errors (the default is to truncate after 50 errors)', '  --strip-root                      Print paths without the root', '  --temp-dir                        Direct
ory in which to store temp files (default: /tmp/flow/)', '  --timeout                         Maximum time to wait, in seconds', '  --version                         (Deprecated, use `flow vers
ion` instead) Print version number and exit', '']
Neomake [0.854]: Channel has been closed: channel 2 closed

どうやら --old-output-format という古いオプションをつけて実行しており、そんなオプションないよと怒られているようだ。

参考(関係ある?):Remove --old-output-format · Issue #2844 · facebook/flow

で、今度は Neomake 側を調べてみたところこの PR でどうも修正したように見える。

Fix flow output by rafaelrinaldi · Pull Request #880 · neomake/neomake

そういえばしばらくプラグインのアップデートとかしてなかったなと思い、私は dein.vim を使っているので

:call dein#update()

でアップデートしてみたところ lint が通るようになった。

※ただ、それ以外にも色々 Neomake ではうまくいかないことがあったので現在は ALE を使うことにした。それはまた別途書くことにする。

react-lightning-design-systemのDatepickerを日本語表記にする

メモ。

react-lightning-design-system の Datepicker の月や曜日の部分を日本語にしたい。

f:id:dackdive:20170119205055p:plain

locale のようなプロパティはないが、内部的に Moment.js を使っているので以下のようにして変更できた。

(2017/01/20追記)
普通に

moment.locale('ja');

だけでいけました...moment のロケールを設定する方法が間違ってたみたい。

locale が ja のときの設定は
https://github.com/moment/moment/blob/develop/src/locale/ja.js
のようなので、デフォルトの表記で問題ない場合は updateLocale する必要はない。

(追記ここまで)

まず、月の表示は
https://github.com/mashmatrix/react-lightning-design-system/blob/master/src/scripts/Datepicker.js#L192
にあるように moment.monthsShort() を使っている。

そしてこのメソッドは通常 Jan, Sep, ... などの英語表記での文字列を返すのだが、それをカスタマイズするには以下のようにする。

moment.updateLocale('ja', {
  monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
});

参考:http://momentjs.com/docs/#/customization/

moment.updateLocale()ロケール設定を変更し、その際に第二引数でフォーマットを定義するオブジェクトを渡す。

同様に、週の表示は
https://github.com/mashmatrix/react-lightning-design-system/blob/master/src/scripts/Datepicker.js#L233
にあるように moment.weekdaysMin() を使っているようなので
monthsShort() のときと同じく updateLocale 時に設定すれば良い。

moment.updateLocale('ja', {
  monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
  weekdaysMin: ['日', '月', '火', '水', '木', '金', '土'],
});

この状態で Datepicker を開くと以下のようになる。

f:id:dackdive:20170119210948p:plain


サンプルコード

import React, { PropTypes } from 'react';
import moment from 'moment';

import { Datepicker } from 'react-lightning-design-system';

class MyDatepicker extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selectedDate: props.selectedDate || moment().format('YYYY-MM-DD'),
    };
  }

  onSelectDate(selectedDate) {
    this.setState({ selectedDate });
    this.props.onSelectDate(selectedDate);
  }

  render() {
    return (
      <div style={ { padding: '12px', width: '20rem' } }>
        <Datepicker
          selectedDate={ this.state.selectedDate }
          onSelect={ this.onSelectDate.bind(this) }
        />
      </div>
    );
  }
}

MyDatepicker.propTypes = {
  selectedDate: PropTypes.string,
  onSelectDate: PropTypes.func.isRequired,
};

export default class App extends React.Component {
  render() {
    moment.updateLocale('ja', {
      monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
      weekdaysMin: ['日', '月', '火', '水', '木', '金', '土'],
    });
    return <MyDatepicker />
  }
}

SlackのOutgoing WebhookとGoogle Apps ScriptでBotを作ったときにつまずいたところメモ

Slack の Bot にメンションしたら Outgoing Webhook で GAS のスクリプトを実行するようなものが作りたくて
とりあえず連携するところまで作ったんだけど、思ったよりハマったところがあったのでメモ。

基本的な作り方に関しては、こちらの記事の通りに作成するとうまくいく。
初心者がGASでSlack Botをつくってみた - CAMPHOR- Tech Blog

主な流れ
  • GAS で doPost() メソッドを定義したスクリプトを作り、「公開 > ウェブ アプリケーションとして導入...」で URL を取得する
  • https://api.slack.com/web から Slack の API token を取得し、GAS のスクリプトでその token を使用する
  • Slack の Outgoing Webhooks を作成し、「URL(s)」に GAS の URL を貼り付ける
  • 「Trigger Words(s)」にスクリプト実行のキーワードとなる文字列を入れる(, 区切りで複数指定可能)

bot のアイコンが表示されない

chat.postMessageusernameBot 名を正確に入力しているにも関わらず、Bot のアイコンに指定した画像が表示されなかった。
icon_url または icon_emoji で指定してあげる必要があったんですね。初歩的。


GAS のスクリプトが更新されない

「ウェブ アプリケーションとして導入」した後にスクリプトを変更した際、バージョンを上げずに単に「更新」してた。

f:id:dackdive:20170118230328p:plain:w320

ここですね。
少しでもスクリプトを更新したら、毎回プロジェクト バージョンで「新規作成」を選びバージョンを1つ上げる必要があった。

これどうにかならないかなと思ったんだけど今のところわからず。
「最新のコードをテスト」で doGet() についてはテストできるんだけど、その URL に POST しても doPost() は見つからないと言われた。

あと、アプリケーションにアクセスできるユーザーを「全員(匿名ユーザーを含む)」にするのも必要。


bot へメンションしてるのに Webhook が反応しない

これが一番ハマったところ。
せっかく Bot アカウントも作っているので、@my-bot のようにメンションでスクリプト実行したかったが何回やっても反応しなかった。

調べてみたところ、メンションしているときは Bot 名でなく Bot の UserId で指定する必要があることがわかった。
参考:SlackのOutgoing Webhookで@つきのmentionを捕まえる - beatsync.net

また、Bot の User Id は画面からはわからなかったので、確認するにはこちらの記事を参考に Slack API を叩いて確認した。
参考:slackに参加しているメンバーのUser IDを調べる方法 - /var/www/yatta47.log

$ curl "https://slack.com/api/users.list?token=[TOKEN]"

https://api.slack.com/web で取得した token を使って上記のように API を叩き、レスポンスを Online JSON Viewer などで整形して目的の Bot の User Id を探す。


おまけ:GAS スクリプト側を動作確認するには

$ curl -X POST -v -F 'hoge=fuga' https://script.google.com/macros/s/***/exec

とかした。

[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 |

npm run + Tabキーでnpm scriptを補完する

知らなかったのでメモ。
参考:npm run should have autocomplete prompt for available scripts · Issue #8030 · npm/npm

# zsh の場合
$ npm completion >> ~/.zshrc

~/.bashrc も可。

$ npm run <TAB>
build-storybook  storybook        test

楽になった。

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>
     );
     );
   }
 }