Using Control Modal with Rails' AJAX framework

Hi,

I am trying to use Control.Modal with Rails' AJAX model. The tricky thing is that Rails trigger the Ajax Request by putting it in the "onClick" trigger, not as a "href" in the element. (Rails puts "#" in href, which points back to the current page!). In short, Rails generates something like the following:

In your javascript, around line 272, the ajax request is created assuming that the URL is in this.href. This breaks Rails AJAX apps.

I think there is a way out, which involves some extracting of string:

  1. At initialization, if this.element.href == '#', do the following: 1.1. parse this.element to extract the URL from the onclick attribute and save it for later use at Object.open().

  2. I think the logic for always calling this.update(request.responseText) for both cases of evalScripts==true and evalScripts==false may not be correct. It is because the responseText in the case of evalSCripts== true will be some javascript. They should not be displayed at all, just be evaluated. The correct sequence of things, IMHO, is are followings: 2.1. evaluate the javascript from the responseText. (this you have done) 2.2. do all the things in the update function except the first line, which reads "Control.Modal.container.update(html);".

I am trying to patch it but without much success yet. May be my solution above is not 100% correct. I will keep trying to find one.

Just want to let you know so maybe you have a better idea to fix these problems.

Thanks for sharing this script. It is elegant, lightweight, and just works (mostly :)

Posted April 11th, 2007 at 1:54am by kwanpeter

Make sure you are using the latest version, but I think some of the issues are still valid. The rails issues you bring up are valid, but I'm not going to rewrite the core functionality of the library for rails. However, here is a helper I wrote (if you know a better way to prepare the javascript options let me know, it smells fishy to me). Oh, and with the whole update issue, I am using Control.Modal in a large rails app with lots of modals some with JS, some without. "Works for me". These problems seem a little like edge cases to me. I you come up with a backwards compatible patched version I would love to see it though.

# Examples: # <%= link_to_modal_window('Testing!','http://url/') %> # <%= modal_window('Testing!','#two') %> # <% modal_window_for('Three','#three',{:class => 'my_link'},{:class => 'my_container'},:height => 400, :afterOpen => 'function(){alert("!");}') do %> # i am three! #<% end %> # # Use <%= link_to_modal_window(...) %> for modals that link to elements that are already on the page, or will be added elsewhere # Use <% modal_window_for(...) do %> modal contents <% end %> otherwise def link_to_modal_window(link_contents,id,link_options = {},container_options = {},options = {},&block) link_id = Digest::MD5.hexdigest(id + link_contents) options_string = '' non_quoted = %w{beforeOpen afterOpen beforeClose afterClose beforeLoad onLoad afterLoad requestOptions} options.each{|item| options_string << item[0].to_s + ':' + (non_quoted.include?(item[0].to_s) || item[1] == 'true' || item[1] == 'false' ? item[1].to_s : "'" + item[1].to_s + "'") + ','} output = '' output << content_tag('a',link_contents,{:href => id,:id => link_id}.update(link_options)) output << "Event.observe(window,'load',function(){new Control.Modal($('" + link_id + "'),{" + options_string.gsub(/,$/,'') + "});});" if(block_given?) concat(tag('div',{:id => id.gsub(/^#/,''), :class => 'modal_container'}.update(container_options),true),block.binding) yield concat('',block.binding) concat(output,block.binding) else output end end alias :modal_window_for :link_to_modal_window

Posted April 11th, 2007 at 1:28pm by ryan

Dammit, I have got to figure out the spacing issue on this forum.

Posted April 11th, 2007 at 1:28pm by ryan

I am using Rails 1.2.3.

Yup. Your helper will make Rails work nice with Control Modal.

Thanks.

Posted April 11th, 2007 at 2:58pm by kwanpeter

If you find a better way to encode the javascript options please let me know.

Posted April 11th, 2007 at 4:05pm by ryan

You code works. If by "fishy" you meant you have to do the gsub then I have the following comment:

I will replace the following set of lines from the original helper:

  1. options_string=''

  2. options.each{|item| options_string << item[0].to_s + ':' + non_quoted.include?(item[0].to_s) || item[1] == 'true' || item[1] == 'false' ? item[1].to_s : "'" + item[1].to_s + "'") + ','}

  3. and that you need to gsub the last dangling comma with options_string.gsub(/,$/,'')

with a more Ruby way of doing things -- use join:

options_string = options.to_a.map { |key,value| key.to_s + ':' + (non_quoted.include?(key.to_s) || value == 'true' || value == 'false' ? value.to_s : "'" + value.to_s + "'") }.join(',')

Then there is no dangling comma to look after. You just use options_string straight in composing the Event.observe line.

Posted April 12th, 2007 at 1:41am by kwanpeter

I always forget about map!

The other thing I was curious about was if there was some magic endode javascript helper I am missing out in rails. Nothing I found seemed to do the trick... Thanks for the code!

Posted April 14th, 2007 at 1:53pm by ryan

I can't seem to get this to work. I'm using Rails 1.2.3 and Control.Modal 2.2.2.

I had to change the above helper method to the following to get the Event.observe items to show up in a <script> tag (see last 'output <<' line).

  def link_to_modal_window(link_contents,id,link_options = {},container_options = {},options = {},█)
    link_id = Digest::MD5.hexdigest(id + link_contents)
    non_quoted = %w{beforeOpen afterOpen beforeClose afterClose beforeLoad onLoad afterLoad requestOptions}
    options_string = options.to_a.map { |key,value| key.to_s + ':' + (non_quoted.include?(key.to_s) || value == 'true' || value == 'false' ? value.to_s : "'" + value.to_s + "'") }.join(',')
    output = ''
    output  id,:id => link_id}.update(link_options))
    output  id.gsub(/^#/,''), :class => 'modal_container'}.update(container_options),true),block.binding)
      yield
      concat('',block.binding)
      concat(output,block.binding)
    else
      output
    end
  end
  alias :modal_window_for :link_to_modal_window

And I'm calling it as follows:

 "border: 1px solid #000000;"}),
                         "/photo_session/#{@client.name}/#{@photo_session.name}/photo_details/#{ @photo_session_image.split(".")[0..-2]}" -%>

The HTML it generates is as follows:

<a href="/photo_session/hc_03" id="714aababc6275ea9b67406aaa05e1f5d"><img alt="Hc_03" src="/image/thumbnail/hc_03.jpg" style="border: 1px solid #000000;" /></a>
<script type="text/javascript">
   Event.observe(window,'load',function(){new Control.Modal($('714aababc6275ea9b67406aaa05e1f5d'),{});});
</script>

However, when I click on one of these images, Firefox (using Firebug) throws the following error and the browser navigates to the URL in the HREF and the dialog never opens.


Control.Modal.overlay.observe is not a function
/javascripts/control.modal.js
Line 324

Also, when the page loads, I get the following error:


Control.Modal.container.observe is not a function
/javascripts/control.modal.js
Line 62

Does anyone know what I might be doing wrong?

Thanks!

Posted July 25th, 2007 at 1:03am by Caleb Jones

Sorry, I forgot to escape some HTML entities in the second code block showing how I call the linktomodal_window() method. You'll have to view the source of this thread to see its entirety.

Posted July 25th, 2007 at 1:05am by Caleb Jones

Ok, here's how the seconed code block above should have appeared:

<%= link_to_modal_window image_tag("/image/thumbnail/#{@filename}", {:style => "border: 1px solid #000000;"}),
                         "/photo_session/#{@client.name}/#{@photo_session.name}/photo_details/#{ @photo_session_image.split(".")[0..-2]}" -%>

Posted July 25th, 2007 at 1:10am by Caleb Jones

Ok, here's how the seconed code block above should have appeared:

<%= link_to_modal_window image_tag("/image/thumbnail/#{@filename}", {:style => "border: 1px solid #000000;"}),
                         "/photo_session/#{@client.name}/#{@photo_session.name}/photo_details/#{ @photo_session_image.split(".")[0..-2]}" -%>

Posted July 25th, 2007 at 1:21am by Caleb Jones

Here's the version I've been using to get Control.Modal working nicely with Rails.

def link_to_modal(name, options = {})
  options.recursively!(&:symbolize_keys)
  contents = options[:for]
  options = {
    :link  => { :id => (contents || 'default').to_s+'_modal_link' },
    :modal => {}
  }.rec_merge!(options)
  options[:modal][:contents] ||= "$('#{options[:for]}').remove().innerHTML".to_jsl if options[:for]
  js_block = lambda do |page|
    page << "Event.observe(window,'load',function() {"
    page.call(
      'new Control.Modal',
      options[:link][:id],
      options[:modal].recursively(&:methodize_keys)
    )
    page << '});'
  end
  output = ""
  output << link_to(name, options[:url], options[:link])
  output << "\n"
  output << update_page_tag(&js_block)
  output
end

I do use some custom methods to make this work nicely: http://pastie.textmate.org/86031

There are 4 main option keys that can specify the behavior of the output.

  1. :url - this is passed directly to the Rails link_to function.
  2. :for - the name of the div you want the modal to use (if different from your :url)
  3. :link - the HTML attributes of the <a> tag
  4. :modal - the options passed to Control.Modal as a hash

The :modal keys can be specified in underscored, camelized, or the actual name (which I call "methodized"). For example...

:modal => {
  :fade => true,
  :fade_duration => 0.25,
  :overlay_close_on_click => false
}
:modal => {
  'fade' => true,
  'fadeDuration' => 0.25,
  'overlayCloseOnClick' => false
}

Both return the same hash in javascript:

{fade: true, fadeDuration: 0.25, overlayCloseOnClick: false}

One of the custom methods I use is for including literal javascript in a ruby hash or whatever:

literal_javascript = %{function() { alert('Hello, World!'); }}
literal_javascript.to_json        # => "\"function() { alert('Hello, World!'); }\""
literal_javascript.to_jsl.to_json # => "function() { alert('Hello, World!'); }"
# or
literal_javascript.literal.to_json

This means that you can do:

{ :before_open => %{function() { alert('Hello, World!'); }}.to_jsl }

And you'll get:

{beforeOpen: function() { alert('Hello, World!');}}

Instead of:

{beforeOpen: "function() { alert('Hello, World!'); }"}

Here are some usage examples...

Simple link:

<%= link_to_modal 'Show modal', :url => '#page_contents' %>
<div id="page_contents" style="display:none;">This should show in the modal window.</div>

Output:

<a href="#page_contents" id="default_modal_link">Show modal</a>
<script type="text/javascript">
//<![CDATA[
try {
  Event.observe(window,'load',function() {
    new Control.Modal("default_modal_link", {});
  });
} catch (e) { alert('RJS error:\n\n' + e.toString()); alert('Event.observe(window,\'load\',function() {\nnew Control.Modal(\"default_modal_link\", {});\n});'); throw e }
//]]>
</script>
<div id="page_contents" style="display:none;">This should show in the modal window.</div>

Complex link:

<%= link_to_modal 'Show modal', :url => page_url, :for => 'page_contents', :link => { :class => 'modal_link' }, :modal => { :fade => true, :fade_duration => 0.25, :width => 300, :height => 50, 'beforeOpen' => %{function() { alert('beforeOpen'); }}.to_jsl, :after_close => %{function() { alert('afterClose'); }}.to_jsl }%>
<div id="page_contents" style="display:none;">This should show in the modal window.</div>

Output:

<a href="http://localhost:3000/page" class="modal_link" id="page_contents_modal_link">Show modal</a>
<script type="text/javascript">
//<![CDATA[
try {
  Event.observe(window,'load',function() {
    new Control.Modal("page_contents_modal_link", {afterClose: function() { alert('afterClose'); }, height: 50, contents: $('page_contents').remove().innerHTML, beforeOpen: function() { alert('beforeOpen'); }, fadeDuration: 0.25, width: 300, fade: true});
  });
} catch (e) { alert('RJS error:\n\n' + e.toString()); alert('Event.observe(window,\'load\',function() {\nnew Control.Modal(\"page_contents_modal_link\", {afterClose: function() { alert(\'afterClose\'); }, height: 50, contents: $(\'page_contents\').remove().innerHTML, beforeOpen: function() { alert(\'beforeOpen\'); }, fadeDuration: 0.25, width: 300, fade: true});\n});'); throw e }
//]]>
</script>
<div id="page_contents" style="display:none;">This should show in the modal window.</div>

Posted August 8th, 2007 at 11:40am by PotatoSalad

Following PotatoSalad's instructions almost works for me. The overlay shows up properly and the AJAX call reaches the server causing the server to log the request properly, but the content window never appears. Firebug seems to indicate that the content window never looses its "display:none;" style. Additionally the content window never gets any content. If I manually show the content div (again using firebug) it just appears empty (though it does have the correct dimensions).

A packet trace shows that the HTML snippet is sent back to the browser, but nothing happens with it. The content contains an image which never is requested by the browser. It almost seems like something is going awry in Control.Modal after the AJAX call is made. Does anyone know what would cause this to happen?

Here's the rhtml template:

<%= link_to_modal image_tag("/image.jpg", {:style => "border: 1px solid #000000;"}),
                          :url => "/url/to/ajax/action",
                          :link => { :id => "unique_id" },
                          :modal => { :fade => true,
                                      :fade_duration => 0.25,
                                      :width => 300,
                                      :height => 300 } %>

Here's the output of the modal link (I removed the text in the alert dialog as it is irrelevant to what my question is):

<a href="/url/to/ajax/action" id="unique_id"><img alt="" src="/image.jpg" style="border: 1px solid #000000;" /></a>
<script type="text/javascript">
//<![CDATA[
try {
Event.observe(window,'load',function() {
new Control.Modal("unique_id", {height: 300, fadeDuration: 0.25, width: 300, fade: true});
});
} catch (e) { alert(''); throw e }
//]]>
</script>

Posted August 22nd, 2007 at 4:22pm by Caleb Jones

I gave up on this a while back (I could never get it to work) and am now revisiting it. I'm still using PotatoSalad's helper and am trying to follow his instructions.

I have an example of what I am trying to do here.

The URL it is going to has a 'jpg' at the end, but I've changed Control.Modal's imgRegexp to /.(gif|png|tiff?)$/ to exclude JPEG extensions as my Rails app responds with HTML for the URLs being passed into Control.Modal.

The modal overlay appears and the div that is to have the contents of the Ajax call appears with its default content, but the Ajax request is never made. I can click on as many of those images as I want while watching the Rails server log but the URL passed into Ajax calls never hits the server.

Interestingly, if I change the imgRegexp back to /.(jp?g|gif|png|tiff?)$/ to include JPEGs, the target Rails action is called, but of course things don't work properly as the action returns HTML and not an image.

I've got to just be doing something dumb here or misunderstanding how to properly call Control.Modal since most people say the Rails helper above works just fine for them.

I've been able to get Control.Modal to work in a Java/Spring/Servlet world, but seem to be having trouble this time around.

If anyone can help me out, I would be very grateful!

Posted November 10th, 2007 at 5:23pm by Caleb Jones

Have you tried solving the problem from the backend? I will take a look at your regex when I get a chance the week of the 19th, but I think you could just have the server return the HTML with no layout.

render :partial => 'your template', :layout => false

Posted November 10th, 2007 at 6:24pm by ryan

I already have the following in my controller's action:

render_partial "photo_details", :layout => false

I modified the regex to force Control.Modal to enter the 'ajax' case block. My URL is of the format "/photosession/[USERNAME]/[SESSION]/photodetails/[IMAGE FILENAME]" which would match the image regex and Control.Modal would enter the 'image' case block (which I assume I don't want if the response is HTML).

The main confusion I'm having is that when it does enter the 'ajax' block, a request is never sent to the server. I put alert()'s before the "this.ajaxRequest = new Ajax.Request(this.href,options);" line to ensure that ' this.href' does in fact point to the correct URL for the Ajax request (which it does).

Also, by way of clarification, I forgot to mention that I have this working fine with Lightbox Gone Wild. So I'm pretty sure things are set up correctly on the server side. The problem with Lightbox Gone Wild is that it requires a version of prototype which is incompatible with Rails 1.2.5. That and it's not nearly as good of an implementation as Control.Modal is ;).

Posted November 11th, 2007 at 12:22pm by Caleb Jones

Login or Register to Post