【超入門編】Ember.js を、チュートリアルを元に作ってみた

投稿日時:2014年8月27日 カテゴリー:JavaScript

Ember.js はVMCフレームワーク を使った JavaScript です。
今回は、VMCフレームワーク は置いておいて、本家サイトのチュートリアルの説明を元に、Ember.js で一つのサイト(アプリケーション)を作りたいと思います。

完成品

このブログの目的

チュートリアル(YouTube)を見ながら、とりあえず作ってみようが目的です。
チュートリアル(YouTube)を補完するためにこのブログを使っていただければと思います。
VMC のロジックや細かい機能などは他の有志が解説してくれるはず。

チュートリアル(YouTube)

以下の Youtube を見ながら作っていきます。

YouTube

目次

VMC とは?

以下を役割を一緒のソースに記載すると煩雑になるため別々のソースに記載しようということです。

V(ビュー)
表示方法です。
M(モデル)
データと連携します。メイン処理も担当。
C(コントローラー)
ユーザのアクションを元にモデルとかビューとかの制御を行います。

ロジックから入りたい方に非常に参考になるサイト

Ember.jsが他のJavaScript MVCフレームワークに比べて優れてるっぽいポイント
シリーズEmber.js入門
PHP だけど、VMC フレームワークのエンジンをわかりかすく作成するサイト

ダウンロード 初期設定(00:35~)

ダウンロード

以下のサイトからダウンロードします。

Ember.js 本家サイト

初期設定

Chrome ウェブストアで「ember inspector」をインストールする。
これは、Ember.js でサイトをデバッグするのに使います。
とりあえず作るだけなら特に、インストールする必要はありません。

chrom store ember inspector

チュートリアルではテキストエディタに「Sublime Text」を使っています。
普通のテキストでも作れますけど、チュートリアルと一緒に作っていく場合はわかりやすいです。

Sublime Text

さらに以下の CSS と JavaScript を追加します。

index.html

<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.no-icons.min.css" rel="stylesheet"> 

bootstrap の css を使用しています。しかし、ずいぶんと古い。

index.html

<script src="//cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.1.0/moment.min.js "></script>

これで、初期設定は完了しました。

グローバルナビゲーションの作成(1:45~)

グローバルナビゲーションのテンプレート追加

チュートリアルの一番最初は、グローバルナビゲーションの作成です。

まずは、「app.js」の中身を「App = Ember.Application.create()」を残して全て消します。

Sublime Text app.js の中身の削除

HTML body 部分の データを一部を消して以下のコードを追加します。

index.html

<script type="text/x-handlebars">
     <div class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="#">Blogger</a>
        <ul class="nav">
          <li><a href="#">Posts</a></li>
          <li><a href="#">About</a></li>
        </ul>
      </div>
    </div>
</script>

これは、グローバルナビゲーションの HTML コードですね。

Sublime Text nav設定

ではブラウザで表示してみます。

Ember.js グローバルナビの表示

Script タグに囲まれているのに、表示はできました。

ちなみにテンプレートは表示部分で「<script type=”text/x-handlebars”></script>」で作成できるようです。

サブテンプレートの作成

次に、以下の関数を「app.js」 に記載します。

app.js

App.Router.map(function(){
	this.resource('about');
});

これはルート(今回はグローバルナビ)に about テンプレートの URL を設定しています。

App.Router.map のリファレンス

さらにチュートリアルでは、かなりの量の HTML コードを追加していましたが、全てをトレースするのはコードが冗長化するので独自につくったものを追加します。

index.html

<script type="text/x-handlebars" id="about">
    <p>私は日本出身のです。</p>
    <p>好きなものは肉です。</p>
</script>

id に about をつけるのを忘れずに。

なお、GitHub に完成品がありますので全てを一緒にしないと気が済まない方はそちらを参照してください。

GitHub の完成品

さて、チュートリアル(4:25)では、上記コードを追加してもブラウザ上での表示に特に変化はありません。

HTML の グローバルナビコードに一部改変/追加します。

index.html

<script type="text/x-handlebars">
     <div class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="#">Blogger</a>
        <ul class="nav">
          <li><a href="#">Posts</a></li>
          <li><a>About</a></li>
        </ul>
      </div>
    </div>
    
    {{outlet}}
 </script>

ヘルパー{{outlet}} はサブのテンプレート(今回の場合は about)の表示位置を決めます。
試しに、{{outlet}} を<script type=”text/x-handlebars”>内の色々な位置に移動させて見てください。
表示場所が変わります。

さらに、以下の URL を打ち込むと。

http://「URL」/index.html#/about

Ember.js の about の表示

おお、出ました。
App.Router.map で設定したテンプレートはURL “index.html#/[テンプレート名]” で出力できるようです。

チュートリアルでは、さらに posts の設定をしていますが、about と同じです。

App.Router.map(function() {
  this.resource('about');
  this.resource('posts');  
});

グローバルナビゲーションにサブテンプレートの URL を設定

index.html

<ul class="nav">
  <li>{{#link-to 'posts'}}Posts{{/link-to}}</li>
  <li>{{#link-to 'about'}}About{{/link-to}}</li>
</ul>

これで、グローバルナビゲーションにリンクできました。

さらに、グローバルナビゲーションのカレントのaタグには class に active がつくようです。
CSS でスタイルが変更できます。

カレントページ css class active

Blog形式テンプレートの作成(8:50~)

HTML で抜粋テンプレート(posts)を設定し、そのテンプレートから詳細へテンプレート(post)のリンクを設定します。

よくブログであるパターンです。

出力する情報を app.js に設定

チュートリアルのコードは長いので、今回は私が独自に作ったテンプレートを使用します。
app.js に以下のテンプレートに出力するデータを設定します。

app.js

var posts = [{
  id: '1',
  title: "テスト1",
  author: { name: "d2h" },
  date: new Date('12-27-2012'),
  excerpt: "テスト1です。",
  body: テスト1の投稿です。長いので色々とはしょります。"
}, {
  id: '2',
  title: "テスト2",
  author: { name: "d2h" },
  date: new Date('11-26-1983'),
  excerpt: "テスト2です。",
  body: 私の誕生日です。"
}];

配列に 2つのオブジェクトが格納されています。JSON 形式です。

一覧形式で表示させる

まずは、データ一覧を設定します。

app.js

App.PostsRoute = Ember.Route.extend({
  model: function() {
    return posts;
  }
});

posts は先ほど作成した JSON データの変数名です。
PostsRoute が何をしているのかは大体わかりますが、うまく説明できません。詳細は リファレンスを参照してください。

PostsRoute のリファレンス

HTML にテンプレートを追加します。

index.html

<script type="text/x-handlebars" id="posts">
  <div class="span3">
    <h2>テンプレート</h2>
    {{#each model}}
      <h3>{{title}}</h3>
     <p>by {{author.name}}</p>

     {{/each}}
  </div>
    <div class="span9">
    {{outlet}}
    </div>
</script>

{{title}} {{author.name}} は先ほど 変数posts に格納したオブジェクトの中身です。
{{#each}} ヘルパーはオブジェクトをリストにして列挙する場合に使用します。

{{#each}} のリファレンス

これで以下のような抜粋ページが表示できました。

postsの表示

一覧形式からリンクをたどり詳細記事として表示させる

まずは、psot(記事詳細) を追加します。さらに第2引数にオブジェクト path を追加します。

app.js

App.Router.map(function() {
  this.resource('about');
  this.resource('posts');  
  this.resource('post', { path: ':post_id' });  
});

HTML に post テンプレートを作成します。以下は私が独自に作成した post です。

index.html

<script type="text/x-handlebars" id="post">

    <h1>{{title}}</h1>
    <h2>by {{author.name}} <small class='muted'>({{date}})</small></h2>

    <hr>

    <div class='intro'>
      {{excerpt}}
    </div>

    <div class='below-the-fold'>
      {{body}}
    </div>
</script>

posts テンプレートのタイトルにpost へのリンクを追加します。

index.html

<h3>
   {{#link-to 'post' this}}
   {{title}}
   {{/link-to}}
</h3>

これで、ブログ一覧ページから詳細ページへ移動できるようになりました。

抜粋から詳細へのリンク

app.js で posts に psot を入れる。

app.js

this.resource('posts', function(){

  this.resource('post', { path: ':post_id' });  
   	
});  

抜粋(posts)を固定して詳細(post)を表示できるようになりました。
また、URL は post 配下にしっかりと変更されています。

post を同一ページで表示

ちなみに、詳細(post)の表示位置は、テンプレート post の以下の部分です。

<div class="span9">
    {{outlet}}
</div>

これは、入れ子にしたことにより、posts が post のルートになったと考えられます。

ページの編集機能(15:40~)

HTML のテンプレート psot の一番上に以下を追加します。

index.html

{{#if isEditing}}
      {{partial 'post/edit'}}
      <button {{action 'doneEditing'}}>Done</button>
{{else}}
      <button {{action 'edit'}}>Edit</button>
 {{/if}}

さらに、app.js に以下のコードを追加します。

app.js

App.PostController = Ember.ObjectController.extend({
  isEditing: false,
  
  actions: {
    edit: function() {
      this.set('isEditing', true);
    },

    doneEditing: function() {
      this.set('isEditing', false);
    }
  }
});

if が指定されているので条件分岐です。

「Done」 が押させると、「doneEditing」 が実行され 「オブジェクト:isEditing」 に 「false」 が格納されます。
「Edit」 が押されると、「edit」 が実行され、「オブジェクト:isEditing に ture」 が格納されます。

つまり、「Doneボタン」 が押されると 「Editボタン」が表示され、「Editボタン」が押されると「Doneボタン」が表示されるというわけです。

さらに、ヘルパーさん{{partial ‘post/edit’}}です。
これは、別のテンプレートを指定位置にレンタリングせるものです。

{{partial}} のリファレンス

今回レンタルされるテンプレートは以下です。
HTML に追加しましょう。

index.html

<script type="text/x-handlebars" id="post/_edit">
    <p>{{input type="text" value=title}}</p>
    <p>{{input type="text" value=excerpt}}</p>
    <p>{{textarea value=body}}</p>
</script>

さらにヘルパー{{input}} は データをバインドしてくれるようです。
要は、値を変更した場合は、関連した全ての値を変更します。
これは、整合性がとれるなかなか強力な機能です。

{{input}}のリファレンス

このままでは、post の URL をリフレッシュ(F5)すると適切にURL をルーティングしていないためクラッシュします。

app.js に以下のコードを追加します。

app.js

App.PostRoute = Ember.Route.extend({
  model: function(params) {
    return posts.findBy('id', params.post_id);
  }
});

変数 var posts オブジェクトに設定した id と Router.map の post に設定した post_id が一致するデータを返します。
これで psot ページでリフレッシュした場合、post のデータを返すのでクラッシュしまっせん。

フォーマット機能(20:00~)

所定の形式で出力する機能を提供することができます。

app.js に以下を設定。

app.js

Ember.Handlebars.helper('format-date', function(date) {
  return moment(date).fromNow();
});

「moment(date).fromNow()」、moment.min.js の機能で、fromNow() は現在から何日前との日付情報を設定してくれます。

HTML の post テンプレート date(時間) を以下に変更します。

index.html

<h2>by {{author.name}} <small class='muted'>({{format-date date}})</small></h2>

日付のテンプレート

日付の出力形式が 2yers ago に変更されました。

チュートリアルでは、showdown.min.js を使ったテンプレートも作成していますが、今回は端折ります。

チュートリアルを22:35 を見ればわかりますが、## や “” などでHTML をマークダウンできる機能のようです。(詳しくは調べていません。)

JSON を使う(23:30~)

チュートリアルでは、最後に JSON 形式で受け取ったデータを出力する方法を解説しています。
ではさっそくWEB 上のサービスで JSON でデータを提供しているサイトからいただいて、、、
ごめんなさい。今回は割愛します。

ソースコード全文

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Ember Starter Kit</title>
  <link rel="stylesheet" href="css/normalize.css">
  <link rel="stylesheet" href="css/style.css">
  <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.no-icons.min.css" rel="stylesheet">
</head>
<body>

  <script type="text/x-handlebars"><!-- ルートテンプレート グローバルナビゲーション -->
     <div class="navbar">
      <div class="navbar-inner">
        <a class="brand" href="#">Blogger</a>
        <ul class="nav">
          <li>{{#link-to 'posts'}}Posts{{/link-to}}</li><!-- Posts テンプレートへのリンク -->
          <li>{{#link-to 'about'}}About{{/link-to}}</li><!-- About テンプレートへのリンク -->
        </ul>
      </div>
    </div>
    
    {{outlet}}<!-- サブテンプレート(about posts)の出力場所 -->
  </script>

  <script type="text/x-handlebars" id="about"><!-- about テンプレート -->
    <p>私は日本出身のです。</p>
    <p>好きなものは肉です。</p>
  </script>

  <script type="text/x-handlebars" id="posts"><!-- posts テンプレート -->
  <div class="span3">
    <h2>テンプレート</h2>
    {{#each model}}<!-- app.js に設定した posts(データ) から一覧を列挙 -->
      <h3>{{#link-to 'post' this}}<!-- post テンプレートへのリンク this は自分自身 -->
          {{title}}
          {{/link-to}}
    </h3>
     <p>by {{author.name}}</p>

     {{/each}}
  </div>
    <div class="span9">
    {{outlet}}<!-- post テンプレートの出力場所 app.js で入れ子にしたことで出力-->
    </div>
  </script>

  <script type="text/x-handlebars" id="post"><!-- post テンプレート-->
    {{#if isEditing}}
      {{partial 'post/edit'}}<!-- post/_edit テンプレートをレンタリング-->
      <button {{action 'doneEditing'}}>Done</button>
    {{else}}
      <button {{action 'edit'}}>Edit</button>
    {{/if}}

    <h1>{{title}}</h1>
    <h2>by {{author.name}} <small class='muted'>({{format-date date}})</small></h2><!-- format-date にてデータのフォーマットを設定-->

    <hr>

    <div class='intro'>
      {{format-markdown excerpt}}
    </div>

    <div class='below-the-fold'>
      {{format-markdown body}}
    </div>
  </script>

  <script type="text/x-handlebars" id="post/_edit"><!-- post/_edit テンプレート-->
    <p>{{input type="text" value=title}}</p>
    <p>{{input type="text" value=excerpt}}</p>
    <p>{{textarea value=body}}</p>
  </script>


  <script src="js/libs/jquery-1.10.2.js"></script>
  <script src="js/libs/handlebars-1.1.2.js"></script>
  <script src="js/libs/ember-1.7.0.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.1.0/moment.min.js "></script>
  <script src="js/app.js"></script>
  <script src="tests/runner.js"></script>
</body>
</html>

app.js

App = Ember.Application.create();

//ルーティングの設定
App.Router.map(function() {
  this.resource('about');
  this.resource('posts', function(){

  //入れ子にすることで、 posts テンプレートの階層構造を設定
  this.resource('post', { path: ':post_id' });  
   	
  });  
 
});

//変数 posts(データ)を受け渡す。
App.PostsRoute = Ember.Route.extend({
  model: function() {
    return posts;
  }
});

//変数 posts(データ)のid と post の post_id が一致するモデルを受け渡す。
App.PostRoute = Ember.Route.extend({
  model: function(params) {
    return posts.findBy('id', params.post_id);
  }
});

//post に設定された if 文の分岐を設定
App.PostController = Ember.ObjectController.extend({
  isEditing: false,
  
  actions: {
    edit: function() {
      this.set('isEditing', true);
    },

    doneEditing: function() {
      this.set('isEditing', false);
    }
  }
});

//フォーマット形式の設定
var showdown = new Showdown.converter();

Ember.Handlebars.helper('format-markdown', function(input) {
  return new Handlebars.SafeString(showdown.makeHtml(input));
});

Ember.Handlebars.helper('format-date', function(date) {
  return moment(date).fromNow();
});
//データ JSON 形式
var posts = [{
  id: '1',
  title: "テスト1",
  author: { name: "d2h" },
  date: new Date('12-27-2012'),
  excerpt: "テスト1です。",
  body: "テスト1の投稿です。長いので色々とはしょります。"
}, {
  id: '2',
  title: "テスト2",
  author: { name: "d2h" },
  date: new Date('11-26-1983'),
  excerpt: "テスト2です。",
  body: "私の誕生日です。\n\n### 誕生日\n\n 11/26 絶対覚えてください。"
}];

最後に

とりあえず、作ってみようで作りました。
私の理解力では追いつかずに、いろいろと説明が違う箇所がある可能性があります。
ここが違っているというコメント大歓迎です。


コメントを残す