プログラミングとSEOと暇つぶし

駆け出しエンジニアdallPのブログです。元SEOコンサルタントです。プログラミング、SEO、アフィリエイト、お金などについて役に立つかもしれない情報をやりたいように書きます。

MeteorJSとReactを勉強してみる その5: タスクの完了と削除を実装してみよう【公式翻訳】

MeteorJSを勉強してみる第5弾です。 タスクを追加できるなら、もちろん削除もできるようにしたいですよね。


タスクの完了と削除を実装する

ここまでは、ただコレクションにドキュメント(タスクのデータ)を追加してきたのみでした。

今回は、これまでに追加したドキュメントを更新する方法と、削除する方法を学びましょう。

タスクのコンポーネントに、チェックボックスと削除ボタン、二つの要素を追加します。 いずれもイベントハンドラと一緒に追加してあげましょう。

imports/ui/Task.js

before

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

after

import React, { Component } from 'react';

import { Tasks } from '../api/tasks.js';

// Task component - represents a single todo item
export default class Task extends Component {
  toggleChecked() {
    // set the checked property to the opposite of its current value
    Tasks.update(this.props.task._id, {
      $set: { checked: !this.props.task.checked },
    })
  };

  deleteThisTask() {
    Tasks.remove(this.props.task._id);
  };

  render() {
    // give tasks a different className when they are checked off,
    // so that we can style them nicely in CSS.
    const taskClassName = this.props.task.checked ? 'checked' : '';

    return (
      <li className={ taskClassName }>
        <button className="delete" onClick={ this.deleteThisTask.bind(this) }>
          &times;
        </button>

        <input
          type="checkbox"
          readOnly
          checked={ !!this.props.task.checked }
          onClick={ this.toggleChecked.bind(this) }
        />

        <span className="text">{ this.props.task.text }</span>
      </li>
    );
  }
}

ドキュメントのアップデート

上記のコード内では、タスクにチェックをつけて完了させる際にTasks.updateを呼んでいます。

コレクションの持つupdate関数は、二つの引数を取ります。

  • 一つ目はコレクションの中でも「どのドキュメントを更新するか」を特定するセレクタ
  • 二つ目はマッチしたオブジェクトに何をすべきかを示す更新内容のパラメータ

です。

この場合、セレクタはタスクの_idです。 また、更新パラメータでは、「そのタスクは完了している」ことを表すためのcheckedを切り替えるために$setを使用しています。

ドキュメントの削除

また、上記コード内ではタスクを削除するためにTasks.removeを呼んでいます。 remove関数は引数をひとつだけ取り、その引数で「どのドキュメントを削除するか」を特定します。


短いですが第五回はこれだけです。

今更ですが、ここまで出てきた「コレクション」とか「フィールド」とか「ドキュメント」などは全てMongoDBの用語です。

僕は普段の業務ではMySQLを使っているので正直あまり慣れないですが、基本的な考え方は一緒です。

まだ調べてなかった人は、是非一度自分でググってみてください。

今週中にあと二記事くらいは上げたい所存です。

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の変更内容を理解するのは鬼門かもしれませんね・・・

とは言え僕も解説できるほどではないので、折を見てちゃんと記事を更新したいと思います。

次回の更新は来週になります。