Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA　フロントエンド編

## はじめに
最近 Riot が気になっていたので、
Lumen 5.4 と Riot 3.3 で簡単なブログを作ってみることにした。  
  
今回はフロントエンド編。
DB との接続なしのモックアップを作っていく。
Material Design for Bootstrap も気になっていたのでついでに使ってみる。  
  
ダミーテキストは[Webtools](https://webtools.yjhm214.com/)のダミーテキストジェネレータで作成している。
これを使えば日本語・英語・数字混合のダミーテキストが作れる。
  
そのほかのエントリーはこちら。
[Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA　バックエンド編](http://www.yjhm214.com/entry/lumen-riot-spa-backend)
[Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA　つなぎこみ編](http://www.yjhm214.com/entry/lumen-riot-spa-system-connection)


## 作るもの
いろいろ頑張って最終的にこんな感じのブログを作る。

- 記事一覧の表示（カテゴリーの絞り込みあり）
- 記事詳細の表示
- 記事の新規作成・更新

記事一覧  
![Blog-list.png](https://qiita-image-store.s3.amazonaws.com/0/41378/f0965c54-cbc0-63c9-9beb-09168211496f.png)

記事詳細  
![Blog-post.png](https://qiita-image-store.s3.amazonaws.com/0/41378/526ee470-971b-cdb0-7fb1-d0c24f5ca771.png)
 
記事編集  
![Blog-edit.png](https://qiita-image-store.s3.amazonaws.com/0/41378/613c4d08-b9da-16e5-82de-f7ffd2f0a831.png)

記事保存
![Blog-saved.png](https://qiita-image-store.s3.amazonaws.com/0/41378/9c6601d1-5210-b28b-b369-9876eb297b8b.png)


## 前提
- Lumen 5.4.5
- Riot.js 3.3.1


## 手順
1. Lumen ルーティングの作成
1. 必要なCSS/JSファイルのダウンロードと設置
    1. Material Design for Bootstrapで使うCSS/JSファイルのダウンロードと設置
    1. Riot で使う JS ファイルの設定
1. ベースとなる View の作成
1. CSS の作成
1. Riot タグの作成
    1. ナビゲーション
    1. 一覧画面
    1. 詳細画面
    1. 編集画面
1. Riot ルーターの作成

## 1. Lumen ルーティングの作成
ベースとなる View ファイルを読み込むためのルーティングを
`routes/web.php` に作成する。

```
$app->get('/', ['as' => 'index', function () use ($app) {
    return view('index');
}]);

$app->get('/categories', function ()  {
     return redirect()->route('index');
});
$app->group(['prefix' => 'categories'], function ($app) {
    $app->get('{any}', function ()  {
        return view('index');
    });
});

```
`/categories` や `/posts` でルートを切っているのは、
これらを切らないと `/categories/{category}` や `/posts/{id}` の画面でリロードすると ルートが存在しなくてエラーになってしまうため。

`/` へのリダイレクトや `index.blade.php` を表示させることで、
いつでもベースとなる view を表示させ、
そのあと Riot のルーティングにより
URL で指定された view を表示させるようにする。

## 2. 必要なCSS/JSファイルのダウンロードと設置
### 2.1. Material Design for Bootstrapで使うCSS/JSファイルのダウンロードと設置
今回は Material Design for Bootstrap を使うため、
必要なファイルをダウンロードする。

- [Bootstrap](http://getbootstrap.com/getting-started/) 
- [Material Design for Bootstrap](http://fezvrasta.github.io/bootstrap-material-design/bootstrap-elements.html)
- [jquery](https://jquery.com/)

ダウンロードしたらそれぞれ以下のようにファイルを設置する。  

また、自分で記述する CSS の空ファイル(`blog.css`) もここで作成しておく。

```
root
├── public
│   ├── css
│   │   ├── bootstrap.css
│   │   ├── bootstrap-material-design.css
│   │   ├── ripples.css
│   │   └── blog.css
│   │
│   └── js
│       ├── jquery-3.1.1.js
│       ├── bootstrap.js
│       ├── material.js
│       └── ripples.js
│    
│    
```

### 2.2. Riot で使う JS ファイルの設定
Riot では以下の２つを使う。

- Riot 本体 
    -[riot+compiler.js](http://riotjs.com/ja/download/) の『ダウンロード』よりダウンロード
- Riot router (ルーティングライブラリ) 
    - [route.min](https://github.com/riot/route) の 『Download by yourself』より ダウンロード

それぞれ `public/js`に設置する。

また、Riot のルーターを定義するファイルとして 
`public/js/app.js` の空ファイルを用意しておく。



## 3. ベースとなる View の作成
JS/CSS を配置したら、ベースとなる view `resources/views/index.blade.php` を作成する。
SPA のため、用意する view は これだけ。

```
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta http-equiv="Cache-Control" content="no-cache">
  <meta http-equiv="Pragma" content="no-cache">
  <meta http-equiv="cache-control" content="no-cache">
  <meta http-equiv="expires" content="0">
  <meta name="description" content="">
  <meta property="og:type" content="website">
  <meta property="fb:app_id" content="">
  <meta property="og:title" content="">
  <meta property="og:description" content="">
  <meta property="og:image" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Blog</title>

  <!-- font -->
  <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Roboto:300,400,500,700" type="text/css">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <!-- font -->

  <!-- Material Design for Bootstrap -->
  <link href="{{ url('/css/bootstrap.css') }}" rel="stylesheet">
  <link href="{{ url('/css/bootstrap-material-design.css') }}" rel="stylesheet">
  <link href="{{ url('/css/ripples.css') }}" rel="stylesheet">
  <!-- Material Design for Bootstrap -->

  <link href="{{ url('/css/blog.css') }}" rel="stylesheet">
</head>


<body>
  <header></header>
  <article></article>

  <!-- Material Design for Bootstrap -->
  <script src="{{ url('/js/jquery-3.1.1.js') }}"></script>
  <script src="{{ url('/js/bootstrap.js') }}"></script>
  <script src="{{ url('/js/material.js') }}"></script>
  <script src="{{ url('/js/ripples.js') }}"></script>
  <script>$.material.init();</script>
  <!-- Material Design for Bootstrap -->

  <!-- Riot -->
  <script src="{{ url('/js/riot+compiler.js') }}"></script>
  <script src="{{ url('/js/route.min.js') }}"></script>
  <script src="{{ url('/tags/raw.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/tags/navbar.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/tags/list.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/tags/post.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/tags/edit.tag') }}" type="riot/tag"></script>
  <script src="{{ url('/js/app.js') }}"></script>
  <!-- Riot -->

  <!-- Saved ダイアログ -->
  <div id="saved-dialog" class="modal fade" tabindex="-1" style="display: none;">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-body">
          <p>Saved.</p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary" data-dismiss="modal">OK<div class="ripple-container"><div class="ripple ripple-on ripple-out" style="left: 40.6562px; top: 20px; background-color: rgb(0, 150, 136); transform: scale(10.875);"></div></div></button>
        </div>
      </div>
    </div>
  </div>
  <!-- Saved ダイアログ -->

  <!-- Connection エラー ダイアログ -->
  <div id="connection-error-dialog" class="modal fade" tabindex="-1" style="display: none;">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-body">
          <p>Connection error. Try again.</p>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary" data-dismiss="modal">OK<div class="ripple-container"><div class="ripple ripple-on ripple-out" style="left: 40.6562px; top: 20px; background-color: rgb(0, 150, 136); transform: scale(10.875);"></div></div></button>
        </div>
      </div>
    </div>
  </div>
  <!-- Connection エラー ダイアログ -->

</body>

</html>

```
### ポイント  
- `<header></header>` に Riot のカスタムタグを挿入する。
- `<article></article>` にも Riot のカスタムタグを挿入する。この部分が切り替わることで SPA を実現。


## 4. CSS の作成
全体で利用する共通の CSS を `public/css/blog.css` に書いていく。
こんな感じ。

```
body {
    line-height: 1.8;
    font-size: 16px;
}

article {
    width: 65%;
    min-width: 300px;
    margin: 0 auto;
}

a:hover {
    text-decoration: none;
}

#edit-btn {
    position: fixed;
    bottom: 40px;
    right: 2%;
}

.label-Tech {
  background-color: #ff5722;
}

.label-Book {
  background-color: #03a9f4;
}

.label-Hobby {
  background-color: #4caf50;
}

.label-Others {
  background-color: #9e9e9e;
}

.p15 {
    padding: 15px;
}

.p50 {
    padding: 50px;
}

.pt0 {
  padding-top: 0!important;
}

.mt10 {
    margin-top: 10px;
}

.mt20 {
    margin-top: 20px;
}
.mr5 {
    margin-right: 5px;
}

.mb0 {
    margin-bottom: 0;
}

.right-align {
  text-align: right;
}

.center {
    text-align: center;
}

```

## 5. Riot タグの作成
ようやく Riot タグを作っていく。
ここでは、
- navbar.tag -- ヘッダーのナビゲーション
- raw.tag    -- Riot による自動エスケープを避けるためのタグ
- list.tag   -- ブログ記事一覧画面
- post.tag   -- ブログ記事詳細画面
- edit.tag   -- ブログ記事編集画面

を作成する。
それぞれ、`public/tags` 以下に作成。

### 5.1. ナビゲーション
navbar.tag

```
<navbar>
  <nav class="navbar navbar-default">
    <div class="container-fluid">
      <!-- Brand and toggle get grouped for better mobile display -->
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <h1>
          <a class="navbar-brand" href="/">Blog</a>
        </h1>
      </div>

      <!-- Collect the nav links, forms, and other content for toggling -->
      <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
        <ul class="nav navbar-nav navbar-right">
          <li each="{ categories }"><a href="/categories/{ name.toLowerCase() }">{ name }</a></li>
        </ul>
      </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
  </nav>

  <style>
    h1 { margin: 0; }
  </style>

  <script>
    this.categories = [
      {id: 1, name: 'Tech'},
      {id: 2, name: 'Book'},
      {id: 3, name: 'Hobby'},
      {id: 4, name: 'Others'},
    ]
    console.log(this.categories)
  </script>

</navbar>

```

#### ポイント
- `<navbar></navbar>` で囲むことで、 viwe 側で このタグが使えるようになる。
- html を記述し、そのタグに適用したいスタイルを `<style>` に記述し、そのタグに適用したい JS を `<script>` に記述。
- データは `this.categories` のように、`this`(Riot タグインスタンス) のプロパティとして持たせることでタグ内で使えるようになる。  
- 持たせたデータは `{ }` で囲むことで展開される。  
- `{}` 内ではjavascript が書ける！
- ぐるぐる回したい場合は、`<li each="{ categories }"><a href="/category/{ name.toLowerCase() }">{ name }</a></li>` のように `each="{ categories }"` とかく。

ちなみに、Riot 3 からは [Scoped CSSがデフォルトに](http://riotjs.com/ja/guide/migration-from-riot2/) なったため、
`<style>` 要素に `scoped` 属性を追加する必要がなくなった！
あと、`<script>`タグはあってもなくてもOK


### 5.2. Riot による自動エスケープを避けるためのタグ
Riot はテンプレート変数を自動でエスケープするため、改行などが反映されない。
[エスケープしないでHTMLを表示する](http://riotjs.com/ja/guide/#テンプレート変数-expressions) を参考にエスケープしないカスタムタグを定義する。  

raw.tag

```
<raw>
  <span></span>

  <script>
    this.root.innerHTML = opts.content
  </script>
</raw>

```
#### ポイント
- `this.root` : 作成した Riot タグ自身


### 5.3. 一覧画面
list.tag

```
<list>
  <ul>
    <li each="{list}">
      <div class="card p15">
        <div class="card-block">
          <h2 class="card-title mb0"><raw content="{ title }"></raw></h2>

          <a each="{ category in categories }" href="/categories/{ category.toLowerCase() }">
            <span  class="label label-{ category } mr5">{ category }</span>
          </a>

          <p class="card-text mt20"><raw content="{ text }"></raw></p>
          <div class="right-align">
            <a href="/posts/{ id }" class="btn btn-primary btn-raised">Contiune</a>
          </div>
        </div>
      </div>
    </li>
  </ul>
  <a id="edit-btn" href="/posts/new/edit" class="btn btn-danger btn-fab">
    <i class="material-icons">mode_edit</i>
  </a>


  <style>
    ul {
        padding-left: 0;
    }

    ul li {
        list-style: none;
        margin-bottom: 20px;
    }
  </style>


  <script>
    this.list = [
      {
        id: 1,
        title: 'Lorem ipsum dolor si',
        text: 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec',
        categories: ['Tech', 'Book'],
      },
      {
        id: 2,
        title: 'Lorem ipsum dolor si',
        text: 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec',
        categories: ['Book', 'Hobby', 'Others']
      },
      {
        id: 3,
        title: 'Lorem ipsum dolor si',
        text: 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec',
        categories: ['Hobby', 'Others']
      },
      {
        id: 4,
        title: 'Lorem ipsum dolor si',
        text: 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec',
        categories: ['Others']
      },
    ];
    console.log(this.list)
  </script>
</list>

```
#### ポイント
- ぐるぐる回す `each`は、`<a each="{ category in categories }" href="/category/{ category.toLowerCase() }">` のように、`each={ category in categories }` としても使える。



### 5.4. 詳細画面
post.tag

```
<post>
  <div class="card p15">
    <div class="card-block">
      <h2 class="card-title mb0"><raw content="{ title }"></raw></h2>

      <a each="{ category in categories }" href="/categories/{ category.toLowerCase() }">
        <span  class="label label-{ category } mr5">{ category }</span>
      </a>

      <p class="card-text mt10"><raw content="{ text }"></raw></p>
    </div>
  </div> 
  <a id="edit-btn" href="/posts/{ id }/edit" class="btn btn-danger btn-fab">
    <i class="material-icons">mode_edit</i>
  </a>

  <style></style>

  <script>
    this.id         = 1
    this.title      = 'Lorem ipsum dolor si'
    this.text       = 'これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consectetur.これはダミーテキストです。フォントやレイアウトを確認するために入れています。Lorem ipsum dolor sit amet, consec'
    this.categories = ['Tech', 'Book']
  </script>
</post>


```
一覧画面と詳細画面で使った文法とほぼ同じ。


### 5.5. 編集画面
submit などのファンクションはまだ実装しない。


edit.tag

```
<edit>

  <form id="edit-form" class="form-horizontal card p50">
    <fieldset>
      <legend>Edit</legend>
      <div class="form-group">
        <label for="inputTitle" class="col-md-1 control-label">Title</label>
        <div class="col-md-11">
          <input type="text" name="title" class="form-control" id="inputTitle" placeholder="Title..." value="{ post.title }">
          <span class="help-block">100 文字以内</span>
        </div>
      </div>


      <div class="form-group">
        <label for="" class="col-md-1 control-label pt0">Category</label>
        <div class="checkbox col-md-2" each="{ categories }">
          <label>
            <input type="checkbox" name="categories[]" value="{ id }" checked="{ (post.categories)&&(post.categories.indexOf(name) >= 0) }"><span class="checkbox-material"><span class="check"></span></span> { name }
          </label>
        </div>
      </div>


      <div class="form-group">
        <label for="inputText" class="col-md-1 control-label">Text</label>
        <div class="col-md-11">
          <textarea name="text" class="form-control" rows="15" id="inputText" value="{ post.text }"></textarea>
          <span class="help-block">5000 文字以内</span>
        </div>
      </div>

      <div class="form-group center">
        <div class="col-md-11">
          <button type="button" class="btn btn-default" onClick="{ cancel }">Cancel</button>
          <button type="submit" class="btn btn-primary" onClick="{ submit }">Submit</button>
        </div>
      </div>

    </fieldset>
  </form>


  <style>
  </style>


  <script>
    this.categories = [
      {id: 1, name: 'Tech'},
      {id: 2, name: 'Book'},
      {id: 3, name: 'Hobby'},
      {id: 4, name: 'Others'},
    ]
    console.log(this.categories)

    this.post = {}

    cancel() {
      history.back()
    }

    submit(e) {
      e.preventDefault()
    }
  </script>

</edit>
```
#### ポイント
- テキストエリアやテキストボックスの `value="{ post.title }"` は更新時のデフォルト値
- Riot の`checked属性`、`selected属性`は、`checked="{ 変数 }"` と書け、変数=false/undefined の場合は無視される。
- クリックイベントは `onClick="{ submit }"`と書ける。 



## 6. Riot ルーターの作成
最後にルータを作る。

app.js

```
route.base('/')
riot.mount('header', 'navbar')

route(function(collection, id, action){
  console.log('collection: ' + collection)
  console.log('id: ' + id)
  console.log('action: ' + action)
  riot.mount('article', collection || 'list', {id: id, action: action})
})

route('/categories/*', function(categoryName) {
  console.log('categoryName ' + categoryName)
  riot.mount('article', 'list', {categoryName: categoryName})
})

route('/posts/*', function(id, action) {
  console.log('id ' + id)
  riot.mount('article', 'post', {id: id})
})

route('/posts/*/edit', function(id) {
  console.log('id ' + id)
  riot.mount('article', 'edit', {id: id})
})

route.start(true)
```

#### ポイント
- `route.base('/')` でペースパスをデフォルトの`#`から`/`に変更。([ルータをカスタマイズする](http://riotjs.com/ja/api/route/#ルータをカスタマイズする))
- `riot.mount('header', 'navbar')` は `riot.mount(selector, tagName, [opts])` の書き方で、view上の`selector`にカスタムタグ`tagName`を設置する、という意味。
- `route(function(collection, id, action){})` でルーティングの設定を行う。 ([ルーティングの設定](http://riotjs.com/ja/api/route/#ルーティングの設定))
- `route.start(true)` でURL変更の検知を開始する。([URL変更の検知](http://riotjs.com/ja/api/route/#routestart))


## まとめ
Riot やばい。
Angular とか React とか Vue とかと比べると一番簡単。
学習コストがほぼ０。
  
Material Design for Bootstrap やばい。
ほとんど CSS 書いてないのにめちゃマテリアルデザインになる。



そのほかのエントリーはこちら。
[Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA　バックエンド編](http://www.yjhm214.com/entry/lumen-riot-spa-backend)
[Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA　つなぎこみ編](http://www.yjhm214.com/entry/lumen-riot-spa-system-connection)