Programming Phoenix勉強その13
Programming Phoenix勉強その13
その13です。ここからPart2です。ここから機能をちゃんと整備します。
ビデオに対してリアルタイムコメントを付けられるように
ビデオを再生可能に
をやっていくようです。はじめにビデオを再生可能にしていきます。
視聴用ページ作成
投稿されたビデオを見るためのページを作ります。いつものを作るのでソースのみ提示します。 app.html.eex
に投稿一覧表示用メニューを付けます。
...
<body>
<div class="container">
<header class="header">
<ol class="breadcrumb text-right">
<!-- assignsで突っ込んだものが使えている -->
<%= if @current_user do %>
<li><%= @current_user.username %></li>
<li><%= link "My Videos", to: video_path(@conn, :index) %></li>
<li>
...
watch_controller.ex
を作成します。
defmodule Rumbl.WatchController do
use Rumbl.Web, :controller
alias Rumbl.Video
def show(conn, %{"id" => id}) do
video = Repo.get!(Video, id)
render conn, "show.html", video: video
end
end
wathc/show.html.eex
を作成します。コメント入力欄がある唯のページです。
<h2><%= @video.title %></h2>
<div class="row">
<div class="col-sm-7">
<%= content_tag :div, id: "video",
data: [id: @video.id, player_id: player_id(@video)] do %>
<% end %>
</div>
<div class="col-sm-5">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Annotations</h3>
</div>
<div id="msg-container" class="panel-body annotations">
</div>
<div class="panel-footer">
<textarea id="msg-input"
rows="3"
class="form-control"
placeholder="Comment...">
</textarea>
<button id="msg-submit" class="btn btn-primary form-control"
type="submit">
Post
</button>
</div>
</div>
</div>
</div>
上記テンプレート内で player_id/1
という関数を使っているので watch_view.ex
を実装します。
defmodule Rumbl.WatchView do
use Rumbl.Web, :view
def player_id(video) do
~r{^.*(?:youtu\.be/|\w+/|v=)(?<id>[^#&?]*)}
|> Regex.named_captures(video.url)
|> get_in(["id"])
end
end
正規表現を使って投稿されたYouTubeのURLに対してパラメータ部分のみを取り出しています。 router.ex
に /
スコープに get "/watch/:id", WatchController, :show
を追加しておきます。
最後に、ビデオ一覧画面にウォッチ画面へのリンクボタンを作成します。 video/index.html.eex
に以下を追加します。
...
<tbody>
<%= for video <- @videos do %>
<tr>
<td><%= video.user_id %></td>
<td><%= video.url %></td>
<td><%= video.title %></td>
<td><%= video.description %></td>
<td class="text-right">
<%= link "Watch", to: watch_path(@conn, :show, video), class: "btn btn-default btn-xs" %>
...
これで準備は完了です。次からJavaScript側のコードを作成します。
視聴用ページ作成
最初にPhoenixでのJavaScriptのビルド周りについて触れられています。
ビルドツールは
Brunch
がデフォルトBrunch
の設定はデフォルトでES6になっているBrunch
を使わないように変えることも可能。プロジェクト作成時に--no-Brunch
オプションを与えると最初から除ける。web/static/js
以下にあるファイルをすべてapp.js
にまとめるstaticファイルの読み込みは
static_path(@conn, "/js/app.js")
で行うモジュールシステムを利用しないライブラリは
web/static/vendor
に追加する公式ドキュメントによると
bower
で入れたものはこっちに配備されるっぽい?
というわけでJavaScript周りを実装します。 static/js/player.js
を以下の通り実装します。
let Player = {
player: null,
init(domId, player, onReadby) {
window.onYouTubeIframeAPIReady = () => {
this.onIframeReady(domId, player, onReadby);
};
let youtubeScriptTag = document.createElement("script");
// APIの読み込み APIが読み込まれるとonYouTubeIframeAPIReady関数が自動で呼ばれる
youtubeScriptTag.src = "//www.youtube.com/iframe_api";
document.head.appendChild(youtubeScriptTag);
},
onIframeReady(domId, playerId, onReady) {
this.player = new YT.Player(domId, {
height: "360",
width: "420",
videoId: playerId,
events: {
"onReady": (event => onReady(event)),
"onStateChange": (event => this.onPlayerStateChange(event))
}
});
},
onPlayerStateChange(event) {},
getCurrentTime() { return Math.floor(this.player.getCurrentTime() * 1000); },
seekTo(millsec) { return this.player.seekTo(millsec / 1000); }
};
export default Player;
YouTubeのAPIを読み込んでいます。本筋から外れてしまうので割愛します。文法がES2015形式なので昔のJavaScriptとはちょっと変わっています。
ソースを作っただけでは読み込んでくれないので static/js/app.js
を以下のように変更します。
...
import Player from "./player";
let video = document.getElementById("video");
if(video) {
Player.init(video.id, video.getAttribute("data-player-id"), () => {
console.log("player ready!");
});
}
import
文もES2015の文法だったと記憶してます。これも特に言うことはありません。
こんな感じで実装して実行したあと、 priv/static/js/app.js
を見に行くとソースがまとめられていることがわかります。 抜粋して載せてみます。
var Player = {
player: null,
init: function init(domId, plyerId, onReadby) {
var _this = this;
window.onYouTubeIframeAPIReady = function () {
_this.onIframeReady(domId, playerId, onReadby);
};
var youtubeScriptTag = document.createElement("script");
// APIの読み込み APIが読み込まれるとonYouTubeIframeAPIReady関数が自動で呼ばれる
youtubeScriptTag.src = "//www.youtube.com/iframe_api";
document.head.appendChild(youtubeScriptTag);
},
onIframeReady: function onIframeReady(domId, playerId, _onReady) {
var _this2 = this;
this.player = new YT.Player(domId, {
height: "360",
width: "420",
videoId: playerId,
events: {
"onReady": function onReady(event) {
return _onReady(event);
},
"onStateChange": function onStateChange(event) {
return _this2.onPlayerStateChange(event);
}
}
});
},
onPlayerStateChange: function onPlayerStateChange(event) {},
getCurrentTime: function getCurrentTime() {
return Math.floor(this.player.getCurrentTime() * 1000);
},
seekTo: function seekTo(millsec) {
return this.player.seekTo(millsec / 1000);
}
};
exports.default = Player;
});
スラッグの追加
各ビデオを任意のURLでアクセス出来るように Slug
を付けます。
mix ecto.gen.migration add_slug_to_video
を実行後以下のようにマイグレーションファイルを変更します。
defmodule Rumbl.Repo.Migrations.AddSlugToVideo do
use Ecto.Migration
def change do
alter table(:videos) do
add :slug, :string
end
end
end
出来たらマイグレーションを実行後、 video.ex
で新たに追加された slug
カラムを使うようにします。
defmodule Rumbl.Video do
use Rumbl.Web, :model
schema "videos" do
field :url, :string
field :title, :string
field :description, :string
field :slug, :string # 追加
belongs_to :user, Rumbl.User
belongs_to :category, Rumbl.Category
timestamps()
end
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:url, :title, :description, :category_id])
|> validate_required([:url, :title, :description])
|> slugify_title() # タイトルをSlugに変換
|> assoc_constraint(:category)
end
defp slugify_title(changeset) do
# タイトルからSlugを作成する
# changesetを弄るだけで変更予定データの追加などが出来ている
if title = get_change(changeset, :title) do
put_change(changeset, :slug, slugify(title))
else
changeset
end
end
defp slugify(str) do
str
|> String.downcase()
|> String.replace(~r/[^\w-]+/u, "-")
end
end
get_change
や put_change
などを使うことで、変更が changeset
の中だけで収まってくれています。
まとめ
JavaScript
のビルドツールのデフォルトはBrunch
JavaScript
の書式はデフォルトでES2015(ES6)形式static系統のファイルは
web/static/*
に色々おいていくとよしなにしてくれる
全体的にクライアントサイドって感じでした。