jQuery UI Dialog instead of alert() for Rails 3 data-confirm attribute
Asked Answered
M

6

14

In Rails 3, passing a :confirm parameter to link_to will populate the data-confirm attribute of the link. This will induce a JS alert() when the link is clicked.

I am using the rails jQuery UJS adapter (https://github.com/rails/jquery-ujs). The relevant code from rails.js is:

$('body').delegate('a[data-confirm], button[data-confirm], input[data-confirm]', 'click.rails', function () {
    var el = $(this);
    if (el.triggerAndReturn('confirm')) {
        if (!confirm(el.attr('data-confirm'))) {
            return false;
        }
    }
});

and

triggerAndReturn: function (name, data) {
        var event = new $.Event(name);
        this.trigger(event, data);

        return event.result !== false;
    }

I would like to know how this could be modified to instead yield a jQuery dialog (e.g. the jQuery UI Dialog) allowing the user to confirm or cancel.

My knowledge of JavaScript isn't sufficient to achieve this elegantly. My current approach would be to simply rewrite the $('body').delegate() function to instead instantiate a lightbox. However I imagine that there is a more effective approach than this.

Moradabad answered 12/12, 2010 at 9:49 Comment(2)
The whole point of :confirm is to ask the user a question with a yes/no answer. AFAIK, lightbox does not have a yes/no response option. How about using a jQuery dialog instead?Jabot
You're right. A dialog would be more appropriate, such as jqueryui.com/demos/dialog/#modal-confirmation. I will edit the question.Moradabad
G
14

I just added an external API to the Rails jquery-ujs for exactly this kind of customization. You can now make rails.js use a custom confirm dialog by plugging into (and re-writing 1 line of) the $.rails.allowAction function.

See my article, Rails jQuery UJS: Now Interactive, for a full explanation with examples.

EDIT: As of this commit, I moved the confirm dialog function to the $.rails object, so that it can be modified or swapped out even more easily now. E.g.

$.rails.confirm = function(message) { return myConfirmDialog(message); };
Garnishee answered 26/4, 2011 at 15:33 Comment(4)
This is what I was looking for, thank you. Thank you to everybody else who provided an answer - however I wasn't particularly satisfied with any approach that involved modifying rails.js. Redefining jquery-ujs functions via $.rails is definitely the way to go.Moradabad
FYI, I just moved the confirm dialog function directly to the $.rails object, so that it's now even easier to use your own confirm dialog function. See updated answer.Garnishee
How exactly do you return the JQuery UI modal dialog answer? I am unable to get this working.Exophthalmos
I'm not sure I can provide a good answer. The main problem is that, for a confirm dialog to work properly, it must be model (i.e. synchronously blocking), and provide a true/false answer. jQuery UI's "modal dialog" does neither. Because of this, it cannot just be dropped in like other js confirm replacements.Garnishee
P
16

As others have mentioned, you cannot use a jQuery dialog box, as $.rails.confirm needs to block until it returns the users answer.

However, you can overwrite $.rails.allowAction in your application.js file like this:

$.rails.allowAction = function(element) {
        var message = element.data('confirm'),
        answer = false, callback;
        if (!message) { return true; }

        if ($.rails.fire(element, 'confirm')) {
                myCustomConfirmBox(message, function() {
                        callback = $.rails.fire(element,
                                'confirm:complete', [answer]);
                        if(callback) {
                                var oldAllowAction = $.rails.allowAction;
                                $.rails.allowAction = function() { return true; };
                                element.trigger('click');
                                $.rails.allowAction = oldAllowAction;
                        }
                });
        }
        return false;
}

function myCustomConfirmBox(message, callback) {
        // implement your own confirm box here
        // call callback() if the user says yes
}

It works by returning false immediately, thus effectively canceling the click event. However, your custom function can then call the callback to actually follow the link/submit the form.

Prescription answered 24/6, 2011 at 12:4 Comment(5)
Thanks for the help Marc. I've tried implementing your suggestion and it's gotten me farther than any other advice so far, however, I'm still not there yet. This is the code I have in my application.js. I'm still unsure about how the answer gets transferred back to the callback. Also, I don't understand the setting and unsetting of $.rails.allowAction in if(callback). I would be very grateful if you could explain. EDIT: I couldn't fit the code here but please contact me if you have time. Thanks much.Hypochondriac
For what it's worth, I've made a fork of jquery-ujs which re-implements $.rails.confirm using asynchronous callbacks. It seems to work for several people, but it relies on some undocumented apis in jQuery, so I wouldn't want to pull it into core unless we had a lot of people actively using it for a while, just to be sure there aren't any weird side effects. github.com/JangoSteve/jquery-ujs/tree/async-confirmGarnishee
This is mind blowing to me that rails doesn't support this out of the box. This is such a common use case these days! @Garnishee were you using undocumented apis as of 2 years ago? I don't see much in that PR that looks crazy. As a side note, this fix is kind of janky but I feel like it's better than using a PR that isn't really endorsed by the maintainer/author of the repo...Frederiksen
This works brilliantly. For some reason, all the examples with trigger('click.rails') did not work for me..Bareback
Thanks for the answer Marc, could you show how you implement the confirm box there please? function myCustomConfirmBox(message, callback) { // implement your own confirm box here // call callback() if the user says yes }Correspondence
G
14

I just added an external API to the Rails jquery-ujs for exactly this kind of customization. You can now make rails.js use a custom confirm dialog by plugging into (and re-writing 1 line of) the $.rails.allowAction function.

See my article, Rails jQuery UJS: Now Interactive, for a full explanation with examples.

EDIT: As of this commit, I moved the confirm dialog function to the $.rails object, so that it can be modified or swapped out even more easily now. E.g.

$.rails.confirm = function(message) { return myConfirmDialog(message); };
Garnishee answered 26/4, 2011 at 15:33 Comment(4)
This is what I was looking for, thank you. Thank you to everybody else who provided an answer - however I wasn't particularly satisfied with any approach that involved modifying rails.js. Redefining jquery-ujs functions via $.rails is definitely the way to go.Moradabad
FYI, I just moved the confirm dialog function directly to the $.rails object, so that it's now even easier to use your own confirm dialog function. See updated answer.Garnishee
How exactly do you return the JQuery UI modal dialog answer? I am unable to get this working.Exophthalmos
I'm not sure I can provide a good answer. The main problem is that, for a confirm dialog to work properly, it must be model (i.e. synchronously blocking), and provide a true/false answer. jQuery UI's "modal dialog" does neither. Because of this, it cannot just be dropped in like other js confirm replacements.Garnishee
F
1

I liked the answer from @Marc Schütz about overriding $.rails.allowAction the most of anything I found online - but I'm not a big fan of overriding the functionality in allowAction since it's used all throughout the jquery-ujs codebase (what if there are side effects? Or if the source for that method changes in a future update?).

By far, the best approach would be to make $.rails.confirm return a promise... But it doesn't look like that's going to happen anytime soon :(

So... I rolled my own method which I think is worth mentioning because it's lighter weight than the method outlined above. It doesn't hijack allowAction. Here it is:

# Nuke the default confirmation dialog. Always return true 
# since we don't want it blocking our custom modal.
$.rails.confirm = (message) -> true

# Hook into any data-confirm elements and pop a custom modal
$(document).on 'confirm', '[data-confirm]', ->
  if !$(this).data('confirmed')
    myCustomModal 'Are you sure?', $(this).data('confirm'), =>
      $(this).data('confirmed', true)
      $(this).trigger('click.rails')
    false
  else
    true

# myCustomModal is a function that takes (title, message, confirmCallback)

How does it work? Well, if you look at the source, you'll notice that the allowAction method halts if the confirm event returns a falsy value. So the flow is:

  1. User clicks link or button with data-confirm attribute. There is no data-confirmed present on the link or button, so we fall into the first if block, trigger our custom modal and return false, thereby stopping the action from continuing in the ujs click handler.
  2. User confirms in the custom modal, and the callback is triggered. We store state on the element via data('confirmed', true) and re-trigger the same event that was triggered previously (click.rails).
  3. This time the confirm event will fall into the else block (since data('confirmed') is truthy) and return true, causing the allowAction block to evaluate to true.

I'm sure I'm even missing other ways that might make this even simpler, but I think this is a really flexible approach to get a custom confirm modal without breaking core jquery-ujs functionality.

(Also, because we're using .on() this will bind to any data-confirm elements on the page at load time or in the future, similarly to how .delegate() works, in case you are wondering.)

Frederiksen answered 29/7, 2015 at 6:45 Comment(1)
You helped me a lot. But $(this).trigger('click.rails') don't work. I replaced this code to click on DOM element.Thionic
E
0

I don't understand why you need to use the jQuery dialog when the JavaScript confirm() function will still work just fine. I would do something like this:

$('a[data-confirm]').click(funciton() {
  confirm($(this).data("confirm"));
});

If you want to use a dialog instead, it's a little different. You can one-off each dialog you want, or you can probably take a uniform approach application wide so that your rails.js or your application.js can handle any dialog instance. For example, you'd need something like this on your page:

<a class="dialogLauncher">The link that creates your dialog</a>
<div class="dialog" title="My confirmation title" style="display:none">
  <p>My confirmation message</p>
</div>

Then, in your js:

$('.dialogLauncher').click(function() {
  var dialog = $(this).next('.dialog');
  dialog.dialog();
})

If you want to customize your dialog a little more, check out this example.

Edit

Now that I think of it, this would be a good opportunity for a custom form builder. You could override one of your Rails link tags to output html similar to what's listed above whenever a certain attribute is present, i.e. :dialog => true. Surely that would be the Railsy way to do it. You could add other options into your tag as well, like the dialog title, etc.

Edit

Better yet, instead of :dialog => true, use :confirm => "my confirm message" just as you would normally, but in your override of link_to, you will use the :confirm option to create the dialog html that jQuery needs, delete that option, and then call super.

Eutrophic answered 13/12, 2010 at 17:32 Comment(1)
I don't need to use a jQuery UI dialog - confirm() function will work fine, you are correct. The purpose is purely aesthetic.Moradabad
S
0

This is how I got it to work. Please suggest any corrections / improvements

#

in rails.js

#
// Added new variable
var deleteConfirmed = false;

// Changed function to use jquery dialog instead of confirm   
$('body').delegate('a[data-confirm], button[data-confirm], input[data-confirm]', 'click.rails', function () {
        var el = $(this);
        /*
        if (el.triggerAndReturn('confirm')) {

            if (!confirm(el.attr('data-confirm'))) {
                return false;
            }

        }
        */

        if (el.triggerAndReturn('confirm')) {    

            if(deleteConfirmed) {
                deleteConfirmed = false;
                return true;
            }

            $( "#dialog-confirm" ).dialog("option", "buttons",
                    {
                        "Delete": function() {
                            $( this ).dialog( "close" );
                            deleteConfirmed = true;
                            el.trigger('click');   
                            return true;
                        },
                        Cancel: function() {
                            $( this ).dialog( "close" );
                            return false;
                        }
                    }
            );

            $( "#dialog-confirm" ).dialog("open");

            return false;

        }


    });
#

in application.js

#
//Ensure confirm Dialog is pre-created
jQuery(function () {


    $( "#dialog-confirm" ).dialog({
        autoOpen: false,
        resizable: false,
        height:140,
        modal: true     
    });

});
#

in layout.html Alt you can place this div anywhere in your generated html

#
        <div id='dialog-confirm' title='Confirm Delete'> 
          <p> 
            <span class='ui-icon-alert' style='float:left; margin:0 7px 20px 0;'> 
              This item will be permanently deleted. Are you sure?
            </span> 
          </p> 
        </div> 
Stuffy answered 17/2, 2011 at 19:2 Comment(0)
T
0

This is how I solved this problem. I tried a lot of different ways, but only this one works.

In rails.js

function myCustomConfirmBox(element, callback) {

    const modalConfirmDestroy = document.getElementById('modal-confirm');

    // wire up cancel
    $("#modal-confirm #cancel-delete").click(function (e) {
        e.preventDefault();
        modalConfirmDestroy.classList.remove('modal--open');
    });

    // wire up OK button.
    $("#modal-confirm #confirm-delete").click(function (e) {
        e.preventDefault();
        modalConfirmDestroy.classList.remove('modal--open');
        callback(element, true);
    });

    // show the dialog.
    modalConfirmDestroy.classList.add('modal--open');
}

In this place I used code of @Mark G. with some changes. Because this $(this).trigger('click.rails') snipped of the code didn't work for me.

$.rails.confirm = function(message) {return true};

$(document).on('confirm', '[data-confirm]', (event)=> {
    if (!$(this).data('confirmed'))
    {
        myCustomConfirmBox($(this), (element, choice)=> {
            element.data('confirmed', choice);
            let clickedElement = document.getElementById(event.target.id);
            clickedElement.click();
        });
        return false;
    }
    else
    {
        return true;
    }
});

Then in the html.erb file I have this code for link:

<%= link_to "documents/#{document.id}", method: "delete", data: {confirm: "sure?"}, id: "document_#{document.id}" %>

and this code for modal:

<div id="modal-confirm" class="modal modal-confirm">
  <h2 class="modal__ttl">Title</h2>
  <div class="modal__inner">
    <p>Description</p>
    <div class="modal__btns">
      <button type="button" name="cancel" id="cancel-delete" class="btn btn-primary">Cancel</button>
      <button type="button" name="confirm" id="confirm-delete" class="btn delete_button btn-secondary">Delete</button>
    </div>
  </div>
</div>

I hope, it will help someone.

Thionic answered 28/12, 2018 at 14:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.