MeteorJSとReactを勉強してみる その4: フォームからタスクを追加してみよう【公式翻訳】
MeteorJSを勉強してみるシリーズ第4弾です。
やっとアプリらしい機能を追加するところまで来ましたね。 やっぱり、ユーザーの行動と表示するコンテンツが何かしらの形で連動するものを作ってこそ「アプリ」っていう感じがしますからね。
このステップでは、ユーザーがリストにタスクを追加するための記入欄を追加していきます。 まず、Appコンポーネントにフォームを追加しましょう。
imports/ui/App.js
before
import React, { Component } from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}).fetch() }; })(App);
after
import React, { Component } from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}).fetch() }; })(App);
Tip: JSXのコードには、{/ ... /}と書くことで...部分をコメントにすることができます
上記のコードを見ると、formエレメントがonSubmit
という属性を持っていることが分かりますね。
この属性は、AppコンポーネントのhandleSubmit
というメソッドを呼び出しています。
Reactでは、これがブラウザ上で起こったイベント(この場合は「ユーザーが新しいタスクを送信した」というイベント)を検知する仕組みになっています。
また、inputエレメントはref属性を持っていますね。 これにより、後々このエレメントにアクセスしやすくなります。
では次に、handleSubmit
メソッドをAppコンポーネントに追加しましょう。
imports/ui/App.js
before
import React, { Component } from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}).fetch() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}).fetch() }; })(App);
これでアプリに入力欄が追加されました。 タスクを追加するには、ただ入力欄に何か入力してエンターキーを押すだけです。
既に開いているブラウザとは別のウィンドウを開き、そこでアプリにアクセスすれば、全てのクライアントでリストが同期されていることが分かるでしょう。
Reactでイベントを検知する
上記で分かる通り、Reactではコンポーネント上でメソッドを呼ぶことで直接DOMイベントを操作することができます。
イベントハンドラの中では、エレメントにref属性を与えることによってReactDOM.findDOMNode
を使ってコンポーネントからエレメントを呼び出すことができます。
Reactがサポートするその他のイベントや、イベントのシステムがどのように動いているのかについてはReact docsを参照してください。
コレクションにデータを入れる
イベントハンドラの中で、Tasks.insert()
を使ってTasksコレクションにタスクを追加しています。
コレクションにおいてはスキーマ(データベースの構造定義書)を定義する必要がないので、上記でも追加しているcreatedAt(そのデータが追加された日時)のように、追加するデータに対してどんな値でも付与することができます。
クライアント側からどんなデータでも追加できるというのはセキュリティ的に甘い状態なのですが、一旦良しとしましょう。 ステップ10では、どのようにアプリをセキュアにし、どのようにデータベースにデータが追加される方法を制限するかを学びます。
タスクをソートする
現状では、追加した新しいタスクは全てリストの下に並んでいます。 これはタスクリストとしては機能的ではないですね。新しいタスクは上に並んでいてほしいものです。
これは先程追加したcreatedAtにより解決できます。 createdAtはタスクを追加した時間なので、それを昇順に並べれば良いわけですね。
これを実装するには、Appコンポーネントを包んでいるデータコンテナ内のfind
メソッドに並び替えのオプションを追加してやればOKです。
imports/ui/App.js before
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}).fetch() }; })(App);
after
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { handleSubmit(event) { event.preventDefault(); // find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Tasks.insert({ text, createdAt: new Date() }); // clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> <form className="new-task" onSubmit={this.handleSubmit.bind(this)}> <input type="text" ref="textInput" placeholder="Type to add new Tasks." /> </form> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch() }; })(App);
再びブラウザを確認し、うまく動いていることを確認しましょう。 新しくタスクを追加すると、リストの一番上に表示されることが確認出来るはずです。
第四弾はここまでです。 いい感じになってきましたね。
完全に余談ですが、個人的にはプログラムの勉強はしっかり理解しながら書くことが大切だと思っています。
もちろん知識ゼロから上記のコードを理解して書くのはかなり難しいです。 ただ、だからといって単純にコードをコピペ・写経するのではいつまでたっても自分でコードを書けるようになりません。
プログラムは簡単だ、という人も巷にはいますが、それは嘘です。 専門職として成り立っているくらいなので、それなりに難しいです。
形から入ることも時には大切ですが、中身の伴わない外面だけではすぐに自分にごまかしが効かなくなります。
本当にプログラムを書けるようになりたい方は、しっかり理解しながら書く、ということを念頭に置いてくださいね!
第五弾も近いうちに。
MeteorJSとReactを勉強してみる その3: コレクションにタスクのデータを蓄積しよう【公式翻訳】
MeteorJSを勉強するシリーズ第三回です。
翻訳しながら、やっぱり完全な初心者にはわからないことだらけな気がするので、気が向いたらそのへんのケアも出来る記事に後から改変していきます。
コレクションにToDoタスクのデータを蓄積する
コレクションとはMeteorで永続的なデータを蓄積する方法です。
Meteorのコレクションの素晴らしい点は、サーバー・クライアント双方からアクセス可能であり、サーバー側のコードを頑張って書かなくてもViewロジックを簡単に書ける、ということです。
さらにコレクションは自動的に更新されるので、コレクションを利用して書かれたViewコンポーネントは常に最新のデータを表示することができます。
もっと詳しく知りたい方は、Meteor Guideのコレクションの記事を読んでみてください。
新しいコレクションを作るには、JavaScript内でMyCollection = new Mongo.Collection("my-collection");
を宣言するだけでOKです。
上記をクライアント側で書くと、サーバー側のコレクションと繋がったキャッシュが作成されます。
本チュートリアルのStep.12ではクライアントとサーバーの分割についてより詳細に解説しますが、現時点では「全てのデータベースがクライアントに表示される」という前提でコーディングしていきましょう。
コレクションを作るため、下記の通りに新しいフォルダとファイル(tasks モジュール)をimports配下に作成しましょう。
imports/api/tasks.js
import { Mongo } from 'meteor/mongo'; export const Tasks = new Mongo.Collection('tasks');
ここではimports/api
という新しいディレクトリを作成し、その中にtasks.js
を作成しました。
imports/api
はアプリケーションのAPI関連のファイルを置くための重要な場所です。
まずはここにコレクションをおいておき、後でこれらのコレクションから読み込みを行うpublication
と、書き込みを行うmethods
を追加していきます。
アプリの構造について、詳しくはMeteor GuideのApplication Structure articleを読みましょう。
このtasksモジュールは、サーバー側で読み込んであげる必要があります。これによりMongoDBのコレクションが作成され、クライアント側にデータを提供する「配管工事」が完了するわけです。
before
import { Meteor } from 'meteor/meteor'; Meteor.startup(() => { // code to run on server at startup });
after
import '../imports/api/tasks.js';
Reactコンポーネント内でコレクションデータを利用する
Reactコンポーネントの中でMeteorコレクションのデータを利用するには、Atmosphereのパッケージであるreact-meteor-data
を活用しましょう。
このパッケージにより、Meteorのreactive dataをReactコンポーネントの階層内で読み込むことのできる「データコンテナ」を作成することができます。
ターミナルで下記のコマンドを打ちましょう。
meteor add react-meteor-data
実際にreact-meteor-data
を利用するには、作成したコンポーネントをwithTracker
と言うより高位のコンポーネントを利用したコンテナで包んであげる必要があります。
imports/ui/App.js
before
import React, { Component } from 'react'; import Task from './Task.js'; // App Component - represnts the whole app export default class App extends Component { getTasks() { return [ { _id: 1, text: 'This is task 1' }, { _id: 2, text: 'This is task 2' }, { _id: 3, text: 'This is task 3' } ]; } renderTasks() { return this.getTasks().map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> </header> <ul> {this.renderTasks()} </ul> </div> ); } }
after
import React, { Component } from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import Task from './Task.js'; // App Component - represnts the whole app class App extends Component { renderTasks() { return this.props.tasks.map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { return { tasks: Tasks.find({}).fetch(); }; })(App);
withTrackerで包まれたAppコンポーネントはTasksコレクションからタスクを取得し、それをタスクpropとして元々のAppコンポーネントに提供しています。
これらのコードはデータベースの変更に対して受動的に動きます。つまり、データベースのコンテンツが変更されるとAppが再読み込みされるようになります。
さて、上記のとおりにコードを変更したら、元々Todoリスト内にあったタスクが消えてしまったのに気づいたでしょうか?
それはデータベースが空のままだからです。いくつかデータベースにタスクを追加してみましょう!
サーバーサイドのデータベース管理画面からタスクを追加する
コレクション内のアイテムは「ドキュメント」と呼びます。 サーバーのデータベース管理画面から、コレクションにドキュメントを追加してみましょう。
新しくターミナルのタブを開いて、アプリのディレクトリまで移動し、下記のコマンドを打ってください。
meteor mongo
これでアプリのローカル開発環境のデータベースに接続できました。 ターミナルに下記の通りに打ち込んでください。
db.tasks.insert({ text: "Hello world!", createdAt: new Date() });
ブラウザで確認すると、アプリのUIが即座に変更され、新しいタスクが表示されました。
サーバーサイドのデータベースをフロントエンドのコードに繋ぐためのコードを書く必要が無かったことが分かりますね。 これらはMeteorが内部的に自動で処理しているのです。
上記と同様に、ターミナルから違うテキストでいくつかタスクを追加してみてください。 次のステップでは、ターミナルを使わずにアプリのUI上からタスクを追加することができるように機能を追加していきます。
今回は以上です。
JavaScriptの言語仕様を知らない場合、最後のimports/ui/App.js
の変更内容を理解するのは鬼門かもしれませんね・・・
とは言え僕も解説できるほどではないので、折を見てちゃんと記事を更新したいと思います。
次回の更新は来週になります。
MeteorJSとReactを勉強してみる その2: ReactコンポーネントでViewを作成しよう【公式翻訳】
Meteor公式翻訳の第二弾です。
基本的に1レッスン1記事、計12記事で書く予定です。
- ReactコンポーネントでViewを定義する
- 初期のコードを書き換えよう
- 変更結果を確認してみよう
- HTMLファイルには静的コンテンツを書く
- ReactでViewを構成する
- マークアップ(HTML)の内容はJSXのrenderメソッドでレンダリングされる
- CSSを追加する
ReactコンポーネントでViewを定義する
アプリのViewライブラリとしてReactを利用するにあたって、まずはいくつかのNPMパッケージをインストールしましょう。 新しくターミナルの画面を開いて、前回作成したアプリがあるディレクトリまで移動して下記のコマンドを打ってください。
meteor npm install --save react react-dom
Note: meteor npmコマンドはnpmコマンドと同様の挙動をしますが、一部重要な差があります。詳細についてはmeteor npmのドキュメントを参照してください。
初期のコードを書き換えよう
手始めに、前回作成したデフォルトアプリのコードを書き換えましょう。 まず、client/main.htmlを下記のように書き換えます。
before
<head> <title>react-meteor-app</title> </head> <body> <h1>Welcome to Meteor!</h1> {{> hello}} {{> info}} </body> <template name="hello"> <button>Click Me</button> <p>You've pressed the button {{counter}} times.</p> </template> <template name="info"> <h2>Learn Meteor!</h2> <ul> <li><a href="https://www.meteor.com/try" target="_blank">Do the Tutorial</a></li> <li><a href="http://guide.meteor.com" target="_blank">Follow the Guide</a></li> <li><a href="https://docs.meteor.com" target="_blank">Read the Docs</a></li> <li><a href="https://forums.meteor.com" target="_blank">Discussions</a></li> </ul> </template>
after
<head> <title>Todo List</title> </head> <body> <div id="render-target"></div> </body>
次に、client/main.jsも下記に書き換えましょう。
before
import { Template } from 'meteor/templating'; import { ReactiveVar } from 'meteor/reactive-var'; import './main.html'; Template.hello.onCreated(function helloOnCreated() { // counter starts at 0 this.counter = new ReactiveVar(0); }); Template.hello.helpers({ counter() { return Template.instance().counter.get(); }, }); Template.hello.events({ 'click button'(event, instance) { // increment the counter when button is clicked instance.counter.set(instance.counter.get() + 1); }, });
after
import React from 'react'; import { Meteor } from 'meteor/meteor'; import { render } from 'react-dom'; import App from '../imports/ui/App.js'; Meteor.startup(() => { render(<App />, document.getElementById('render-target')); });
そうしたら、次はimportsという新しいディレクトリを作ります。 Meteorのプロジェクトにおいて、importsという名前のディレクトリは特別で、他のものとは異なる挙動をします。
importsの中にないファイルはMeteorサーバーが立ち上がると自動的にロードされますが、importsだけはimport
宣言がされたときのみにロードされます。
importsディレクトリをアプリのディレクトリ直下に作成したら、その中に新しく2つのファイルを作成しましょう。
imports/ui/App.js
import React, { Component } from 'react'; import Task from './Task.js'; // App component - represents the whole app export default class App extends Component { getTasks() { return [ { _id: 1, text: 'This is task 1' }, { _id: 2, text: 'This is task 2' }, { _id: 3, text: 'This is task 3' }, ]; } renderTasks() { return this.getTasks().map((task) => ( <Task key={task._id} task={task} /> )); } render() { return ( <div className="container"> <header> <h1>Todo List</h1> </header> <ul> {this.renderTasks()} </ul> </div> ); } }
imports/ui/Task.js
import React, { Component } from 'react'; // Task component - represents a single todo item export default class Task extends Component { render() { return ( <li>{this.props.task.text}</li> ); } }
ここまでで3つの新しい要素をアプリに追加しました。
3つめの初期化文には、ページがロードされ表示の準備が整った状態で、どのように他のコードを呼び出すかを書いています。
この初期化文は他のコンポーネントをロードし、#render-target
内のHTMLとして描写します。
importsディレクトリがどのように動作するのか、そして実際にはどのような構成でアプリを作っていけば良いのか、より詳細にはApplication Structure articleを読んでみてください。
チュートリアルを進め、コードを追加・変更していく中でこれらのコンポーネントについても再度解説します。
変更結果を確認してみよう
アプリを立ち上げてブラウザを見てみると、アプリの内容が下記のように書き換わっているのが分かるかと思います。
もしこのようになっていなければ、このページ(訳者注: 元ページ)の各コードスニペットの右上リンクからGitHubをチェックし、自分の書いたコードが確実に例と合致しているか確認してみてください。
HTMLファイルには静的コンテンツを書く
Meteorはアプリのフォルダ内の全てのHTMLファイルを解析し、<head>
、<body>
、<template>
の3つのタグを特定します。
<head>
内の全ての要素はクライアントに送信されるHTMLのheadセクションに、そして<body>
内の全ての要素はbodyセクションに追加されます。これは普通のHTMLファイルと同様です。
ただ、<template>
内の全ては「Meteor template」にコンパイルされます。templateはHTML内では{{> templateName}}
、JavaScript内ではTemplate.templateName
で表現されます。
このチュートリアルでは、View内のコンポーネント定義には全てReactを利用するので、templateは利用しません。
ReactでViewを構成する
Reactでは、ViewコンポーネントはReact.Componentのサブクラスです。
なお、このReact.Componentはimports/ui/App.js
で書いたようにimport { Component } from 'react';
という宣言でインポートできます。
自分で作ったコンポーネントには好きなメソッドを持たせることができますが、render
のように特殊な機能を持つメソッドが既に存在しています。
また、コンポーネントはその親からprop
という属性でデータを受け取ることができます
このチュートリアルではさらにいくつかのReactの一般的な特徴について解説していきます。詳細なReactの解説については、FacebookのReactチュートリアルを参照してください。
マークアップ(HTML)の内容はJSXのrenderメソッドでレンダリングされる
どのReactコンポーネントにおいても最も重要なのが、render()
メソッドです。
そのコンポーネントは表示すべきである、というHTMLの内容を取得するためにReactによって呼び出されるメソッドがrender()
です。
Reactでは、HTMLはJSXというJavaScriptの拡張構文で表現します。イメージとしては、JavaScriptファイルの中にHTMLをそのまま書く感じです。
ただし既に見たとおり、明らかに通常のHTMLと異なる点がいくつかあります。
特に理解すべきなのが、JSXではclassではなくclassName属性を用いるということです。 重要なのは、JSXはSpacebarsやAngularのようなテンプレート言語ではない、ということです。JSXはそのまま正規のJavaScriptコードにコンパイルされます。 より詳しくは、Reactのドキュメントで読んでください。
JSXはecmascriptのAtmosphereJSパッケージを利用していますが、新しいMeteorアプリの中にはデフォルトで上記が含まれています。
CSSを追加する
before
/* CSS declarations go here */
after
/* CSS declarations go here */ body { font-family: sans-serif; background-color: #315481; background-image: linear-gradient(to bottom, #315481, #918e82 100%); background-attachment: fixed; position: absolute; top: 0; bottom: 0; left: 0; right: 0; padding: 0; margin: 0; font-size: 14px; } .container { max-width: 600px; margin: 0 auto; min-height: 100%; background: white; } header { background: #d2edf4; background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%); padding: 20px 15px 15px 15px; position: relative; } #login-buttons { display: block; } h1 { font-size: 1.5em; margin: 0; margin-bottom: 10px; display: inline-block; margin-right: 1em; } form { margin-top: 10px; margin-bottom: -10px; position: relative; } .new-task input { box-sizing: border-box; padding: 10px 0; background: transparent; border: none; width: 100%; padding-right: 80px; font-size: 1em; } .new-task input:focus{ outline: 0; } ul { margin: 0; padding: 0; background: white; } .delete { float: right; font-weight: bold; background: none; font-size: 1em; border: none; position: relative; } li { position: relative; list-style: none; padding: 15px; border-bottom: #eee solid 1px; } li .text { margin-left: 10px; } li.checked { color: #888; } li.checked .text { text-decoration: line-through; } li.private { background: #eee; border-color: #ddd; } header .hide-completed { float: right; } .toggle-private { margin-left: 5px; } @media (max-width: 600px) { li { padding: 12px 15px; } .search { width: 150px; clear: both; } .new-task input { padding-bottom: 5px; } }
CSSを追加したので、アプリの見栄えも良くなったはずです。 ブラウザで上記のスタイルが読み込まれているか確認しましょう。
第二弾はここまでです。 どうでしょうか。僕の翻訳もごく一部自信のない部分があるので、もし間違いに気づいたら教えてくださいm( )m
第三弾は来週中にでも。