Tech memo

日々学んだ技術のびぼうろく

Riot.js 3.3 と Lumen 5.4 でつくる 初めてのSPA つなぎこみ編

はじめに

最近 Riot が気になっていたので、
Laravel製軽量フレームワーク Lumen 5.4 と Riot 3.3 で簡単なブログを作ってみることにした。

今回はいよいよフロントエンドとバックエンドをつなぎこむ。 フロントエンドの ajax まわりを実装していく。

他のエントリーはこちら。

www.yjhm214.com

www.yjhm214.com

スポンサーリンク

作るもの

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

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

記事一覧
f:id:yjhm214:20170325170558p:plain

記事詳細
f:id:yjhm214:20170325170608p:plain

記事編集
f:id:yjhm214:20170325170616p:plain

記事保存 f:id:yjhm214:20170325170621p:plain

前提

  • Lumen 5.4.5
  • Riot.js 3.3.1

手順

  1. ナビゲーション画面(ajax でカテゴリー一覧を取得)
  2. 一覧画面(ajax で記事一覧を取得)
  3. 詳細画面(ajax で記事詳細を取得)
  4. 編集画面(ajax で新規作成・更新処理)

1. ナビゲーション

ナビゲーションに表示カテゴリーをマスターデータから取ってくる。
カスタムタグの <script> 部分を変更する。

navbar.tag

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

    // ajax 呼び出しの中では、`this` が Riot のタグインスタンスではなく、ajax のレスポンスオブジェクトを参照してしまうため、`this` を `self` に代入。
    var self = this

    $.ajax({
      type : 'GET',
      url  : window.location.origin + '/api/v1/categories',
      data : {}
    })
    .done(function(res){
      self.categories = res
      // テンプレート変数と子要素を更新
      self.update()
    })
    .fail(function(){
      // bootstrap modal を表示
      $("#connection-error-dialog").modal();
      // bootstrap modal が閉じた時のイベント
      $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
        window.location.href = window.location.origin
      })
    })
    .always(function(){
      console.log('GET categories')
    })

  </script>

ポイント

  • var self = this とすることで Riot のタグインスタンスを ajax 内でも使えるようにする。
  • self.update()で現在のタグインスタンス上のすべてのテンプレート変数を更新する。(参考)
  • $("#connection-error-dialog").modal(); は Bootstrap の modal 関数
  • $('#connection-error-dialog').on('hidden.bs.modal', function (e) {}); も同じく Bootstrap の modalイベント。

2. 一覧画面

list.tag

  <script>
  
    // this.list = [
    //   {
    //     id: 1,
    //     title: 'Hello, world!',
    //     text: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
    //     categories: ['Tech', 'Book'],
    //   },
    //   {
    //     id: 2,
    //     title: 'Hello, world!',
    //     text: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
    //     categories: ['Book', 'Hobby', 'Others']
    //   },
    //   {
    //     id: 3,
    //     title: 'Hello, world!',
    //     text: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
    //     categories: ['Hobby', 'Others']
    //   },
    //   {
    //     id: 4,
    //     title: 'Hello, world!',
    //     text: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
    //     categories: ['Others']
    //   },
    // ];
    // console.log(this.list)


    var self = this

    // opts 変数の中にルータから渡されたオブジェクトが格納されている
    console.log(opts)
    var params = {}
    if (opts.categoryName) params.category = opts.categoryName

    $.ajax({
      type : 'GET',
      url  : window.location.origin + '/api/v1/posts',
      data : params
    })
    .done(function(res){
      self.list = res

      for (var i=0; i<self.list.length; i++) {
        // タイトルが60文字以上だった場合省略
        if (self.list[i].title.length >= 60) {
          self.list[i].title = self.list[i].title.slice(0, 60) + '...'
        }

        // テキストが100文字以上だった場合省略
        if (self.list[i].text.length >= 100) {
          self.list[i].text = self.list[i].text.slice(0, 100) + '...'
        }
        self.list[i].title = self.list[i].title.replace(/(\r\n)|\n|\r/g, "<br />")
        self.list[i].text  = self.list[i].text.replace(/(\r\n)|\n|\r/g, "<br />")
      }

      self.update()

      // ajax 終了前にタグがマウントされてしまうため、
      // タグを再マウントして描画しなおす。
      riot.mount('raw')

    })
    .fail(function(){
      $("#connection-error-dialog").modal();
      $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
        window.location.href = window.location.origin
      })
    })
    .always(function(){
      console.log('GET posts.')
    });
  </script>

ポイント

  • ルータから渡された変数は opts でアクセスできる。ここでは、カテゴリーでソートされたときのカテゴリー名を取得する。
  • テンプレート内にある カスタムタグ<raw>は、ajax 通信が終わる前にマウントされてしまうため、ajax 通信が終わったあとにriot.mount('raw') で再マウントすることで描画し直している。

3. 詳細画面

ここは一覧画面とほぼ同じ。

post.tag

  <script>
  
    // this.id         = 1
    // this.title      = 'Hello, world!'
    // this.text       = 'Some quick example text to build<br/> on the card title and make up the bulk of the cards content.'
    // this.categories = ['Tech', 'Book']

    var self = this

    console.log(opts)
    var id = opts.id

    $.ajax({
      type : 'GET',
      url  : window.location.origin + '/api/v1/posts/' + id,
      data : {}
    })
    .done(function(res){
      self.id         = res.id;
      self.title      = res.title.replace(/(\r\n)|\n|\r/g, "<br />")
      self.text       = res.text.replace(/(\r\n)|\n|\r/g, "<br />")
      self.categories = res.categories

      self.update()

      riot.mount('raw')
    })
    .fail(function(){
      $("#connection-error-dialog").modal();
      $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
        window.location.href = window.location.origin
      })
    })
    .always(function(){
      console.log('GET post.')
    });
  </script>

4. 編集画面

edit.tag

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

    var self  = this
    var id    = opts.id // 'new' か 記事ID
    self.post = {}

    // category の取得
    $.ajax({
      type : 'GET',
      url  : window.location.origin + '/api/v1/categories',
      data : {}
    })
    .done(function(res){
      self.categories = res
      self.update()
    })
    .fail(function(){
      $("#connection-error-dialog").modal();
      $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
        window.location.href = window.location.origin
      })
    })
    .always(function(){
      console.log('GET categories.')
    })

    // 更新の場合、デフォルト値を設定
    if (id !== 'new') {
      $.ajax({
        type : 'GET',
        url  : window.location.origin + '/api/v1/posts/' + id,
        data : {}
      })
      .done(function(res){
        self.post = res
        self.update()
      })
      .fail(function(){
        alert('Connection error.');
      })
      .always(function(){
        console.log('GET post.')
      })
    }

    cancel() {
      history.back()
    }

    submit(e) {
      e.preventDefault()
      var form = $('#edit-form')

      var type = 'POST';
      var url  = window.location.origin + '/api/v1/posts'
      var data = form.serialize()

      // 更新の場合はPUT
      if (id !== 'new') {
        type = 'PUT';
        url  = window.location.origin + '/api/v1/posts/' + id
        data = form.serialize() + '&id=' + id
      }

      $.ajax({
        type : type,
        url  : url,
        data : data
      })
      .done(function(res){
        console.log('Submitted.')
        $("#saved-dialog").modal();
        $('#saved-dialog').on('hidden.bs.modal', function (e) {
          window.location.href = window.location.origin
        })

      })
      .fail(function(){
        $("#connection-error-dialog").modal();
        $('#connection-error-dialog').on('hidden.bs.modal', function (e) {
          window.location.href = window.location.origin
        })
      })
      .always(function(){
        console.log('GET categories.')
      })
    }
  </script>

ポイント

  • 編集画面は新規作成と更新があるため、更新の場合は既存データを取得して表示させる

まとめ

Riot と Lumen でブログを作った。
Riot は本当に簡単で、やりたいことは大体ドキュメントを見ればすぐわかる。
書き方もシンプルだし、今後 SPA を作るときは Riot が最も有力な候補となりそう。

関連記事

www.yjhm214.com

www.yjhm214.com

スポンサーリンク