QA@IT

railでrenderの表示する順番をコントロールしたい

4452 PV

例えばrailsのコントローラーで
def index
@1nums = [1,3,5,7]
@2nums = [2,4,6,10,12]
@3nums = [9,11]
end
とし、
index.html.erbで
<%=render"@1nums"%>
<%=render"@2nums"%>
<%=render"@3nums"%>
と記述した場合
1357246812911
と表示されてしまうので、
12345678901112
と表示するように順番を指定する方法をおしえてください。

回答

ちょっとコード(それだとエラー出ませんか?)から読み取れないんですが、部分テンプレートの話でしょうか?

コントローラであらかじめ結合しておくことはできないんですか?


結論を先に

現状コードから読み取れない部分があるので後で丁寧に説明しますが、
コメントで書いたような部分テンプレートを使用しているのであれば、
コントローラでソート済みの一つのコレクションにしておいて、viewの中でループで回してrenderを呼び出せばテンプレートはモデルのクラスから自動で判別されるので、コレクションのソート順に出力させることができます。

詳細

railsに詳しくないので適切な省略しどころがわからないため、丁寧すぎる(冗長な)説明になるかもしれません。
ルーティングなどもサンプルなので適当なURLにしています。並び替えの部分だけ参考にしてください。

環境: Ubuntu 14.04 + rails 3.2.16
rails は 4でも問題ないと思います。

実際に実行する必要はありません、実際の説明は create_at順に表示する 以降になります。
準備部分に関しては自分の環境と相違ないか確認したりするために使ってください。

ModelやControllerの作成から始めます。

Modelの用意

3種類、適当に用意しました。内容は適当です。
設計方針によってはModelをいろいろそろえることで共通するモデル(今回で言えばどれもツイートを表すモデルという意味では共通のモデル)で同じテンプレートが使える様にすべき場合もありますが、
今回はフィールドやテンプレートがどうなっているのか見えなかったので、一つだけフィールドが異なるために共通化できないモデルにしました。

以下のコマンドでモデルを用意します。

$ rails g model text_model text:string
$ rails g model image_model uri:string
$ rails g model link_model uri:string

text_modelだけフィールドが異なります。

DBの更新を行います。

$ rake db:migrate

このサンプル用のコントローラ/テストデータの作成

このサンプル用のコントローラを作成します。

$ rails g controller qait9372_sample

テストデータの初期化用に メソッドを追加します。

app/controllers/qait9372_sample_controller.rb

class Qait9372SampleController < ApplicationController

  def init_data
    TextModel.delete_all
    LinkModel.delete_all
    ImageModel.delete_all

    TextModel.create(text: "Sample1 001")
    LinkModel.create(uri: "http://example.com/samplelink1_002.html")
    ImageModel.create(uri: "http://example.com/example1_003.jpg")

    LinkModel.create(uri: "http://example.com/samplelink2_004.html")
    TextModel.create(text: "Sample2_005")
    ImageModel.create(uri: "http://example.com/example2_006.jpg")

    TextModel.create(text: "Sample3_007")
    ImageModel.create(uri: "http://example.com/example3_008.jpg")
    LinkModel.create(uri: "http://example.com/samplelink3_009.html")

    render :text => "Done"
  end
end

textまたはuriの最初の1けたの数字がモデル内の順番、3桁が全てのモデルの中での順番を表しています。
最終的に3桁の数字の順に並んでいれば、古い順に並んでいるだろうという目安になります。

ルーティングを設定して、該当ページにアクセスしてデータの初期化を行います。

config/routes.rb

Qa::Application.routes.draw do

  # 以下の 1行を追加
  get '/create', to: 'qait9372_sample#init_data'

rails sで起動して http://localhost:3000/create にアクセスすればデータが再作成されます。

質問の再現

ここで、質問と大体おなじ状況がそろったと思いますので、質問の状態を再現したいと思います。

まだコメントの回答をもらえていませんが、この質問にある index.html.erb でのrenderはパーシャルを利用してコレクションをレンダリングしている、つまり http://railsguides.jp/layouts_and_rendering.html の 3.4.5 にあたるものと考えて話を進めます。

このメソッドには略記法もあります。@productsがproductインスタンスのコレクションであるとすると、index.html.erbに以下のように書くことで同じ結果を得られます。

<h1>Products</h1>
<%= render @products %>

コントローラにアクションの追加

コントローラにbad_oneメソッドを追加します。

app/controllers/qait9372_sample_controller.rb

class Qait9372SampleController < ApplicationController

  def init_data
    # 変更なしなので省略 
  end

  def bad_one
    @text_models = TextModel.all
    @link_models = LinkModel.all
    @image_models = ImageModel.all
  end
end

ビューの作成

app/views/qait9372_sample/bad_one.html.erb を作成し、以下の様にします

<%= render @text_models %>
<%= render @image_models %>
<%= render @link_models %>

renderにモデルのコレクションを渡す事で、コレクション内のそれぞれのテンプレートを適用しています。
そのため各モデルの部分テンプレートを用意する必要があります。

テンプレートの作成

前述の通りパーシャル(部分)テンプレートが必要になるため、
アンダースコアで始まるファイル名で、各モデル用のテンプレートを作成します。

app/views/text_models/_text_model.html.erb

<%= text_model.text %><br/>

app/views/link_models/_link_model.html.erb

<%= link_model.uri %><br/>

app/views/image_models/_image_model.html.erb

<%= image_model.uri %><br/>

ルートの設定

Qa::Application.routes.draw do

  get '/create', to: 'qait9372_sample#init_data'
  # 以下の 1行を追加
  get '/bad_one', to: 'qait9372_sample#bad_one'

http://localhost:3000/bad_one

にアクセスします。すると各モデル毎に並んだ状態になります。

Sample1 001
Sample2_005
Sample3_007
http://example.com/example1_003.jpg
http://example.com/example2_006.jpg
http://example.com/example3_008.jpg
http://example.com/samplelink1_002.html
http://example.com/samplelink2_004.html
http://example.com/samplelink3_009.html

余談ですが、app/views/layouts/application.html.erb の スタイルシートの行(
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>)
でエラーが出る場合は、今回は必要ないので該当の行を削除してください。
( sass-rails のバージョンの問題です。 )

これをcreate_at順になるようにします。


create_at順に表示する

 

単一のコレクションをviewに渡すようにする

現在、viewでは各モデルごとに1回ずつ、renderを呼び出しています。
モデル毎に別々に処理されてしまうので、現在のままではこの塊の順番は変更できません。

今はモデル毎に3つのコレクションを受け取っていますが、これを1つのコレクションにまとめてしまってそれをviewで処理するようにつくりかえます。

コントローラに新しいアクションの用意

bad_oneは比較用に残しておくとして、新しいメソッドを追加します。

app/controllers/qait9372_sample_controller.rb

class Qait9372SampleController < ApplicationController

  def init_data
    # 省略
  end


  def bad_one
    # 省略
  end

  def alternate_way

    @models = []

    @models.concat(TextModel.all)
    @models.concat(LinkModel.all)
    @models.concat(ImageModel.all)

    @models.sort! { |a,b| a.created_at <=> b.created_at }
  end

end

modelsインスタンス変数にすべてのアイテムを格納して、破壊的ソートで created_at 順に並び替えます。

ビューの追加

以下のファイルを作成し、ビューの内容を記述します。
まずは部分テンプレートは使わずに、確認のためのコードを書きます。

app/views/qait9372_sample/alternate_way.html.erb

<!-- models の件数を表示します -->
models : <%= @models.length %><br />

<!-- models を一つずつ確認し、どのクラスのインスタンスかと、created_at をミリ秒まで表示します -->

<% @models.each do | model | %>
Text? :<%= model.is_a?(TextModel) %> /
Image? :<%= model.is_a?(ImageModel) %> /
Link? :<%= model.is_a?(LinkModel) %>
<br />
<%= model.created_at.strftime("%Y-%m-%d %H:%M:%S.%L") %><br />
<br />
<% end %>

ルートの追加

ルーティングを追加します。

Qa::Application.routes.draw do

  get '/create', to: 'qait9372_sample#init_data'
  get '/bad_one', to: 'qait9372_sample#bad_one'
  # 以下の 1行を追加
  get '/alternate_way', to: 'qait9372_sample#alternate_way'

実行

きちんとモデルが扱えている事を確認します。以下の様な出力になっていれば問題ありません。

models : 9
Text? :true / Image? :false / Link? :false
2015-05-10 03:57:42.310

Text? :false / Image? :false / Link? :true
2015-05-10 03:57:42.334

Text? :false / Image? :true / Link? :false
2015-05-10 03:57:42.348
~ 以下略 ~

テンプレートを使うように修正する。

並び変わりましたが、これまでの部分テンプレートが利用できていません。
部分テンプレートを使用してレンダリングされるように以下のコードを追加します。

まずは text_models だけで試してみます。

app/views/qait9372_sample/alternate_way.html.erb に以下を追加します。

(邪魔であれば元のコードは消してしまっても構いません)

<ul>
  <% @models.each do | model | %>
    <% case %>
    <% when model.is_a?(TextModel) %>
    <li> <%= render partial: "text_models/text_model", object: model %> </li>
    <% end %>
  <% end %>
</ul>

caseを使って、Model毎の場合分けを行っています。
renderメソッドに、パーシャルに名前を与え、対象オブジェクトを指定することで、部分テンプレートを使って単一のTextModelをレンダリングするように指示します。

実行して以下の様な表示になればOKです。

* Sample1 001
* Sample2_005
* Sample3_007

残りの二つも同様に追加して、今回追加した @models.each のブロックは以下の様になります。

<ul>
  <% @models.each do | model | %>
    <% case %>
    <% when model.is_a?(TextModel) %>
    <li> <%= render partial: "text_models/text_model", object: model %> </li>
    <% when model.is_a?(ImageModel) %>
    <li> <%= render partial: "image_models/image_model", object: model %> </li>
    <% when model.is_a?(LinkModel) %>
    <li> <%= render model %> </li>
    <% end %>
  <% end %>
</ul>

結果は以下の様になり、3桁の数字が昇順で並んでいてDBへの追加順になっていることがわかると思います

* Sample1 001
* http://example.com/samplelink1_002.html
* http://example.com/example1_003.jpg
* http://example.com/samplelink2_004.html
* Sample2_005
* http://example.com/example2_006.jpg
* Sample3_007
* http://example.com/example3_008.jpg
* http://example.com/samplelink3_009.html

LinkModelのrenderに注目してください。
<%= render model %>というシンプルな形になっています。

この場合、railsが modelの型(クラス)を見て、決まった位置にあるテンプレートを探してレンダリングしてくれます。

修正前の <%= render @link_models %> とも似ていますが、このときは @link_models がコレクションであることも railsが検知して、その中の 1つ1つの型から、決まった位置にあるテンプレートを探してレンダリングします。
内部的には少しことなりますが、どちらの場合も(ルールに従ってテンプレートを置いておけば)railsがそれに応じて処理します。

細かく指定することも、railsのルールに任せることも可能です。

最後の整理

    <li> <%= render model %> </li>

で指定して、各モデル用の部分テンプレートを用意しておけば上手く処理してくれるわけですから、case文自体が不要ということになりますね(railsが自動でやってくれることを自分でもやってしまっている)。
これを利用すると @models.each は以下の様に書けます。

<ul>
  <% @models.each do | model | %>
    <li> <%= render model %> </li>
  <% end %>
</ul>

同様に、コレクションのまま渡してもレンダリング自体は上手く処理してくれますが、
要素ごとにhtml要素を分けたい場合は注意してください。たとえば以下の様にした場合は、上の場合と結果が異なります。

<ul>
  <li> <%= render @models %> </li>
</ul>

前者は 1つの li の中で モデル1つ分のレンダリングを行い、liはモデルの数だけ作られます。
後者は 1つの li の中で すべてのモデルのレンダリングを行ってしまいますので liは 1つだけです。

このあたりは用途に併せて使い分けてください。

最終的な コントローラとビューは以下の様になります。 ( 省略している部分、部分テンプレートの内容は上から探してください。 )

app/views/qait9372_sample/alternate_way.html.erb

<ul>
  <% @models.each do | model | %>
    <li> <%= render model %> </li>
  <% end %>
</ul>

app/controllers/qait9372_sample_controller.rb

class Qait9372SampleController < ApplicationController

  def init_data
    # サンプルデータの作成
    # 省略
  end

  def bad_one
    # 質問の再現
    # 省略
  end

  def alternate_way
    @models = []

    @models.concat(TextModel.all)
    @models.concat(LinkModel.all)
    @models.concat(ImageModel.all)

    @models.sort! { |a,b| a.created_at <=> b.created_at }
  end
end

config/routes.rb

Qa::Application.routes.draw do

  get '/create', to: 'qait9372_sample#init_data'
  get '/bad_one', to: 'qait9372_sample#bad_one'
  get '/alternate_way', to: 'qait9372_sample#alternate_way'

※ 部分テンプレートとして app/views/text_models/_text_model.html.erb, app/views/link_models/_link_model.html.erb, app/views/image_models/_image_model.html.erb が必要

編集 履歴 (4)
  • 上記のコードは簡略化して書いているのでわかりにくくてすいません。
    現在twitterようなものを作っているのですが、主に投稿形式が3つありまして、3つともtweetのデザインが違い、モデルも違います。そして私はその3つの投稿をcreated_at順でタイムラインに流したいのですが、上記のようにrenderを書いてしまいますと書いたインスタンス変数順に投稿が表示されてしまうので、
    -
  • それをどうにか投稿の時間順に流したいのですが、どうすればいいかわかりますか?
    わかりにくい説明ですみません。
    -
  • 内容は大体わかりました。インスタンス変数名を数字から始めることができないので、それだけ(偽の名前でよいので)エラーにならない名前にしてほしいです(説明しにくいため)。あと各モデル用のテンプレート( 今の名前から言えば _1num.html.erb等 )も存在すると思っていますがこの認識は合ってますか? -
  • 私の拙い説明でありながらこれほど丁寧でわかりやすい解説をしていただき心から感謝します。疑問は全て解決しました。本当にありがとうございます。
    後ひとつ質問させていただいてもよろしいでしょうか?
    この質問で気を悪くしてしまったのなら先にお詫びを申し上げときます。すみません。
    なぜあなたは無償で面識もない私や他のユーザーの質問にまるで聖人君子のようにこれほど丁寧に的確に回答してくれるのでしょうか?
    -
  • こんにちは。この質問に答えた理由は時間があったのと答えようかと思ったですね。おっしゃる通り金銭的には無償です。でも課題が得られる・復習・書く練習などメリットはありますよ。気分や忙しさや質問者の対応などによって気が向かない場合ももちろんあります。全然聖人君子ではありませんね(笑) -
  • 「拙い説明でありながら」という点は、初学者が何を聞けば良いかわからない、別の理由で開かせない場合など事情によってそうなってしまうことがあるのはわかります。真の理由はネットを通してはわかりませんが、コメントのやりとりなどで答える情報があつまりそうなら答えています。 -
  • 私個人でいえばブログだとかが続かない質なのですが、不思議と回答は続けられるようです。
    やろう!と気合い入れてもやりたくない場合は逆効果だそうで(心理学では生理学的覚醒の優勢強化っていうそうで最近気がつきました)、質問に回答するぐらいが距離感としてちょうどいいみたいです。
    結局は自分のために回答しているわけですね(誰かの訳にたつ回答ができた場合はWin-Winかも...そうなれば素敵ですね)。
    -
  • 長くなりましたが答えになってますか?
    最後に、問題が解決した場合は役に立った回答をベストアンサーにしてくださいね(そうしないと一覧で未解決の様に見えてしまうので)。
    -
  • 答えて頂きありがとうございます。なるほど、そうゆうメリットを感じているのですね。でも私は初学者のためそのような考えに至らず、自分のことでいっぱいいっぱいなのでなんと言われようが私には聖人君子に見えます。w
    本当にありがとうございました。
    -
ウォッチ

この質問への回答やコメントをメールでお知らせします。