はじめに
最近 Riot が気になっていたので、
Lumen 5.4 と Riot 3.3 で簡単なブログを作ってみることにした。
今回はフロントエンド編。
DB との接続なしのモックアップを作っていく。
Material Design for Bootstrap も気になっていたのでついでに使ってみる。
ダミーテキストはWebtoolsのダミーテキストジェネレータで作成している。
これを使えば日本語・英語・数字混合のダミーテキストが作れる。
そのほかのエントリーはこちら。
Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA バックエンド編
Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA つなぎこみ編
作るもの
いろいろ頑張って最終的にこんな感じのブログを作る。
- 記事一覧の表示(カテゴリーの絞り込みあり)
- 記事詳細の表示
- 記事の新規作成・更新
前提
- Lumen 5.4.5
- Riot.js 3.3.1
手順
- Lumen ルーティングの作成
- 必要なCSS/JSファイルのダウンロードと設置
- Material Design for Bootstrapで使うCSS/JSファイルのダウンロードと設置
- Riot で使う JS ファイルの設定
- ベースとなる View の作成
- CSS の作成
- Riot タグの作成
- ナビゲーション
- 一覧画面
- 詳細画面
- 編集画面
- 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 を使うため、
必要なファイルをダウンロードする。
ダウンロードしたらそれぞれ以下のようにファイルを設置する。
また、自分で記述する 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 では以下の2つを使う。
- Riot 本体
-riot+compiler.js の『ダウンロード』よりダウンロード - Riot router (ルーティングライブラリ)
- route.min の 『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がデフォルトに なったため、
<style>
要素に scoped
属性を追加する必要がなくなった!
あと、<script>
タグはあってもなくてもOK
5.2. Riot による自動エスケープを避けるためのタグ
Riot はテンプレート変数を自動でエスケープするため、改行などが反映されない。
エスケープしないでHTMLを表示する を参考にエスケープしないカスタムタグを定義する。
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('/')
でペースパスをデフォルトの#
から/
に変更。(ルータをカスタマイズする) -
riot.mount('header', 'navbar')
はriot.mount(selector, tagName, [opts])
の書き方で、view上のselector
にカスタムタグtagName
を設置する、という意味。 -
route(function(collection, id, action){})
でルーティングの設定を行う。 (ルーティングの設定) -
route.start(true)
でURL変更の検知を開始する。(URL変更の検知)
まとめ
Riot やばい。
Angular とか React とか Vue とかと比べると一番簡単。
学習コストがほぼ0。
Material Design for Bootstrap やばい。
ほとんど CSS 書いてないのにめちゃマテリアルデザインになる。
そのほかのエントリーはこちら。
Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA バックエンド編
Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA つなぎこみ編