背景を暗くするモーダルウィンドウで表示したい!(Control.Modalの使い方)その3

モーダルウィンドウで表示していると、常に背景にメインウィンドウが透けて見える状態だ。だから、ユーザー環境設定を変更していると、何も変化しないことが気になってくる...。具体的には以下のような状況が妄想される...。

  1. 1ページ当りの表示件数を、10件から6件に変更してみる。
  2. 「ユーザー環境を更新しました。」とメッセージが表示される。
  3. でも、背景に見えているメインウィンドウの表示件数に変化無し。
  4. せっかちなユーザーからは、「表示件数を変更したのに反映されない。」と連絡が入る。
  5. 開発者としては、「メインウィンドウに戻って、何か操作をすると設定した件数になります。」と答える。
  6. ユーザーは、「そうですか...。」と返事をするが、「どうしてすぐに反映させないんだ?」と思っているはず。
  7. そして、「どうせ社内開発だから、期待してもしょうがないか...。」と諦めているかも。

以上、勝手な妄想だが、開発者としては上記のような状況になってしまうと、とても悔しい。上記ユーザーの要望は、もっともなことです。GUIは、リアルタイムで変化する状況を表現するから使い易いのであって、誰かが更新操作をするまで内部の変化を表現できないようでは、ユーザーの誤解を招く原因になってしまう...。見えている情報は、可能な限り辻褄を合わせておくべきなのだ。
でもそれって、どうやって実現すればいいんだろう?そもそも、どのような仕組みでモーダルウィンドウが表示されているかも理解できていない。その仕組み探れば何かヒントが見つかるかもしれない。(駄目出しするのは簡単だけど、作る側の負担は確実に増える...。)

Control.Modalのiframeがtrueとfalseの違い

iframeとは何か?

iframeとはインラインフレームのことで、ページの中に特定の領域を作って、その中に別のwebページを表示することが出来る機能らしい。でも、言葉で説明されてもピンと来ない。以下のHTMLソースをテキストファイル保存にして、ブラウザで開いて体感してみると一目瞭然だ。面白い!こんな機能、きっとどこかで見ていたはず。

<HTML>
  <HEAD>
    <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <TITLE>iframeサンプル</TITLE>
  </HEAD>

  <BODY>
    <h1>iframe</h1>
    <p>以下、iframeの利用例</p>
    <iframe src="http://www.google.co.jp/" width=500 height=400 frameborder=1>
            未対応のブラウザへのメッセージになる部分</iframe>
    <iframe src="http://www.hatena.ne.jp/" width=500 height=400 frameborder=1>
            未対応のブラウザへのメッセージになる部分</iframe>
    <iframe src="http://www.ruby-lang.org/ja/" width=500 height=400 frameborder=1>
            未対応のブラウザへのメッセージになる部分</iframe>
    <iframe src="http://www.rubyonrails.org/" width=500 height=400 frameborder=1>
            未対応のブラウザへのメッセージになる部分</iframe>
  </BODY>
</HTML>

iframeを使ったページを操作してみると分かるが、iframe内で操作した結果は、iframeの領域内のみ更新して、そのフレームの外には影響を与えない。(新規ウィンドウを開くリンクの場合は、新たにウィンドウが開くが...。)以上の知識から、Control.Modalの仕組みとか機能を、勝手に空想してみた...。

trueの場合
  1. iframeがtrueだと、モーダルウィンドウはiframe内に描画されることになるはず。
  2. モーダルウィンドウの中でページ遷移しても、モーダルウィンドウとして表示され続けるのは、更新範囲がiframe内に限定されるからなのであった。
  3. ウィンドウサイズを指定しないと、意図しないサイズで表示されてしまった。それはiframeの領域を指定しない場合と同じ現象が起こっていたのだ。iframeの領域は、自動サイズ設定には出来ない。領域を明示的に指定する必要がある。
falseの場合
  1. iframeがfalseだと、モーダルウィンドウの描画にiframeを使用しないはず。そうするとdiv要素とスタイルシートのみで実現しているのかな?CSVサーバーで、メニューを階層化した時のような効果を応用しているのかもしれない...。
  2. ページ遷移するとモーダルウィンドウが解除されてしまうのは、iframeで限定された領域が存在せず、ページ遷移の結果がページ全体の更新として扱われるからなのだ。
  3. ウィンドウサイズを指定しなくても、表示内容に合わせてウィンドウが内容にピッタリのサイズに可変してくれる。

iframeがfalseのモーダルウィンドウで表示

今までiframeがfalseでは、モーダルウィンドウの中でページ更新されるとモーダル状態が解除されてメインページに戻ってしまっていた。でも、これまでの経過を見て、ajaxで更新範囲を限定すれば、モーダルウィンドウの表示をコントロールできるようになると考えた。iframeがfalseのモーダルウィンドウを利用すれば、以下の効果が期待できる。

  • モーダルウィンドウのサイズを最初に指定する必要が無い。サイズの指定が無い場合は、ブラウザ側で表示に必要なサイズに自動調整してくれる。メッセージやエラー表示で内容が変化すれば、モーダルウィンドウのサイズもその都度可変する。

以下、iframeをfalseにしたモーダルウィンドウ化の手順。

app/views/layouts/_menu2.rhtml
レイアウト(メニューの描画を担当)
  • モーダルウィンドウへの最初に入り口になるメニューリンクの変更は無し。
  • iframeにはfalseを設定した。
...(途中省略)...
<div id="navcontainer">
  <ul id="navlist">
  <li><%= link_if_authorized_current 'CSVリスト', {:controller=>'csvs', :action=>'list'} %></li>
  <li><%= link_if_authorized_current '設定', {:controller=>'defaults', :action=>'edit_preference'}, :class=>"modal" %></li>

...(途中省略)...

<%#= javascript_tag <<'END_javascript_tag'
  // $$('a.modal')は、<a class="modal">である要素オブジェクト(element)を全て取得する。
  $$('a.modal').each(function(link){
	new Control.Modal(link,{
		            iframe: false
	});
  });
END_javascript_tag
%>
  • 上記JavaScript部分で、iframeがfalseの状態はデフォルト設定なので、全てのオプションを省略できる。以下のように簡潔に1行で書いてしまってもOK。
<%= javascript_tag "$$('a.modal').each(function(link){new Control.Modal(link)})" %>
app/views/defaults/edit_preference.rhtml
ビュー(ユーザー環境設定ページの描画を担当)

ajax更新で処理するため、以下の修正を行った。

  • ユーザー環境設定の更新範囲 id="default_update" を指定した。
  • 更新ボタンを押した時のフォームデータの送信範囲 id="default_params を指定した。
  • 「アプリケーションの初期設定へ戻す」リンクを、link_toから、link_to_remoteへ変更した。
  • 「更新」ボタン押した時の動作を、start_form_tagから、form_remote_tagへ変更した。
<div id="default_update"><%#<------ユーザー環境設定の更新範囲 %>
<h2>ユーザー環境の設定</h2>

<div>
<%#= link_to 'アプリケーションの初期設定へ戻す', :action => 'set_app_default' %>
<%# 上記1行を、以下のようにlink_to_remoteへ変更 %>
<%= link_to_remote 'アプリケーションの初期設定へ戻す', 
                   :update => 'default_update',
                   :url => {:action => 'set_app_default'} %>
</div>

<%#= start_form_tag :action => 'update_preference', :id => @default %>
<%# 上記1行を、以下のようにform_remote_tagへ変更 %>
<%# form_remote_tagは、returnキーを押した時も動作する。 %>
<%= form_remote_tag :update => 'default_update', 
                    :submit => 'default_params', 
                    :url => {:action => 'update_preference'} %>

  <div id="default_params"><%#<------更新ボタンを押した時のフォームデータの送信範囲 %>
  <%= render :partial => 'form_preference' %>
  </div>
  
  <div>
  <%#= link_to 'キャンセル', :controller => 'csvs', :action => 'list' %>
  <%= link_to_function 'キャンセル', "parent.Control.Modal.close();" %> |
  <%= submit_tag '更新', :id=>'submit' %>
  </div>
<%= end_form_tag %>
</div>

モーダルウィンドウとメインウィンドウの連携

これで、iframeがfalseのモーダルウィンドウをコントロール可能になった!残る課題は、モーダルウィンドウの更新結果を、リアルタイムにメインウィンドウに反映させることだ。
iframeをfalseにすることで、メインウィンドウと、モーダルウィンドウを、フラットな1ページとして取り扱うことが出来ると考えた。(きっと、モーダルウィンドウ上のリンクからでも、メインウィンドウのid属性を指定すれば、その部分の更新が可能になると思う。)だから、モーダルウィンドウの更新ボタンを押した時に、メインウィンドウの変更すべき部分も同時に更新すれば、背景に透けて見えるメインウィンドウが連動して更新されるように見えるはず。以下のようにやってみた。

app/views/defaults/edit_preference.rhtml
ビュー(ユーザー環境設定ページの描画を担当)
  • form_remote_tagの:completeオプションに、処理したいJavaScriptを設定しておけば、一連のajax通信が完了したタイミングでそれが実行される。
  • "new Ajax.Updater('list_update', '/csvs/list_update');"は、id属性が'list_update'のタグが示す範囲を、コントローラーcsvs、アクションlist_updateを実行した結果で更新してくれる。
...(途中省略)...
<%= form_remote_tag :update => 'default_update', 
                    :submit => 'default_params', 
                    :url => {:action => 'update_preference'}, 
                    :complete => "new Ajax.Updater('list_update', '/csvs/list_update');" %>
...(途中省略)...


以上で、ユーザー環境設定の変更が、リアルタイムに背景のメインウィンドウに反映されるようになった!

試行錯誤のJavaScript

  • ちなみに、iframeがtrueのモーダルウィンドウからは、単にメインウィンドウのid属性を指定しただけでは、その部分の更新は処理されなかった。おそらく、iframe内に限定してそのid属性の部分を更新しようとするから、モーダルウィンドウ内にその対象が見つからずに更新できないのかも。(勝手な空想)
  • 以下、:complete => "new Ajax.Updater('list_update', '/csvs/list_update');"に辿り着くまでに試したJavaScript
:complete => "parent.window.location.href = '/csvs/list'"
  =>メインウィンドウ全体がコントローラーcsvs、アクションlistで更新される。モーダル状態は解除される。

:complete => "parent.window.location.reload()"
  =>メインウィンドウ全体を現在のURLで再読み込みする。モーダル状態は解除される。

:complete => "parent.document.getElementById('list_update').innerHTML = '#{Time.now}'"
  =>メインウィンドウのid='list_update'部分だけ、現在の時刻で更新する。モーダル状態は維持される。
  • おまけで、link_to_functionが使えないか試した時のJavaScript
<%#= link_to_function 'function', "parent.Element.toggle('list_update')" %>
  =>メインウィンドウのid='list_update'部分の表示、非表示を繰り返す。モーダル状態は維持される。

<%= link_to_function 'function', "parent.Element.update('list_update', '/csvs/list_update')" %>
  =>メインウィンドウのid='list_update'部分に「/csvs/list_update」と表示される。モーダル状態は維持される。


結局、どの方法も、メインウィンドウの更新範囲を限定して、コントローラー、アクションで指定した描画結果を設定する方法が分からなくて諦めた...。

iframeがtrueでモーダルウィンドウとメインウィンドウを連携

あれ、この日記を書きながらいろいろ試しているうちに、iframeがtrueの状態でも、モーダルウィンドウからメインウィンドウの一部を更新して連携させることが出来てしまった...。以下のように、parent を付加すれば良かったのだ!

<%= form_remote_tag :update => 'default_update', 
                    :submit => 'default_params', 
                    :url => {:action => 'update_preference'}, 
                    :complete => "new parent.Ajax.Updater('list_update', '/csvs/list_update');" %>
そうするとiframeは、trueにするべきか、falseにするべきか、その判断基準は...
  • ajaxで更新するためには、更新範囲を設定したり、パラメーターの送信範囲を設定したりと、設定する項目が多く、手間がかかる。とりあえず動くものを手っ取り早く作りたいので、最初のiframeはtrue。
  • そのまま、メインウィンドウと連携させる必要が無ければ、iframeはtrueのままでOK。
  • ウィンドウを自動的にサイズ調整して欲しくなったら、iframeはfalse。
    • 但し、モーダルウィンドウに表示する内容によっては、大き過ぎたり、細長くなり過ぎてしまうことがある。その時は自分でサイズを指定する必要あり。
  • 外部のページ(例:googleなどの検索ページ)をモーダルウィンドウで表示したければ、iframeはtrue。
    • 外部のページに対してajax更新の指定は出来ない...。iframeがfalseではページが更新された瞬間モーダル状態が解除されて、結果がメインウィンドウに表示されてしまう...。

参考ページ

JavaScriptがわからない自分にとって、以下のページが大変参考になりました。感謝です!