redux Vol1

Reactと一緒にされることが多いReduxですが、まずはReactなしでいきます。

# やること： API → 表示 

<img src="https://j.gifs.com/Y6mEX0.gif">

Calil のAPI から図書館情報を取得して一覧で表示。
https://calil.jp/doc/api.html

同じ構成でIos(ReSwift)実装はこちら
http://qiita.com/nakadoribooks/items/c5f53071abdfafd47d4f

## 成果物
https://github.com/nakadoribooks/CalilClient-web/releases/tag/vol1

#準備

### 環境

es2015, Babel

### ライブラリ

Redux, superagent

### 環境設定

```json:package.json
{
  "name": "CalilClient-web",
  "version": "1.0.0",
  "description": "CalilClient-web",
  "main": "main.js",
  "scripts": {},
  "repository": {
    "type": "git",
    "url": "git+https://github.com/nakadoribooks/CalilClient-web"
  },
  "author": "nakadoribooks",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/nakadoribooks/CalilClient-web/issues"
  },
  "homepage": "https://github.com/nakadoribooks/CalilClient-web#readme",
  "devDependencies": {
    "babel-core": "^6.23.1",
    "babel-loader": "^6.2.10",
    "babel-preset-es2015": "^6.22.0",
    "webpack": "^2.2.1"
  },
  "dependencies": {
    "redux": "^3.0.0",
    "superagent": "^3.5.0",
    "superagent-jsonp": "^0.1.1"
  }
}
```

`npm install`

```javascript:webpack.config.js
var webpack = require('webpack');
var path = require('path');

var config = {
  devtool: "eval",
  context: __dirname + '/source',
  entry: {
    'application': './main.js'
  },
  output: {
    path: __dirname + '/public/javascript',
    filename: '[name].js'
  },
  resolve: {
      extensions: ['.js']
  },
  plugins: [],
  module: {
    loaders: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query: {
          presets: ['es2015']
        }
      }
    ]
  }

};

module.exports = config;
```

`node_modules/.bin/webpack --watch`

###ファイル構成
![スクリーンショット 2017-03-11 17.24.22.png](https://qiita-image-store.s3.amazonaws.com/0/159266/447b0036-7651-be7e-b4c1-e01ea2c4a277.png)


# 処理の流れ

0: globalにstoreを作る (Global)

```javascript:main.js
import { applyMiddleware, createStore } from 'redux';
import reducer from './model/reducer/CalilReducer.js'
import LibraryListViewController from './vc/list/LibraryListViewController.js'

// setup redux. put global
const store = createStore(reducer);
window.store = store;
// ↑ これ

// createView
var appDom = document.getElementById("app");
var libraryListViewController = new LibraryListViewController();
appDom.appendChild(libraryListViewController.view());
```

1: 読み込みイベントを作る (View)

```javascript:LibraryListViewController.js
_dispatchLoadData(){
  let action = ActionCreator.loadLibraries() /*← こっち*/
  store.dispatch(action)
}
```

1.5: 読み込む and Actionを作って返す (ActionCreator)

```swift:ActionCreator.js

import {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction} from './actions/Actions.js'

// 〜 略 〜 

static loadLibraries(prefName="秋田県") {

    var params = { appkey: config.API_KEY, pref: prefName, format: "json", callback: "f" };
    superagent.get(config.API_LIBRARY).query(params).use(superagentJsonp({timeout: 1000}))
    .end(function(err, res){

      // 〜 略 〜 
      var libraryList = Library.createList(list)

      /* 読み込んだら loadedActionをdispatchして 5 へ*/
      var loadedAction = Object.assign(LibraryListLoadedAction)
      loadedAction.libraryList = libraryList
      store.dispatch(loadedAction)

    })
    
    // 読み込み開始アクションを返す 2 へ
    return Object.assign(LibraryListLoadAction)
}

```

2: (from 1) 作ったイベントをdispatch (View)

```javascript:LibraryListViewController.js
_dispatchLoadData(){
  let action = ActionCreator.loadLibraries()
  store.dispatch(action) /*← こっち*/
}
```

3: loadActionを受け取ってStateのisLoading を trueに (Reducer)

```javascript:CalilReducer.js
case LibraryListLoadAction.type:
      return Object.assign({}, state, {
        isLoading: true,
        errorMessage: null
      })
```

4: stateの変更を受け取ってViewを更新 (View)

```javascript:LibraryListViewController.js

// 〜 略 〜  
store.subscribe(() => {
  this._newState(store.getState())
})
// 〜 略 〜  

_newState(state){

	// 〜 略 〜  

	// loading
	if(state.isLoading){
		console.log("loading")
		this._button.setAttribute("disabled", "disabled")
		this._statusLabel.innerHTML = '<span style="color:bray">読み込み中</span>'
		return
	}

        // 〜 略 〜  
```

5: (from 1.5) 読み込み完了のイベントを受け取ってStateを更新 (Reducer)

```javascript:CalilReducer.js
    case LibraryListLoadedAction.type:
      return Object.assign({}, state, {
        isLoading: false,
        libraryList: action.libraryList,
        errorMessage: null
      })
```

6: stateの変更を受け取ってViewを更新 (View)

```javascript:LibraryListViewController.js
_newState(state){
	// 〜 略 〜

	// loaded
	console.log("loaded")

	this._statusLabel.innerHTML = '<span style="color:blue">読み込み完了</span>'
	this._reloadListView(state.libraryList)
}
```

# 以下、コード全体

##State 3つ
読み込み中フラグ
表示するデータ
エラー有無

```javascript:AppState.js
export default {
  isLoading: false,
  libraryList: [],
  errorMessage: null
}
```

##Action 3つ
読み込み開始
読み込み完了
エラー

```javascript:Actions.js
let LibraryListLoadAction = {
  type: "LibraryListLoadAction"
}

let LibraryListFailLoadAction = {
  type: "LibraryListFailLoadAction"
}

let LibraryListLoadedAction = {
  type : "LibraryListLoadedAction"
  , libraryList: []
}

export {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction};

```

## Reducer: Actionを受け取ってStateを変更
```javascript:CalilReducer.js
import {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction} from '../action/actions/Actions.js'
import initialState from '../state/AppState.js'

export default function reducer(state = initialState, action) {

  switch (action.type) {
    case LibraryListLoadAction.type:
      return Object.assign({}, state, {
        isLoading: true,
        errorMessage: null
      })
    case LibraryListFailLoadAction.type:
      return Object.assign({}, state, {
        isLoading: true,
        errorMessage: "通信に失敗しました"
      })
    case LibraryListLoadedAction.type:
      return Object.assign({}, state, {
        isLoading: false,
        libraryList: action.libraryList,
        errorMessage: null
      })
    default:
      return state
  }

}

```

## ActionCreator: Action を作って返却 or 通信 → Action作って dispatch
この中でdispatchするのはどうなんだろう。

```javascript:ActionCreator.js
import {LibraryListLoadAction, LibraryListFailLoadAction, LibraryListLoadedAction} from './actions/Actions.js'
import Library from '../entity/Library.js'
import superagent from 'superagent'
import superagentJsonp from 'superagent-jsonp'
import config from '../../config.js'

class ActionCreators{

  static loadLibraries(prefName="秋田県") {

    var params = { appkey: config.API_KEY, pref: prefName, format: "json", callback: "f" };
    superagent.get(config.API_LIBRARY).query(params).use(superagentJsonp({timeout: 1000}))
    .end(function(err, res){

      // error
      if(err != null){
        let errorAction = Object.assign(LibraryListFailLoadAction)
        store.dispatch(errorAction)
        return;
      }

      var list = res.body
      // wrong dictionary
      if (list == null){
        let errorAction = Object.assign(LibraryListFailLoadAction)
        store.dispatch(errorAction)
        return;
      }

      // success
      var libraryList = Library.createList(list)
      var loadedAction = Object.assign(LibraryListLoadedAction)
      loadedAction.libraryList = libraryList

      // 時間かかっているてい
      setTimeout(function(){
        store.dispatch(loadedAction)
      }, 1000)

    })

    return Object.assign(LibraryListLoadAction)
  }
}

export default ActionCreators
```

## View: Stateの変更を受け取って表示を更新
```javascript:LibraryListViewController.js
import LibraryCell from './LibraryCell.js'
import ActionCreator from '../../model/action/ActionCreator.js'

class LibraryListViewController {

  constructor() {
    var view = document.createElement("div")
    var listView = document.createElement("ul")
    var headerView = document.createElement("div")
    var statusLabel = document.createElement("p")
    var button = document.createElement("button")
    button.appendChild(document.createTextNode("読み込む"))
    button.addEventListener("click", (event)=>{
      this._tapReload(event)
    }, false);

    listView.style.height = window.parent.screen.height + "px"
    listView.style.overflow = "auto"

    headerView.appendChild(statusLabel)
    headerView.appendChild(button)

    view.appendChild(headerView)
    view.appendChild(listView)

    // save property
    this._libraryList = []
    this._libraryCellList = []
    this._view = view
    this._listView = listView
    this._button = button
    this._statusLabel = statusLabel
    
    // storeの変更通知を受け取る
    store.subscribe(() => {
      this._newState(store.getState())
    })

    this._dispatchLoadData();
  }

  _tapReload(event){
    console.log(event)
    console.log(this)
    this._dispatchLoadData()
  }

  _dispatchLoadData(){
    let action = ActionCreator.loadLibraries();
    store.dispatch(action);
  }

  _newState(state){
    console.log("onChangeStore")
    this._button.removeAttribute("disabled")

    // error
    if (state.errorMessage != null){
      console.log("error");
      this._statusLabel.innerHTML = '<span style="color:red">読み込み失敗</span>'
      return
    }

    // loading
    if(state.isLoading){
      console.log("loading")
      this._button.setAttribute("disabled", "disabled")
      this._statusLabel.innerHTML = '<span style="color:bray">読み込み中</span>'
      return
    }

    // loaded
    console.log("loaded")

    this._statusLabel.innerHTML = '<span style="color:blue">読み込み完了</span>'
    this._reloadListView(state.libraryList)
  }

  _reloadListView(libraryList){
    this._libraryList = libraryList

    // reloadListView
    let listView = this._listView
    let libraryCellList = this._libraryCellList
    for(var i=0,max=libraryCellList.length;i<max;i++){
      let cell = libraryCellList[i]
      listView.removeChild(cell.view())
    }
    this._libraryCellList = []

    for(var i=0,max=libraryList.length;i<max;i++){
      var library = libraryList[i]
      var cell = new LibraryCell()
      cell.reload(library)
      listView.appendChild(cell.view())
      this._libraryCellList.push(cell)
    }
  }

  // public

  view(){
    return this._view;
  }

}

export default LibraryListViewController;

```

# その他

```javascript:config.js

var API_ROOT = "https://api.calil.jp/"

export default {
  API_ROOT: API_ROOT,
  API_LIBRARY: API_ROOT + "library",
  API_KEY: "変更してね"
}

```

カーリルに開発者登録してAPI_KEYをゲットして書き換えてね。


```javascript:main.js
import { applyMiddleware, createStore } from 'redux';
import reducer from './model/reducer/CalilReducer.js'
import LibraryListViewController from './vc/list/LibraryListViewController.js'

// setup redux. put global
const store = createStore(reducer);
window.store = store;

// createView
var appDom = document.getElementById("app");
var libraryListViewController = new LibraryListViewController();
appDom.appendChild(libraryListViewController.view());
```

AppDelegate てきなやつ

```html:pubic/index.html
<!doctype html>
<html>
<head>
  <title>CalilClient</title>
  <meta charset="UTF-8">
</head>
<body style="margin:0px;padding:0px;">
  <div id="app"></div>
  <script src="javascript/application.js"></script>
</body>
</html>
```

# 課題
非同期のActionどうしよう。

# ありがとう Calil APi

https://calil.jp/doc/api.html
> 引用
未来の図書館を作るのはあなただ！
全国6000の図書館に簡単アクセス！ カーリルの図書館APIを使ってみよう
全国の図書館を対象としたリアルタイム蔵書検索を可能にするAPI群を開発者向けに提供します。
法人・個人を問わず無償で利用できます。