Tuesday, February 3, 2009

link_to with prompt for rails 2.2.2

The feature request said: Create a mechanism whereby a user will be notified when an administrator suspends their account... and further... ensure that the administrator inputs a reason for the suspension. And include that reason in the notification email to the user..

So.. I thought to myself... sure... lets not stop at merely asking the administrator if they are sure about what they're doing when they press that shiny red button that makes the database row magically implode... Lets go a step further and ask the user why they want to press the shiny red candy-like button... that makes the record to vanish in a fiery ball of liquid hot mag-ma.

I thought about this issue for a few moments and decided:

  1. I want this to be as painless as possible to implement (i mean retarded-simple)... i need to be able to do this after a long morning of skulling Irish coffee...
  2. I want to be able to seriously annoy my users and administrators by asking them to explain themselves for every single action they ever do... (not that I would... but I could)
  3. I don't want to have to overload actions or create duplicate actions for every single explainable action in the system... (after all, duplicating work means i have to type that much more... or hit command-c, command-v alot... and how much fun is that... really?)
  4. It would be a huge bonus if this was as easy as link_to :confirm=>'blah blah'
So, in a brief brain dump with my coworkers... somebody (the boss... this is probably why he's the boss)... says in a moment of pure brilliance... use a javascript prompt...

To which i scratched my head... and thought way back to those yonder days of yore... yes, there were alert-style pop-ups that contained text inputs... (that was way-way-back in the days of never-ending q&a sessions with a pre-programmed script... the great-grandfather of being rick-rolled)... i've not seen one of those in a long time... it's crazy... but it just might work...

But wait... Seriously... i figured that somebody else had surely done this already... like maybe the rails core team... surely such basic function already existed... right... I mean, those guys that write this stuff think of everything, don't they?

Seriously... no sarcasm meant in that line... really... nine out of ten times i start doing anything in ruby these days i come to realize that somebody else has already done it... and usually done it far better than i was planning to... so... I scan the API docs, do some serious Googling and eventually scan the source code for link_to... and verify as best i can that nobody has done this yet...

Thinking that nobody has ever done this... This sometimes makes me nervous (like any time i have this feeling... i start to question my motives)... if nobody else has done it... does that mean there is a good reason... has nobody ever needed it... or maybe, like genetic manipulation... maybe it's better that we don't do it... but i think i'm getting a little off track here...

FUH--CUS... EV-A-RAY BODDY MUST FUH-KUS

After much source code tracing and digestion the three simple methods below crafted into a strategically placed barrel-of-monkey-patch will have your rails link_to method capturing that extra explanation string and passing in on through to your controller so you can decide what to do with it... i.e. Email the user whose account just got suspended that they won't be able to log in anymore... or whatever you want... the possibilities are truly limitless.

The beauty is: you get all this using the same conventions you already use.. that is assuming 'you' are my fellow ruby on rails coders and 'you' do things like i do... i know it'a stretch... but ya never know...


link_to 'my Action', action_url, :prompt=>'why do you want to do this?'


This little gem will confront your user with a nifty javascript alert with a string input and will refuse to pass go until the user has entered an excuse for their request. Additional validation on the supplied string should be done server-side and perhaps a flash message and redirect tells the user their excuse wasn't up to snuff..



And i do suppose it's important to mention that the supplied reason will be posted in to your controller action in the form or params[:prompt_reply] ...



Hooray for perpetuating the everlasting nag-screen!!!



And for those of you have been ever-so-patient and read this far i give you... the code... and better yet... forget the code... i mixed this into a simple plugin... snag it from github. http://github.com/bhedana/link_to_with_prompt/




module ActionView
module Helpers
module UrlHelpers

def convert_options_to_javascript!(html_options, url = '')
prompt, confirm, popup = html_options.delete("prompt"), html_options.delete("confirm"), html_options.delete("popup")
method, href = html_options.delete("method"), html_options['href']

html_options["onclick"] = case
when popup && method
raise ActionView::ActionViewError, "You can't use :popup and :method in the same link"
when confirm && popup
"if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;"
when confirm && method
"if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method)} };return false;"
when confirm
"return #{confirm_javascript_function(confirm)};"
when prompt
method ||= :post
"#{prompt_javascript_function(prompt)}"+
"if(prompt_reply) { #{send_prompt_javascript_function(method, url, href)} }; return false;"
when method
"#{method_javascript_function(method, url, href)}return false;"
when popup
"#{popup_javascript_function(popup)} return false;"
else
html_options["onclick"]
end
end

def prompt_javascript_function(prompt)
"var prompt_reply = prompt('#{escape_javascript(prompt)}');"
end

def send_prompt_javascript_function(method, url = '', href = nil)
action = (href && url.size > 0) ? "'#{url}'" : 'this.href'
submit_function =
"var f = document.createElement('form'); f.style.display = 'none'; " +
"this.parentNode.appendChild(f); f.method = 'POST'; f.action = #{action}; "+
"var p = document.createElement('input'); p.setAttribute('type', 'hidden'); p.setAttribute('name', 'prompt_reply'); p.setAttribute('value', prompt_reply); f.appendChild(p);"

unless method == :post
submit_function << "var m = document.createElement('input'); m.setAttribute('type', 'hidden'); "
submit_function << "m.setAttribute('name', '_method'); m.setAttribute('value', '#{method}'); f.appendChild(m);"
end

if protect_against_forgery?
submit_function << "var s = document.createElement('input'); s.setAttribute('type', 'hidden'); "
submit_function << "s.setAttribute('name', '#{request_forgery_protection_token}'); s.setAttribute('value', '#{escape_javascript form_authenticity_token}'); f.appendChild(s);"
end

submit_function << "f.submit();"
end



end
end
end

7 comments:

  1. I could have used this back when I created a work log program for my old company. I instead created a different page for just that action, fortunately I only had a couple of users that needed to be canceled in that time, so it didn't matter too much.

    ReplyDelete
  2. nice work, very helpful.

    ReplyDelete
  3. Thanks for taking the time to do this and for making it available to others!! Inspires me to start doing the same.

    ReplyDelete
  4. link_to_remote option:

    I found a bunch of sites talking about using ":with =>" to pass along a prompt request to the link_to_remote prototype helper, but it would still trigger the ajax call whether the field was filled in or not, even if the cancel button was pressed?!? Not good. Using your example as inspiration, I put this in my environment.rb file to override the prototype_helper remote_function method, adding a proper prompt. If I have the time/energy, I'll turn it into my first plugin.

    (sorry for the format. Not sure your comments allow nice formatted colored code)

    module ActionView
    module Helpers
    module PrototypeHelper
    def remote_function(options)
    javascript_options = options_for_ajax(options)

    update = ''
    if options[:update] && options[:update].is_a?(Hash)
    update = []
    update << "success:'#{options[:update][:success]}'" if options[:update][:success]
    update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure]
    update = '{' + update.join(',') + '}'
    elsif options[:update]
    update << "'#{options[:update]}'"
    end

    function = update.empty? ?
    "new Ajax.Request(" :
    "new Ajax.Updater(#{update}, "

    url_options = options[:url]
    url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash)
    function << "'#{escape_javascript(url_for(url_options))}'"
    function << ", #{javascript_options})"

    function = "#{options[:before]}; #{function}" if options[:before]
    function = "#{function}; #{options[:after]}" if options[:after]
    function = "if (#{options[:condition]}) { #{function}; }" if options[:condition]
    function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm]
    function = "var prompt_reply = prompt('#{escape_javascript(options[:prompt])}');if(prompt_reply) { #{function}; }" if options[:prompt]

    return function
    end
    end
    end
    end


    You will need to add the :with option in the view, as in:
    :with => "'your_named_variable_here=' + (prompt_reply)"

    cancel prompt = no ajax call
    ok button clicked, but blank field = no ajax call
    something in the text field and click ok = ajax call along with your_named_variable = text field value.

    Thanks again for your post! I never would have figured out where in rails to change it, nor how to simply override it in environment.rb.

    ReplyDelete
  5. excellent work randy..
    i'm happy to have helped.. even if my only function was acts_as_signpost..

    plugin-izing your code is a pretty straightforward process.. if you grabbed this project from github and spend just a few seconds looking at the code you should see just how simple

    ReplyDelete
  6. I created a simple plugin using script/generate plugin right after getting it working. Now I'm looking into creating a github account so I can easily include it in future rails projects via the new templates functionality. Will post back here when available.

    ReplyDelete
  7. git://github.com/randyinla/link_to_remote_with_prompt.git

    -=Cheers!

    ReplyDelete