Learn プログ

-マナビメモ-

Riot.js Observable Rails 2 / 5 「Todoリストを表示する」

はじめに

Riot.js Redux Railsシリーズで作ったTodoアプリのRedux部分をObservableを使って書いた。 (Observerパターンとか全然わかってない。ReduxをObservableで置き換える意味があるかもわからない。なんとなく似ていると思ったのでやってみた。)

Riot.js Redux Railsで作ったTodoアプリと全く同じものを作成するのでコードが大分重なっている(Redux部分をObservableにしたのと、それに伴う変更以外全く同じ)が、今回の一連の記事だけ読んでも出来るように省略はしない。

環境構築

Todo Appの作成

最終的に完成したTodo App
https://github.com/atfeo/Riot_Observable_Rails

参考

Rails

todo-appタグをマウントするトップページを作成

$ rails g controller Top index

routes.rbを書き換える

# .config/routes.rb
Rails.application.routes.draw do
  root 'top#index'
end

次にindex.html.erbにタグを書く

# ./app/views/top/index.html.erb
<todo-app></todo-app>

index.js

まず以下のようにしてみた。

// ./client/src/index.js
import riot from 'riot';

class Store {
  constructor() {
    this.state = { tasks: [] };
    riot.observable(this);
  }

  getState() {
    return this.state;
  }

  setState(value) {
    this.state = Object.assign({}, this.state, value);
  }
}

const store = new Store();

document.addEventListener('DOMContentLoaded', () => {
  riot.mount('todo-app', { store });
});

stateを持つStoreクラスを作成し、インスタンスをobservableで監視出来るようにし、storeインスタンスを作成。作成されたobservableインスタンス(storeインスタンス)をtodo-appタグに渡す。

タグの作成

todo-appタグ

Todo Appのメインとなるtodo-appタグを作成。

<!-- ./client/src/tags/todo-app.tag -->
<todo-app>
  <task-list tasks={this.state.tasks}></task-list>

  <script>
    const actions = require('../actions.js')
    const store = this.opts.store

    // Reduxと違う部分
    store.on('*', () => {
      this.state = store.getState()
      this.update()
    })
  </script>
</todo-app>

Reduxのsubscribeではなくel.on(events, callback)を使い '*' で全てのイベントを監視するようにしてある。 todoのリストはtask-listタグで表示させる。

task-listタグ

<!-- ./client/src/tags/task-list.tag -->
<task-list>
  <ul>
    <li each={task in this.opts.tasks}>
      {task.name}
    </li>
  </ul>
</task-list>

RailsAPIサーバーを作成

RailsJSONを返すAPIサーバーを作成する。

Controller

まずターミナルで

$ rails g controller api/tasks --no-assets

作成したtasks_controller.rbを

# ./app/controllers/api/tasks_controller.rb
class Api::TasksController < ApplicationController
  def index
    @tasks = Task.all
  end
end

このようにし、次にroutes.rbに

# ./config.routes.rb
namespace :api, format: 'json' do
  resources :tasks
end

を追加。

Model

次にまたターミナルで

$ rails g model task name:string

日時_create_tasks.rbに

# ./db/migrate/日時_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[5.0]
  def change
    create_table :tasks do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end

のように , null: false (nameが空だと登録できないようにしている)を追加しターミナルで

$ rails db:migrate

する。また、task.rbにvalidationを追加(同じくnameが空にならないように。しかし、apiとして使う場合モデルのバリデーションは必要ないかもしれない。わからない。)。

# ./app/models/task.rb
class Task < ApplicationRecord
  validates :name, presence: true
end

次にseeds.rbファイルに

# ./db/seeds.rb
Task.create(name: 'Lesson outline')
Task.create(name: 'Record video')

を追加し、ターミナルで

$ rails db:seed

を実行しデータを登録しておく。

View(JSON)

次にJSONを返すindex.json.jbuilderを作成。

# ./app/views/api/tasks/index.json.jbuilder
json.tasks(@tasks) { |t| json.extract! t, :id, :name }

ターミナルで

$ foreman start

し、localhost:5000/api/tasksにアクセスすると {"tasks":[{"id":1,"name":"Lesson outline"},{"id":2,"name":"Record video"}]}が表示されている。

サーバーからの読み込み

actions.jsにサーバーからデータを読み込むためのメソッドを作成する。

// ./client/src/actions.js
module.exports = {
  loadTasks,
};

function loadTasks(store) {
  // redux-thunkのようなmiddlewareを使わなくても良い
  $.ajax({
    url: '/api/tasks.json',
    dataType: 'json',
    success: (res) => {
      // dispatchではなくtriggerでイベントをトリガー
      tasksloaded(store, res.tasks)
    },
    error: (xhr, status, err) => {
      console.log('/api/tasks.json', status, err.toString());
    },
  });
}

function tasksloaded(store, tasks) {
  store.trigger('TASKS_LOADED', { data: tasks });
}

index.jsにマウントするタグの読み込みを追加。
イベントがトリガーされるのを監視して処理する部分を追加。

// ./client/src/index.js
import './tags/todo-app.tag';
import './tags/task-list.tag';

// reducerが担っていた部分
store.on('TASKS_LOADED', (action) => {
  store.setState({ tasks: action.data });
});

todo-app.tagにtodo-appタグがマウントされた時にする処理を追加する。

// ./client/src/tags/todo-app.tag
this.on('mount', () => {
  actions.loadTasks(store)
})

ここで一度、ブラウザでlocalhost:5000を更新して確認。先程登録したデータが表示される。

ロード時のアニメーションをつける

参考動画に沿って擬似的にロード時間を設け、ロード時のアニメーションを表示出来るようにする。アニメーションはデータ読み込み後消えるようにする。

まず2秒間の擬似的なロード時間を設けるために、actions.jsを書き換える。

// ./client/src/actions.js
function loadTasks(store) {
    $.ajax({
      url: '/api/tasks.json',
      dataType: 'json',
      success: (res) => {
        // 書き換え
        setTimeout(() => {
          tasksloaded(store, res.tasks);
        }, 2000)
      },
      error: (xhr, status, err) => {
        console.log('/api/tasks.json', status, err.toString());
      },
    });
  };
}

参考動画のソースのloading.gifをapp/assets/imagesにおく。

次にloading-indicatorタグを作成。

<!-- ./client/src/tags/loading-indicator.tag -->
<loading-indicator>
  <img src="/assets/loading.gif" show={this.opts.loading}>
</loading-indicator>

loading-indicatorタグをtodo-appタグにマウント。

<!-- ./client/src/tags/todo-app.tag -->
<loading-indicator loading={this.state.isLoading}></loading-indicator>

loading-indicatorをマウントできるようにindex.jsで読み込む。

// ./client/src/index.js
import './tags/loading-indicator.tag';

状態の変化のためのメソッド(イベントのトリガー)を作成。

// ./client/src/actions.js
function toggleLoading(store, isLoading) {
  store.trigger('TOGGLE_LOADING', { data: isLoading });
}

イベントがトリガーされた時の処理をindex.jsに追加。

// ./client/src/index.js
store.on('TOGGLE_LOADING', (action) => {
  store.setState({ isLoading: action.data });
});

ここまででloading-indicatorの用意は出来たので、loadTasks()にイベントをトリガーする処理を追加。

// ./client/src/actions.js
function loadTasks(store) {
    // 追加
    toggleLoading(store, true);
    $.ajax({
      url: '/api/tasks.json',
      dataType: 'json',
      success: (res) => {
        setTimeout(() => {
          tasksloaded(store, res.tasks);
          // 追加
          toggleLoading(store, false);
        }, 2000)
      },
      error: (xhr, status, err) => {
        // 追加
        toggleLoading(store, false);
        console.log('/api/tasks.json', status, err.toString());
      },
    });
  };
}

ブラウザでlocalhost:5000を更新して確認。
まずloading-indicatorが表示され、2秒後にデータの表示とともにloading-indicatorが消える。

Reduxのdispatch(action)をel.trigger(event)、reducerの処理をel.on(event)にすることで、同じようなものができた。

Riot Observable Rails 3 / 3へ続く