jQuery Delete Link With Downlevel Support

asp.net mvc, asp.net 0 comments suggest edit

Earlier this morning, I posted on making a simple jQuery delete link which makes it easy to create a delete link that does a form post to a delete action. Commenters pointed out that my solution won’t work for down-level browsers such as some mobile phones, and they were right. I wasn’t really concerned about down-level browsers.

One solution for down-level browsers is to render a proper form with a submit button, and then hide the form with JavaScript. Of course this takes a bit more work. Here’s what I did. I made sure I had the following script in my master template.

<script type="text/javascript">
 $("form.delete-link").css("display", "none");
 $("a.delete-link").show();
 $("a.delete-link").live('click', function(ev) {
    ev.preventDefault(); 
    $("form.delete-link").submit(); 
 });
</script>

When the following HTML is rendered in the page…

<form method="post" action="/go/delete/1" 
  class="delete-link">
  <input type="submit" value="delete" />
  <input name="__RequestVerificationToken" type="hidden" 
  value="Jrcn83M7T...8Z6RkdIfMZIJ5mVb" />
</form>
<a class="delete-link" href="/go/delete/1" 
  style="display:none;">delete</a>

… the jQuery code shown above will hide the form, but display the link (notice the link is hidden by default). When the link is clicked, it posts the form. However, in cases where there is no JavaScript, the form will be displayed, but the link will not be because the JavaScript is the thing that hides the form.

To make this easier to use, I wrote the following helper:

public static string DeleteLink(this HtmlHelper html
  , string linkText
  , string routeName
  , object routeValues) {
  var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
  string url = urlHelper.RouteUrl(routeName, routeValues);

  string format = @"<form method=""post"" action=""{0}"" 
  class=""delete-link"">
<input type=""submit"" value=""{1}"" />
{2}
</form>";

  string form = string.Format(format, html.AttributeEncode(url)
    , html.AttributeEncode(linkText)
    , html.AntiForgeryToken());
  return form + html.RouteLink(linkText, routeName, routeValues
  , new { @class = "delete-link", style = "display:none;" });
}

Notice that we’re using the AntiForgery helpers included with ASP.NET MVC. What this means is that I need to make one small change to my delete method on my controller. I need to add the ValidateAntiForgeryToken attribute to the method.

[ValidateAntiForgeryToken]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id) {
  //Delete it
}

I’ve left out a bit. For example, I didn’t specify a callback to the jQuery code. So what should happen when this action method returns? I leave that as an exercise to the reader. I may address it in a future follow-up to this blog post. In my code, I’m just being cheesy and doing a full redirect, which works fine.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

22 responses

  1. Avatar for Ricky
    Ricky January 30th, 2009

    Does this really work? From what I can tell $.post(this.href); is just going to do an AJAX Post and the user will think nothing has happened (the page will not reload). What I would do here is make the form submit on click of the link. So something like this.
    HTML:

    <form id="delete-form" method="post" action="/go/delete/1" class="delete-link">
    <input type="submit" value="delete" />
    </form>
    <a href="/go/delete/1">delete

    JavaScript:

    $("a.delete-link").live('click', function(ev) {
    ev.preventDefault();
    $('form#' + $(this).attr('rel')).submit();
    });


    Make sense? Sorry to keep bugging you :)

  2. Avatar for Ricky
    Ricky January 30th, 2009

    crap, the link rendered... meant this:

    &lt;form id="delete-form" method="post" action="/go/delete/1" class="delete-link"&gt;
    &lt;input type="submit" value="delete" /&gt;
    &lt;/form&gt;
    &lt;a rel="delete-form" class="delete-link" href="/go/delete/1"
    style="display:none;"&gt;delete&lt;/a&gt;

  3. Avatar for Kazi Manzur Rashid
    Kazi Manzur Rashid January 30th, 2009

    This is cool, seems you are using jq 1.3?

  4. Avatar for haacked
    haacked January 30th, 2009

    @Ricky I updated the post since you last commented. Note that I'm now submitting the original form (yes, I tested this). That allows me to use the anti-forgery helpers, like you suggested. Thanks for the feedback!

  5. Avatar for Rick
    Rick January 30th, 2009

    How about this for down-level (including non-css!)
    JavaScript:

    $(function() {
    $("form.post-link").each(replaceButtonsInForm);
    });
    function replaceButtonsInForm() {
    var form = $(this);
    $(":submit", form).each(function() {
    var button = $(this);
    var text = button.val();
    var link = $("<a href=\"#\">").text(text).click(function(ev) { ev.preventDefault(); form.submit(); });
    button.replaceWith(link);
    return link;
    });
    }

    HTML:

    <form method="post" action="delete.ashx" class="post-link"><input type="submit" value="Delete" /></form>

  6. Avatar for Rick
    Rick January 30th, 2009

    Ummm, except for the bad habit of using $(this) inside the replaceButtonsInForm method.

  7. Avatar for Elijah Manor
    Elijah Manor January 30th, 2009

    Very nice! I usually don't have to work about down-level browsers at work because we defined a white & black list of browsers to support & not support... also required JavaScript :)
    Thanks for the pointers. Will definitely come in handy when I need to support those scenarios!
    In either case, I do need to make sure my deletes and modifies are post only operations!
    http://twitter.com/elijahmanor
    http://zi.ma/webdev

  8. Avatar for Steve
    Steve January 30th, 2009

    It's a bit trickier than this, assuming you're using ".live()" because of its AJAX-tolerant benefits. (If not you could just use .click() like you did in previous example.) The script will run once and hide the form, but later AJAX updates might insert a form that doesn't get hidden.
    Instead, you should use CSS to hide the form. A one-time call to add a CSS class "js-enabled" to the body will let you put in styles like:
    a.delete-link { display: none; }
    .js-enabled a.delete-link { display:inline; }
    and the benefit is that it's automatically applied to any DOM manipulations that happen later.
    I'd also suggest putting this inside a $(document).ready() rather than just directly embedding as script (shortcut: $(function() { ... }); will do the same thing.) But maybe that's what you intended and it's not a big deal.

  9. Avatar for Steve
    Steve January 30th, 2009

    (forgot one style:
    .js-enabled form.delete-link { display: none; }
    )

  10. Avatar for haacked
    haacked January 30th, 2009

    @Steve thanks for the tips. I need to spend some time getting this production worthy and revisit. Maybe later. ;)

  11. Avatar for Brian J. Cardiff
    Brian J. Cardiff January 30th, 2009

    One problem this solution is regarding nested FORM tags. Not all browser and not all jquery plugins (e.g.: validation) play nice with nested FORMs. One alternative would be to delay the render of those helpers forms until a safe zone in the markup is reached.
    I should double check all the [delete] links in my web apps ;-).

  12. Avatar for Patrik Akselsson
    Patrik Akselsson January 30th, 2009

    Why do you want a link to perform a delete operation in the first place? Doesn't that go against the semantics of html? An alternative solution would be to make GET /go/delete/1 return a page with an "Are you sure"-form that POSTs to /go/delete/1.
    Much simpler imho, and you won't have to try to add fudge to accomodate non-javascript browsers

  13. Avatar for Duncan Smart
    Duncan Smart January 31st, 2009

    "...down-level browsers" no no no, it's not about that. Progressive enhancement is about satisfying (typically) governmental organisations that require WCAG compliance (for the blind and other disabilities).

  14. Avatar for Mark
    Mark February 2nd, 2009

    Still think css is the way to go...

    <form method="post" action="/go/delete/1"
    class="delete-link">
    <input type="submit" class="delete" value="delete" />
    <input name="__RequestVerificationToken" type="hidden"
    value="Jrcn83M7T...8Z6RkdIfMZIJ5mVb" />
    </form>
    <style >
    input.delete { border: none; background: none; color: Blue; padding: 0; margin: 0; cursor: pointer; }
    </style>

  15. Avatar for selaromdotnet
    selaromdotnet February 4th, 2009

    couldn't you make it a post for javascript-enabled browsers (using the technique in your previous entry) then also make a controller action for GET (that intercepts the standard link click non-javascript browsers) that displays a confirmation page with a standard form submit button, POSTing to the same page (this time invoking the POST method) to delete that object?
    It will require an extra step sure, but only for browsers that don't use javascript. Seems like a clean, safe solution...
    I don't know enought about mvc to know if this is a viable solution (can you have the same method name, overloaded for POST and GET?) but it seems logical to me!

  16. Avatar for Erik van Brakel
    Erik van Brakel February 10th, 2009

    @SelArom: No, you can't have the same method signature twice, regardless of the attributes. Normal C# rules still apply in MVC, there's no magic going on in the code as far as that's concerned.
    I actually implemented what you suggested on my pet project this week, seeing how you mention it, I think I'll blog about it (might attract some people :P)

  17. Avatar for Erik van Brakel
    Erik van Brakel February 11th, 2009
  18. Avatar for sagar
    sagar December 17th, 2009

    Online art gallery for contemporary artists, painters, sculptors, and those devoted to art photography, traditional art, digital art, video art, animation, poetry, prose, music.

  19. Avatar for John Smith
    John Smith January 10th, 2010

    Visit BerenjiLaw to learn about the finest and most professional providers of personal injury lawyer services in the entire Los Angeles. We provide the best legal assistance in the whole California state!

  20. Avatar for Oliver
    Oliver May 7th, 2010

    Hi Phil,
    I am trying to create a form with java and also include the AntiForgeryToken within the from. The blow code is what I am trying to generate. I liked your helper class that injects the AntiForgeryToken into the from... can something similar be done to produce the output below?
    Thank you,
    Oliver
    onclick="var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;var s = document.createElement('input'); s.setAttribute('type', 'hidden'); s.setAttribute('name', '__RequestVerificationToken'); s.setAttribute('value', 'NTcwdh77hWox5dGULzvutUDMqp+3ma+NmsHRGliq6XL4j7jyKb6oV8wu+AgqQsBqo5tF4y6fEgHXDDEiboFy7Q=='); f.appendChild(s);f.submit();return false;"

  21. Avatar for Kyle Bailey
    Kyle Bailey September 8th, 2010

    I think that this is a very serious approach to a not so common problem. If you're going to be implementing this type of scenario on your website then you should go all the way and ACTUALLY check that someone is authorized to delete what they are deleting.

  22. Avatar for Lorenzo Melato
    Lorenzo Melato November 17th, 2010

    I think that the javascript you post have a little (big!) side-effect, if you do this:
    $("form.delete-link").submit();
    than all the forms with delete-link class will submit.
    If you have a grid of records and you want to have a delete link for each row, when you click a delete link you will submit all the forms in the page, with unexpected results.
    You can change the code in this manner:
    $("form.delete-link").css("display", "none");
    $("a.delete-link").show();
    $("a.delete-link").live('click', function (ev) {
    ev.preventDefault();
    var result = confirm("Sure you want to delete this row?");
    if (result) {
    $(this).prev("form.delete-link").submit();
    }
    });
    So, instead of submit all the "form.delete-link" forms, you will submit only the first "form.delete-link" previous to the "a.delete-link" (that is the related form).
    What you think about?
    Lorenzo

    <script type="text/javascript">
    $("form.delete-link").css("display", "none");
    $("a.delete-link").show();
    $("a.delete-link").live('click', function(ev) {
    ev.preventDefault();
    $("form.delete-link").submit();
    });
    </script>