CanJSで初めて作る!GitHub連携のリアルタイムなToDoアプリ

2017/08/14

Chasen Le Hara

41

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

動きの早いJavaScriptフレームワークの世界で、長い歴史と実績を持つCanJSを知っていますか? その魅力を解説しながら、簡単なWebアプリを作成するチュートリアルをお届けします。夏休みの課題にどうぞ。

CanJSは、長期にわたってメンテナブルなWebアプリの作成に役立つ、革新的なフロントエンドライブラリーです。何十個もの独立したパッケージがあり、必要なライブラリーを選択できるため、100kb以上の依存オブジェクトに苦労することもなくなります。

CanJSはMVVM(モデル・ビュー・ビューモデル)構造です。以下の主要パッケージで成り立ちます。

このチュートリアルではGitHubリポジトリのissueリストをソースに、ToDoリストアプリを作ります。GitHubのWebhook APIでリアルタイムに更新され、jQuery UIのSortable(並べ替え)によって順番が変更できます。

完成したアプリはGitHubにあります。

Gif of adding issues and sorting them in our example app

CanJSのMVVM

チュートリアルのプロジェクトを開始する前に、CanJSにおけるMVVMを詳しく解説します。

データモデル

MVVMにおける「モデル」はデータモデルで、アプリ内のデータを指します。今回のアプリで扱う個別のissueissueのリストがモデルのデータ型に当たります。

CanJSではcan-define/list/listcan-define/map/mapにより、配列とオブジェクトを定義します。観察対象(observable)のデータで、変更があればMVVMのViewもしくはViewModelを自動で更新します。

一例ですが、今回のアプリは以下の型があります。

import DefineMap from 'can-define/map/map';
const Issue = DefineMap.extend('Issue', {
  id: 'number',
  title: 'string',
  sort_position: 'number',
  body: 'string'
});

Issueのインスタンスはid、title、sort_position、body4つのプロパティを持ちます。値を代入したら、値がnullやundefinedでない限り、can-define/map/mapが指定した型に変換します。たとえばidを文字列型の"1"にしても、idプロパティには数値の1が入ります。ただし値がnullならnullのままです。

issueの配列の型を定義します。

import DefineList from 'can-define/list/list';
Issue.List = DefineList.extend('IssueList', {
  '#': Issue
});

can-define/list/list#プロパティは、リスト中の項目すべて指定した型に変換できます。この記述によりIssue.ListのオブジェクトもIssue型のインスタンスになります。

Viewテンプレート

Webアプリの「ビュー」とは、訪問者が閲覧、操作するHTMLのユーザーインターフェイスです。CanJSのHTML描画では、MustacheHandlebarsによく似た構文のcan-stacheを含む、複数の形式のテンプレートが使えます。

can-stacheテンプレートの例です。

<ol>
  {{#each issues}}
    <li>
      {{title}}
    </li>
  {{/each}}
</ol>

いくつものissuesの繰り返しで{{#each}}を使い、各issueの題名titleを{{title}}で示しています。issuesリストや題名の変更でも、DOMを更新します(例:新しいissueがリストに追加されたら、DOMにliが追加されます)。

ビューモデル

MVVMのビューモデルは、モデルとビューの橋渡しをするコードです。ビューに必要でモデルには含まれないロジックは、ビューモデルが実現します。

CanJSでは、can-stacheテンプレートはViewModelで描画されます。単純化した例です。

import stache from 'can-stache';
const renderer = stache('{{greeting}} world');
const viewModel = {greeting: 'Hello'};
const fragment = renderer(viewModel);
console.log(fragment.textContent);// Logs “Hello world”

コンポーネント

これらを1つにまとめた概念がコンポーネント(もしくはカスタム要素)です。コンポーネントは、機能をひとまとまりにしたり、アプリ全体にわたって再利用可能にしたりする場合に便利です。

CanJSのcan-componentは、ビュー(can-stacheファイル)、ビューモデル(can-define/map/map)、オプションでJavaScriptのイベントを監視するオブジェクトで構成されています。

import Component from 'can-component';
import DefineMap from 'can-define/map/map';
import stache from 'can-stache';

const HelloWorldViewModel = DefineMap.extend('HelloWorldVM', {
  greeting: {value: 'Hello'},
  showExclamation: {value: true}
});

Component.extend({
  tag: 'hello-world',
  view: stache('{{greeting}} world{{#if showExclamation}}!{{/if}}'),
  ViewModel: HelloWorldViewModel,
  events: {
    '{element} click': () => {
      this.viewModel.showExclamation = !this.viewModel.showExclamation;
    }
  }
});

const template = stache('hello-world');
document.body.appendChild(template);

上記のテンプレートでは、カスタム要素をユーザーがクリックしたのか判別して「Hello world!」もしくは「Hello warld(!記号なし)」を表示します。

CanJSアプリの作成は以上4つの概念を押さえればよいのです。サンプルアプリでも、MVVM構造のアプリ制作のため4つの概念を使用します。

本チュートリアルの前提条件

開始する前に、最新のNode.jsをインストールしてください。GitHubのAPIと通信するバックエンドのサーバーの設置にはnpmを使用します。

GitHubアカウントが無ければ登録してください。

ローカルプロジェクトの作成

プロジェクトの新規フォルダーを作り、そのフォルダーに移動します。

mkdir canjs-github
cd canjs-github

プロジェクトに必要なファイルを新規作成します。

touch app.css app.js index.html

スタイル設定にapp.cssを、JavaScriptの記述にapp.jsを、ユーザーインターフェイスにindex.htmlを使います。

CanJSのはじめの一歩

コーディング開始です。以下をindex.htmlファイルに加えます。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>CanJS GitHub Issues To-Do List</title>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <link rel="stylesheet" href="app.css">
</head>
<body>

<script type="text/stache" id="app-template">
  <div class="container">
    <div class="row">
      <div class="col-md-8 col-md-offset-2">
        <h1 class="page-header text-center">
          {{pageTitle}}
        </h1>
      </div>
    </div>
  </div>
</script>

<script type="text/stache" id="github-issues-template">
</script>

<script src="https://unpkg.com/jquery@3/dist/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="app.js"></script>
</body>
</html>

たくさんのパーツがあるので、1つずつ解説します。

  • csshead内の2つのlink要素はプロジェクトのスタイルシートを参照する。基本としてBootstrapを使いつつ、app.css内でいくつか独自のカスタムを加える
  • 最初のscript要素(id="app-template")はアプリのルートテンプレートを参照する
  • 2番目のscript要素(id="github-issues-template")は、あとで作成するgithub-issuesコンポーネントのテンプレートを参照する
  • ページの最後のscript要素は、依存オブジェクトを読み込むためのもの。:jQuery、jQuery UI、CanJS、Socket.io、このアプリのコード

ドラッグ&ドロップでissueを並べ替えるため、jQuery UIjQueryに依存)を使用します。can.all.jsを読み込んでいるためすべてのCanJSモジュールにアクセスが可能です。通常はStealJSのようなモジュールローダーもしくはwebpackを使用しますが、この記事の範疇ではないので割愛します。ここではGitHubのイベントを受け取ってアプリを逐次更新するためにSocket.ioを使用します。

app.cssファイルにいくつかスタイルを追記します。

form {
  margin: 1em 0 2em 0;
}

.list-group .drag-background {
  background-color: #dff0d8;
}

.text-overflow {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}


app.js
ファイルに以下のコードを加えます。

var AppViewModel = can.DefineMap.extend('AppVM', {
  pageTitle: {
    type: "string",
    value: "GitHub Issues",
  }
});

var appVM = new AppViewModel();
var template = can.stache.from('app-template');
var appFragment = template(appVM);
document.body.appendChild(appFragment);

JavaScriptを分解して解説します。

  • can.DefineMap:監視対象オブジェクト型のカスタムに使用
  • AppViewModel:本アプリのルートビューモデルになる、観察対象オブジェクト
  • pageTitle:すべてのAppViewModelfインスタンスのもつプロパティで、GitHub Issuesの初期値(ページタイトル)
  • appVM:このアプリのビューモデルの新規インスタンス
  • can.stache.from:scriptタグの中身を、テンプレート描画のための関数と入れ替える
  • appFragment:appVMデータで描画されたテンプレートのドキュメント・フラグメント
  • document.body.appendChild:DOMのノードを取得しHTMLのbodyに追加する

注:このページのcan.all.jsスクリプトの読み込みにより、どのCanJSモジュールにもアクセスできるcanのグローバル変数が使えます。たとえばcan-stacheモジュールはcan.stacheとして、スクリプト内で使用できます。

ブラウザーでindex.htmlを開きます。

Screenshot of our example app with an error loading Socket.IO

コンソールにエラーが1つ表示されるのは、リアルタイムSocket.ioサーバーを設定していないためです。次節で作業します。

サーバー側の作業

GitHubのWebhooks APIは、リポジトリに変更があった際にサーバーに通知を送信します。サーバー側のコードに時間をかけないため、github-issue-server npmモジュールを用意しました。役割は以下の6つです。

  • GitHub Webhookイベントを受け取るためにngrokサーバーをセットアップ
  • アプリのUIが新規issueを作成すると、認証済みリクエストをGitHub APIに送信
  • アプリUIのリアルタイム通信にはSocket.ioを使用
  • プロジェクトのフォルダーにファイルを作成
  • 各issueにsort_position(並び順)プロパティを付与
  • ローカルのissues.jsonファイルで、issueのリストとsort_position(並び順)を保持

サーバーが認証済みのリクエストでGitHubと通信するためアクセストークンを作成します

  1. github.com/settings/tokens/newへ移動
  2. Token descriptionを入力(私の場合「CanJS GitHub Issue To-Do List」)
  3. public_repoのスコープ(範囲)を選択
  4. Generate tokenをクリック
  5. 次のページで、トークンの次にあるクリップボード型アイコンCopy Tokenをクリック

これでサーバーをインストールできます。ここではpackage.jsonの作成github-issue-serverのインストールのためにnpmを使用します。

npm init -y
npm install github-issue-server

サーバーを起動するため以下のコマンドを実行します。ACCESS_TOKENの部分は先程GitHubからコピーしたアクセストークンを入れて下さい。

node node_modules/github-issue-server/ ACCESS_TOKEN

サーバーが立ち上がり、以下を表示します。

Started up server, available at:
  http://localhost:8080/
Started up ngrok server, webhook available at:
  https://829s1522.ngrok.io/api/webhook

ngrokサーバーアドレスは上記と異なった一意のサブドメインになります。

localhostもしくはngrok.ioのアドレスをブラウザーで開くと、以前と同じホームページが表示されますが、今回はコンソールにエラーは表示されません。

Screenshot of our example app with no errors in the DevTools console

GitHub Isssuesコンポ―ネントの作成

CanJSのコンポーネントとは、ビュー(can-stacheテンプレート)とビューモデル(データをビューと結びつける役割)を持ったカスタム要素です。コンポーネントは、一連の機能を1つにまとめてアプリ内での再利用が可能になるので重宝します。

すべてのGitHub issueをリスト化したり新規作成したりするためのコンポーネント、github-issuesを作ります。

app.jsファイルの先頭に以下を加えます。

var GitHubIssuesVM = can.DefineMap.extend('GitHubIssuesVM', {
  pageTitle: 'string'
});

can.Component.extend({
  tag: 'github-issues',
  view: can.stache.from('github-issues-template'),
  ViewModel: GitHubIssuesVM
});

GitHubIssuesVMはコンポーネントのビューモデルにあたります。コンポーネントの各インスタンスはpageTitle(タイトル)プロパティを持っていてHTMLのビューに反映されます。

github-issues要素のテンプレートを定義します。

<script type="text/stache" id="github-issues-template">
  <h1 class="page-header text-center">
    {{pageTitle}}
  </h1>
</script>

{{pageTitle}}が、テンプレートのビューモデルのpageTitleを反映します。

HTMLのヘッダーを変更します。

<h1 class="page-header text-center">
  {{pageTitle}}
</h1>

新しいカスタム要素を使います。

<github-issues {page-title}="pageTitle" />

上記のコードで、アプリのビューモデルからgithub-issuesコンポーネントへ、pageTitleプロパティを渡しました。{page-title}文は、親テンプレートから子テンプレートへの一方向のバインドです。つまり親の変更は子に反映されますが、子の変更は親には影響しません。CanJSでは一方向・双方向の両方のデータバインディングに対応しています。後ほど双方向の例を紹介します。

ページは以前とまったく同じですが、HTMLの構造は変わります。

Screenshot of the DOM with the github-issues custom element

GitHubリポジトリの作成

GitHubリポジトリ(repo)のissueからTo-Doリストを生成するので、GitHubリポジトリの設定が必要です。

リポジトリが無ければ作成してください

リポジトリができたら、Settingsを開いてWebhooksをクリックし、Add webhook(Webhookの追加)をクリックします。認証後、フォームを埋めます。

  • Payload URLの欄にはローカルサーバーのngrokサーバーアドレスを貼る(例:https://829s1522.ngrok.io/api/webhook
  • Content typeは、application/jsonを選択
  • Let me select individual events(個別にイベントを選択)をクリックし、Issuesチェックボックスを選択
  • そのほかいろいろ
  • Add webhookボタンを押して作成を終了する

リポジトリのissuesに変更があれば、ローカルサーバーはそのWebhookイベントを受け取れます。試してみます。

GitHubリポジトリのIssuesタブから新規issueを作成してください。Test issueという名前のissueを作ったら、コマンドラインに以下のメッセージが表示されます。

Received “opened” action from GitHub for issue “Test issue”

Screenshot of the server running on a command-line

GitHub Issueのリスト化

GitHubリポジトリにいくつかissueを作成しました。続いてアプリUIに表示します。

issueデータのモデルとして、観察対象(observable)のIssue型を定義します。app.jsファイルの先頭に以下を加えます。

var Issue = can.DefineMap.extend('Issue', {
  seal: false
}, {
  id: 'number',
  title: 'string',
  sort_position: 'number',
  body: 'string'
});

各Issueのインスタンスはid、title、sort_position、bodyプロパティを持っています。GitHub issueはここで設定したプロパティが複数あるので、sealをfalseに設定してGitHub APIからほかのプロパティが来てもエラーが出ないようにします。

issueの配列用にcan.DefineList型を定義します。

Issue.List = can.DefineList.extend('IssueList', {
  '#': Issue
});

can-connectが2つの特別なプロパティを認識するためにcan-set.Algebraを設定します。特別なプロパティとは、各issueの一意の識別子であるidと、指定した順序でissueを取得するためIssue.getListと一緒に使うsortです。

Issue.algebra = new can.set.Algebra(
  can.set.props.id('id'),
  can.set.props.sort('sort')
);

IssueIssue.Listをサーバーのエンドポイントに接続します。以下のGITHUB_ORG/GITHUB_REPOの部分は、自分のリポジトリの情報に置き換えて記述してください。

Issue.connection = can.connect.superMap({
  url: '/api/github/repos/GITHUB_ORG/GITHUB_REPO/issues',
  Map: Issue,
  List: Issue.List,
  name: 'issue',
  algebra: Issue.algebra
});

アプリがcan.connect.superMapを呼ぶと、IssueオブジェクトはいくつかのCRUDメソッド(データベースの基本機能である新規作成、読み出し、更新、削除の頭文字)が使えるようになります。さらに、指定した型のすべてのインスタンスのリストを取得するメソッドgetListも使えます。

このアプリはサーバーから全issueを取得するためIssue.getListを使います。ここでGitHubIssuesVMを変更してissuesPromiseプロパティを加えます:

var GitHubIssuesVM = can.DefineMap.extend('GitHubIssuesVM', {
  issuesPromise: {
    value: function() {
        return Issue.getList({
          sort: 'sort_position'
        });
    }
  },
  issues: {
    get: function(lastValue, setValue) {
      if (lastValue) {
        return lastValue;
      }
      this.issuesPromise.then(setValue);
    }
  },
  pageTitle: 'string'
});

issuesPromiseプロパティは、Issue.getListから返されるプロミスです。sortプロパティとして指定したsort_positionに従ってリストが整列します。またissuesプロパティは、解決後のプロミスの値になります。

index.htmlファイルのgithub-issues-templateを変更します。

 <div class="list-group">
    {{#if issuesPromise.isPending}}
      <div class="list-group-item list-group-item-info">
        <h4>Loading…</h4>
      </div>
    {{/if}}
    {{#if issuesPromise.isRejected}}
      <div class="list-group-item list-group-item-danger">
        <h4>Error</h4>
        <p>{{issuesPromise.reason}}</p>
      </div>
    {{/if}}
    {{#if issuesPromise.isResolved}}
      {{#if issues.length}}
        <ol class="list-unstyled">
          {{#each issues}}
            <li class="list-group-item">
              <h4 class="list-group-item-heading">
                {{title}} <span class="text-muted">#{{number}}</span>
              </h4>
              <p class="list-group-item-text text-overflow">
                {{body}}
              </p>
            </li>
          {{/each}}
        </ol>
      {{else}}
        <div class="list-group-item list-group-item-info">
            <h4>No issues</h4>
        </div>
      {{/if}}
    {{/if}}
  </div>

can-stacheテンプレートでは条件式として{{#if}}が使えるので、issueのプロミスの状態により3つのブロック、sPending(保留)、isRejected(拒否)、isResolved(解決)に分岐します。isResolvedの場合は{{#each}}でissueの配列を処理します。1つもissueが無ければメッセージを表示します。

これでページを更新しても、同じissueのリストを表示します。

Screenshot of the example app with a list of GitHub issues

GitHub Issueの作成

題名と説明付きのissueを新規作成するフォームを作ります。そのフォームから、GitHubのAPIを使って新規issueを生成します。

index.htmlファイル内のgithub-issues-templateテンプレートのh1タグの下にフォームを作成します。

  <form ($submit)="send()">
    <div class="form-group">
      <label for="title" class="sr-only">Issue title</label>
      <input class="form-control" id="title" placeholder="Issue title" type="text" {($value)}="title" />
    </div>
    <div class="form-group">
      <label for="body" class="sr-only">Issue description</label>
      <textarea class="form-control" id="body" placeholder="Issue description" {($value)}="body"></textarea>
    </div>
    <button class="btn btn-primary" type="submit">Submit issue</button>
  </form>

上記コードでは、まだ紹介していないCanJSの機能を使っています。

  • ($submit):フォームが送信された際にビューモデルのsend()関数を呼ぶDOMイベントリスナー
  • {($value)}="title"{($value)}="body":どちらも値の双方向バインディング。入力欄の値が変更されるとビューモデルも更新され、その逆でも同じく値が更新される

app.jsファイル内のGitHubIssuesVMを変更して、新しいプロパティを3つ追加します。

var GitHubIssuesVM = can.DefineMap.extend('GitHubIssuesVM', {
  issuesPromise: {
    value: function() {
        return Issue.getList({
          sort: 'sort_position'
        });
    }
  },
  issues: {
    get: function(lastValue, setValue) {
      if (lastValue) {
        return lastValue;
      }
      this.issuesPromise.then(setValue);
    }
  },
  pageTitle: 'string',
  title: 'string',
  body: 'string',
  send: function() {
    var firstIssue = (this.issues) ? this.issues[0] : null;
    var sortPosition = (firstIssue) ? (Number.MIN_SAFE_INTEGER + firstIssue.sort_position) / 2 : 0;

    new Issue({
        title: this.title,
        body: this.body,
        sort_position: sortPosition
    }).save().then(function() {
        this.title = this.body = '';
    }.bind(this));
  }
});

新しいissueには、body、titleプロパティに加えて新規issue作成メソッドsend()を実装しています。このメソッドはissuesリストを受け取って新規issuesort_position(位置)を計算します。新規issueは最初のissueの前が望ましいです。新規issueの値がそろったらnew Issue()を実行して新規作成し、.save()メソッドでサーバーに保存し、プロミスが解決されるのを待ちます。それが成功したら、フォームの題名と本文(title、body)を消去して空欄に戻します。

app.jsファイル内のgithub-issuesコンポーネントを更新して新規にeventsオブジェクトを作成します。

can.Component.extend({
  tag: 'github-issues',
  view: can.stache.from('github-issues-template'),
  ViewModel: GitHubIssuesVM,
  events: {
    '{element} form submit': function(element, event) {
      event.preventDefault();
    }
  }
});

フォームの送信イベントを検知するために、can-componenteventsプロパティを使用します。ユーザーのフォームを送信でページを更新しないため、preventDefault()でフォーム送信時に通常のふるまいを取り消します。

issueを追加して、GitHub UIにも表示できました。さらにcan-set.algebraの影響でissueはリストの最後に表示されます。

Gif of adding an issue through our app and having it show up in GitHub

リアルタイム更新機能の追加

新規issueをGitHubに送信できますが、GitHubで加えた変更はアプリに反映されません。そこで、Socket.IOによるリアルタイムの更新機能を追加します。

app.jsファイル内の、Issue.connectionに続いて以下のコードを追記します。

var socket = io();
socket.on('issue created', function(issue) {
  Issue.connection.createInstance(issue);
});
socket.on('issue removed', function(issue) {
  Issue.connection.destroyInstance(issue);
});
socket.on('issue updated', function(issue) {
  Issue.connection.updateInstance(issue);
});

ローカルサーバーは、issueが作成・削除・更新されると3種類のイベントを発生します。イベントを受けてイベントリスナーがcreateInstance(作成)、destroyInstance(削除)、updateInstance(更新)をコールしてIssueデータが変更されます。IssueのインスタンスはもちろんのことIssue.Listも観察対象(observable)であるため、CanJSはIssueモデルを参照するいかなるパーツでも自動更新します。

ページを更新したあと、GitHubのUIを変更すると、アプリのUIも同様に変更します。

Gif of adding an issue on GitHub.com and the issue showing up in our example app

Issueの並び替え

issueの並び替えをドラッグ&ドロップでする機能を加えます。ローカルサーバーでは、issueリストの順序を変更したら、プロジェクトフォルダーのissues.jsonファイルに保存するように設定します。必要なのはアプリにissueを並び替える操作機能を追加することと、issueに対し新しい位置(sort_position)を代入するロジックを書くことです。

前章で加筆したSocket.IOのコードに続いて、下記を追記します。

can.view.callbacks.attr('sortable-issues', function(element) {
  $(element).sortable({
    containment: 'parent',
    handle: '.grab-handle',
    revert: true,
    start: function(event, ui) {
      var draggedElement = ui.item;
      draggedElement.addClass('drag-background');
    },
    stop: function(event, ui) {
      var draggedElement = ui.item;
      draggedElement.removeClass('drag-background');
    },
    update: function(event, ui) {
      var draggedElement = ui.item[0];
      var draggedIssue = can.data.get.call(draggedElement, 'issue');
      var nextSibling = draggedElement.nextElementSibling;
      var previousSibling = draggedElement.previousElementSibling;
      var nextIssue = (nextSibling) ? can.data.get.call(nextSibling, 'issue') : {sort_position: Number.MAX_SAFE_INTEGER};
      var previousIssue = (previousSibling) ? can.data.get.call(previousSibling, 'issue') : {sort_position: Number.MIN_SAFE_INTEGER};
      draggedIssue.sort_position = (nextIssue.sort_position + previousIssue.sort_position) / 2;
      draggedIssue.save();
    }
  });
});

詳しく解説します。

  • can.view.callbacksは、DOMに新しく属性要素を加えた際に呼ばれるコールバックを設定する。このコードの場合、要素にsortable-issues属性を与えたときに関数が呼ばれる
  • DOM要素のドラッグ&ドロップ機能の実装に使用したjQuery UIのsortable interactionのオプションcontainmenthandlerevertの設定をする
  • ユーザーがissueをドラッグしたら、start関数が呼ばれ、DOM要素にクラス(drag-background)を追加する
  • ユーザーがissueをドロップ(配置)したら、stop関数が呼ばれ、start関数で加えたクラスを取り除く
  • 並び替えが完了し、DOMが更新されたらupdate関数が呼ばれる。ドラッグで移動したIssueモデルおよび前後のissueモデルを取得して、間にあるこのissueの位置(sort_position)が計算できる。sort_positionを代入したのちsave()関数をコールし、更新されたissueデータをローカルサーバーに保存する(PUTメソッド)

index.htmlファイル内で、issueの<ol>(順序付きリストタグ)を変更します。

        <ol class="list-unstyled" sortable-issues>
          {{#each issues}}
            <li class="list-group-item" {{data('issue', this)}}>
              {{^is issues.length 1}}
                <span class="glyphicon glyphicon-move grab-handle pull-right text-muted" aria-hidden="true"></span>
              {{/is}}
              <h4 class="list-group-item-heading">
                {{title}} <span class="text-muted">#{{number}}</span>
              </h4>
              <p class="list-group-item-text text-overflow">
                {{body}}
              </p>
            </li>
          {{/each}}
        </ol>

新しく追加したものは以下の通りです。

  • sortable-issues属性はapp.jsファイル内で定義したように、DOM上の属性が加わるとコールバックを呼ぶ
  • {{data('issue',this)}}はDOM要素にissueデータを載せる。sortable-issuesコールバックでデータを取得する
  • {{^is issues.length 1}}は、リスト上に2つ以上のissueがあるときにissueを動かすためのグラブハンドル(取っ手)を追加する

ページを更新すると、各issueにハンドルが現れます。ハンドルをつかんで並び替えができます。

Gif of reordering issues in our example app with drag and drop

さらに詳しく知るために

CanJSを使った、GitHub issueのリアルタイムTo-Doリストが完成しました。さらにCanJSを学びたい意欲が湧いたら、CanJS.comで以下の導入記事をチェックしてください。

疑問があればGitterで質問するか、CanJSのフォーラム私のツイッター、で質問してください。

本記事はCamilo Reyesが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:How to Build a Real-Time GitHub Issue To-Do List with CanJS

[翻訳:西尾 健史/編集:Livit

Copyright © 2017, Chasen Le Hara All Rights Reserved.

Chasen Le Hara

Chasen Le Hara

カリフォルニア州ロングビーチのソフトウェア技術者です。十年以上のWeb開発経験があり、CanJSの貢献者の1人。現在は、Walmartをはじめとする大手企業のWebアプリを開発したことで知られるJavaScriptコンサルティング会社・Bitoviに勤務しています。

Loading...