Method: :delete is not working in specific places of my app

When I use the link tag of Phoenix and make a “delete” link, it works fine if this code is on its own (see below first example) but once I put it inside of a dropdown, the method: :delete is ignored and the link is turned into a GET request (instead of DELETE). This obviously does not work for logging out as the only accepted route is a DELETE request. To demonstrate the difference (I can’t upload photos yet as I’m a new user): https://vpn.toruda.nl/index.php/s/gwNrDqqtnatLGy2/preview

This works (red circled button):

<div class="ml-auto">
  <%= link to: Routes.user_session_path(@conn, :delete), method: :delete do %>
    <i class="nova-power-off"></i>
  <% end %>
</div>

This does not work (blue circled button)

<div class="dropdown mx-3">
  <a id="profileMenuInvoker" class="header-complex-invoker" href="#" aria-controls="profileMenu" aria-haspopup="true" aria-expanded="false"
      data-unfold-event="click"
      data-unfold-target="#profileMenu"
      data-unfold-type="css-animation"
      data-unfold-duration="300"
      data-unfold-animation-in="fadeIn"
      data-unfold-animation-out="fadeOut">
    <img class="avatar rounded-circle mr-md-2" src="<%= MyApp.AvatarUploader.url(@current_user.avatar, signed: true) %>" alt="Image description">
    <span class="d-none d-md-block"><%= @current_user.email %></span>
    <i class="nova-angle-down d-none d-md-block ml-2"></i>
  </a>

  <ul id="profileMenu" class="unfold unfold-user unfold-light unfold-top unfold-centered position-absolute pt-2 pb-1 mt-4" aria-labelledby="profileMenuInvoker">
    <li class="unfold-item">
      <%= link to: Routes.user_settings_path(@conn, :edit), class: "unfold-link d-flex align-items-center text-nowrap" do %>
        <span class="unfold-item-icon mr-3">
          <i class="nova-user"></i>
        </span>
        My Profile
      <% end %>
    </li>

    <li class="unfold-item unfold-item-has-divider">
      <%= link to: Routes.user_session_path(@conn, :delete), method: :delete, class: "unfold-link d-flex align-items-center text-nowrap" do %>
      <span class="unfold-item-icon mr-3">
        <i class="nova-power-off"></i>
      </span>
      Sign Out
      <% end %>
    </li>
  </ul>
</div>

The red-circled link looks like this when I inspect the HTML:

<a data-csrf="EwUScD42AyQ0ejZ9JzgCKlNVVVU9QAQ8QNTGSyMPx6X4VNKA4d9bZqkR" data-method="delete" data-to="/users/log_out" href="/users/log_out" rel="nofollow">
    <i class="nova-power-off"></i>
</a>

and the blue link looks like this:

<a class="unfold-link d-flex align-items-center text-nowrap" data-csrf="EwUScD42AyQ0ejZ9JzgCKlNVVVU9QAQ8QNTGSyMPx6X4VNKA4d9bZqkR" data-method="delete" data-to="/users/log_out" href="/users/log_out" rel="nofollow">
      <span class="unfold-item-icon mr-3">
        <i class="nova-power-off"></i>
      </span>
      Sign Out
</a>

(to clarify, I also did try to copy exactly the red button into the space of the blue one, without applying any of the formattings and that still didn’t work)

So these look identical to me but for some reason, the red button works just fine, and the blue button is turned into a GET request once I click on it.

I’ve been looking into this and there are similar threads (Method: :delete is not working in multiple locations in my phoenix app and Phoenix Link with other method than GET not working) but this does not seem to be my issue as for me it’s only in certain places that it doesn’t work whereas in their case (and having the node modules rebuild) the delete method didn’t work anywhere in their app.

Can you post the JS code that controls your dropdown? This is where phoenix_html.js listens & handles the DELETE click events. My hunch is that your dropdown JS code stops propagation of those events, thus never reaching Phoenix’s code.

Okay, that must be the problem. I’ve only just started with software development and Elixir is my first language so I would have no clue on how to fix this in the javascript part. The dropdown came as part of a template so it’s rather extensive.

Summary
/**
 * Unfold component.
 *
 * @author Htmlstream
 * @version 1.0
 */

;(function ($) {
  'use strict';

  $.HSCore.components.HSUnfold = {

    /**
     * Base configuration of the component.
     *
     * @private
     */
    _baseConfig: {
      unfoldEvent: 'click',
      unfoldType: 'simple',
      unfoldDuration: 300,
      unfoldEasing: 'linear',
      unfoldAnimationIn: 'fadeIn',
      unfoldAnimationOut: 'fadeOut',
      unfoldHideOnScroll: true,
      unfoldHideOnBlur: false,
      unfoldDelay: 350,
      afterOpen: function (invoker) {},
      afterClose: function (invoker) {}
    },

    /**
     * Collection of all initialized items on the page.
     *
     * @private
     */
    _pageCollection: $(),

    /**
     * Initialization.
     *
     * @param {jQuery} collection
     * @param {Object} config
     *
     * @public
     * @return {jQuery}
     */
    init: function (collection, config) {

      var self;

      if (!collection || !collection.length) return;

      self = this;

      var fieldsQty,
        items = [];

      collection.each(function (i, el) {

        var $this = $(el), itemConfig;

        if ($this.data('HSUnfold')) return;

        itemConfig = config && $.isPlainObject(config) ?
          $.extend(true, {}, self._baseConfig, config, $this.data()) :
          $.extend(true, {}, self._baseConfig, $this.data());

        switch (itemConfig.unfoldType) {

          case 'css-animation' :

            $this.data('HSUnfold', new UnfoldCSSAnimation($this, itemConfig));

            break;

          case 'jquery-slide' :

            $this.data('HSUnfold', new UnfoldJSlide($this, itemConfig));

            break;

          default :

            $this.data('HSUnfold', new UnfoldSimple($this, itemConfig));

        }

        self._pageCollection = self._pageCollection.add($this);
        self._bindEvents($this, itemConfig.unfoldEvent, itemConfig.unfoldDelay);
        var Unfold = $(el).data('HSUnfold');

        fieldsQty = $(Unfold.target).find('input, textarea').length;

      });

      var items,
        index = 0;

      $(document).on('keydown.HSUnfold', function (e) {

        // if (!$('[aria-expanded="true"]').length) return;

        if (e.keyCode && e.keyCode === 27) {

          self._pageCollection.each(function (i, el) {

            var windW = $(window).width(),
              optIsMobileOnly = Boolean($(el).data('is-mobile-only'));

            items = $($($(el).data('unfold-target')).children());

            if (!optIsMobileOnly) {
              $(el).data('HSUnfold').hide();
            } else if (optIsMobileOnly && windW < 769) {
              $(el).data('HSUnfold').hide();
            }

          });

        }

        self._pageCollection.each(function (i, el) {
          if (!$($(el).data('unfold-target')).hasClass('unfold-hidden')) {
            items = $($($(el).data('unfold-target')).children());
          }
        });

        if (e.keyCode && e.keyCode === 38 || e.keyCode && e.keyCode === 40) {
          e.preventDefault();
        }

        if (e.keyCode && e.keyCode === 38 && index > 0) {
          // up
          index--;
        }

        if (e.keyCode && e.keyCode === 40 && index < items.length - 1) {
          // down
          index++;
        }

        if (index < 0) {
          index = 0;
        }

        if (e.keyCode && e.keyCode === 38 || e.keyCode && e.keyCode === 40) {
          $(items[index]).focus();
        }
      });

      $(window).on('click', function (e) {

        self._pageCollection.each(function (i, el) {

          var windW = $(window).width(),
            optIsMobileOnly = Boolean($(el).data('is-mobile-only'));

          if (!optIsMobileOnly) {
            $(el).data('HSUnfold').hide();
          } else if (optIsMobileOnly && windW < 769) {
            $(el).data('HSUnfold').hide();
          }

        });

      });

      self._pageCollection.each(function (i, el) {

        var target = $(el).data('HSUnfold').config.unfoldTarget;

        $(target).on('click', function (e) {

          e.stopPropagation();

        });

      });

      $(window).on('scroll.HSUnfold', function (e) {

        self._pageCollection.each(function (i, el) {

          var Unfold = $(el).data('HSUnfold');

          if (Unfold.getOption('unfoldHideOnScroll') && fieldsQty === 0) {

            Unfold.hide();

          } else if (Unfold.getOption('unfoldHideOnScroll') && !(/iPhone|iPad|iPod/i.test(navigator.userAgent))) {

            Unfold.hide();

          }

        });

      });

      $(window).on('resize.HSUnfold', function (e) {

        if (self._resizeTimeOutId) clearTimeout(self._resizeTimeOutId);

        self._resizeTimeOutId = setTimeout(function () {

          self._pageCollection.each(function (i, el) {

            var Unfold = $(el).data('HSUnfold');

            Unfold.smartPosition(Unfold.target);

          });

        }, 50);

      });

      return collection;

    },

    /**
     * Binds necessary events.
     *
     * @param {jQuery} $invoker
     * @param {String} eventType
     * @param {Number} delay
     * @private
     */
    _bindEvents: function ($invoker, eventType, delay) {

      var $unfold = $($invoker.data('unfold-target'));

      if (eventType === 'hover' && !_isTouch()) {

        $invoker.on('mouseenter.HSUnfold', function (e) {

          var $invoker = $(this),
            HSUnfold = $invoker.data('HSUnfold');

          if (!HSUnfold) return;

          if (HSUnfold.unfoldTimeOut) clearTimeout(HSUnfold.unfoldTimeOut);
          HSUnfold.show();

        })
          .on('mouseleave.HSUnfold', function (e) {

            var $invoker = $(this),
              HSUnfold = $invoker.data('HSUnfold');

            if (!HSUnfold) return;

            HSUnfold.unfoldTimeOut = setTimeout(function () {

              HSUnfold.hide();

            }, delay);

          });

        if ($unfold.length) {

          $unfold.on('mouseenter.HSUnfold', function (e) {

            var HSUnfold = $invoker.data('HSUnfold');

            if (HSUnfold.unfoldTimeOut) clearTimeout(HSUnfold.unfoldTimeOut);
            HSUnfold.show();

          })
            .on('mouseleave.HSUnfold', function (e) {

              var HSUnfold = $invoker.data('HSUnfold');

              HSUnfold.unfoldTimeOut = setTimeout(function () {
                HSUnfold.hide();
              }, delay);

            });
        }

      }
      else {

        $invoker.on('click.HSUnfold', function (e) {

          var $curInvoker = $(this);

          if (!$curInvoker.data('HSUnfold')) return;

          if ($('[data-unfold-target].active').length) {
            $('[data-unfold-target].active').data('HSUnfold').toggle();
          }

          $curInvoker.data('HSUnfold').toggle();

          $($($curInvoker.data('unfold-target')).children()[0]).trigger('focus');

          e.stopPropagation();
          e.preventDefault();

        });

      }

    }
  };

  function _isTouch() {
    return 'ontouchstart' in window;
  }

  /**
   * Abstract Unfold class.
   *
   * @param {jQuery} element
   * @param {Object} config
   * @abstract
   */
  function AbstractUnfold(element, config) {

    if (!element.length) return false;

    this.element = element;
    this.config = config;

    this.target = $(this.element.data('unfold-target'));

    this.allInvokers = $('[data-unfold-target="' + this.element.data('unfold-target') + '"]');

    this.toggle = function () {
      if (!this.target.length) return this;

      if (this.defaultState) {
        this.show();
      }
      else {
        this.hide();
      }

      return this;
    };

    this.smartPosition = function (target) {

      if (target.data('baseDirection')) {
        target.css(
          target.data('baseDirection').direction,
          target.data('baseDirection').value
        );
      }

      target.removeClass('unfold-reverse-y');

      var $w = $(window),
        styles = getComputedStyle(target.get(0)),
        direction = Math.abs(parseInt(styles.left, 10)) < 40 ? 'left' : 'right',
        targetOuterGeometry = target.offset();

      // horizontal axis
      if (direction === 'right') {

        if (!target.data('baseDirection')) target.data('baseDirection', {
          direction: 'right',
          value: parseInt(styles.right, 10)
        });

        if (targetOuterGeometry.left < 0) {

          target.css(
            'right',
            (parseInt(target.css('right'), 10) - (targetOuterGeometry.left - 10 )) * -1
          );

        }

      }
      else {

        if (!target.data('baseDirection')) target.data('baseDirection', {
          direction: 'left',
          value: parseInt(styles.left, 10)
        });

        if (targetOuterGeometry.left + target.outerWidth() > $w.width()) {

          target.css(
            'left',
            (parseInt(target.css('left'), 10) - (targetOuterGeometry.left + target.outerWidth() + 10 - $w.width()))
          );

        }

      }

      // vertical axis
      if (targetOuterGeometry.top + target.outerHeight() - $w.scrollTop() > $w.height()) {
        target.addClass('unfold-reverse-y');
      }

    };

    this.getOption = function (option) {
      return this.config[option] ? this.config[option] : null;
    };

    return true;
  }


  /**
   * UnfoldSimple constructor.
   *
   * @param {jQuery} element
   * @param {Object} config
   * @constructor
   */
  function UnfoldSimple(element, config) {
    if (!AbstractUnfold.call(this, element, config)) return;

    Object.defineProperty(this, 'defaultState', {
      get: function () {
        return this.target.hasClass('unfold-hidden');
      }
    });

    this.target.addClass('unfold-simple');

    this.hide();
  }

  /**
   * Shows Unfold.
   *
   * @public
   * @return {UnfoldSimple}
   */
  UnfoldSimple.prototype.show = function () {

    var activeEls = $(this)[0].config.unfoldTarget;

    $('[data-unfold-target="' + activeEls + '"]').addClass('active');

    this.smartPosition(this.target);

    this.target.removeClass('unfold-hidden');
    if (this.allInvokers.length) this.allInvokers.attr('aria-expanded', 'true');
    this.config.afterOpen.call(this.target, this.element);

    return this;
  }

  /**
   * Hides Unfold.
   *
   * @public
   * @return {UnfoldSimple}
   */
  UnfoldSimple.prototype.hide = function () {

    var activeEls = $(this)[0].config.unfoldTarget;

    $('[data-unfold-target="' + activeEls + '"]').removeClass('active');

    this.target.addClass('unfold-hidden');
    if (this.allInvokers.length) this.allInvokers.attr('aria-expanded', 'false');
    this.config.afterClose.call(this.target, this.element);

    return this;
  }

  /**
   * UnfoldCSSAnimation constructor.
   *
   * @param {jQuery} element
   * @param {Object} config
   * @constructor
   */
  function UnfoldCSSAnimation(element, config) {
    if (!AbstractUnfold.call(this, element, config)) return;

    var self = this;

    this.target
      .addClass('unfold-css-animation unfold-hidden')
      .css('animation-duration', self.config.unfoldDuration + 'ms');

    Object.defineProperty(this, 'defaultState', {
      get: function () {
        return this.target.hasClass('unfold-hidden');
      }
    });

    if (this.target.length) {

      this.target.on('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function (e) {

        if (self.target.hasClass(self.config.unfoldAnimationOut)) {
          self.target.removeClass(self.config.unfoldAnimationOut)
            .addClass('unfold-hidden');


          if (self.allInvokers.length) self.allInvokers.attr('aria-expanded', 'false');

          self.config.afterClose.call(self.target, self.element);
        }

        if (self.target.hasClass(self.config.unfoldAnimationIn)) {

          if (self.allInvokers.length) self.allInvokers.attr('aria-expanded', 'true');

          self.config.afterOpen.call(self.target, self.element);
        }

        e.preventDefault();
        e.stopPropagation();
      });

    }
  }

  /**
   * Shows Unfold.
   *
   * @public
   * @return {UnfoldCSSAnimation}
   */
  UnfoldCSSAnimation.prototype.show = function () {

    var activeEls = $(this)[0].config.unfoldTarget;

    $('[data-unfold-target="' + activeEls + '"]').addClass('active');

    this.smartPosition(this.target);

    this.target.removeClass('unfold-hidden')
      .removeClass(this.config.unfoldAnimationOut)
      .addClass(this.config.unfoldAnimationIn);

  }

  /**
   * Hides Unfold.
   *
   * @public
   * @return {UnfoldCSSAnimation}
   */
  UnfoldCSSAnimation.prototype.hide = function () {

    var activeEls = $(this)[0].config.unfoldTarget;

    $('[data-unfold-target="' + activeEls + '"]').removeClass('active');

    this.target.removeClass(this.config.unfoldAnimationIn)
      .addClass(this.config.unfoldAnimationOut);

  }

  /**
   * UnfoldSlide constructor.
   *
   * @param {jQuery} element
   * @param {Object} config
   * @constructor
   */
  function UnfoldJSlide(element, config) {
    if (!AbstractUnfold.call(this, element, config)) return;

    this.target.addClass('unfold-jquery-slide unfold-hidden').hide();

    Object.defineProperty(this, 'defaultState', {
      get: function () {
        return this.target.hasClass('unfold-hidden');
      }
    });
  }

  /**
   * Shows Unfold.
   *
   * @public
   * @return {UnfoldJSlide}
   */
  UnfoldJSlide.prototype.show = function () {

    var self = this;

    var activeEls = $(this)[0].config.unfoldTarget;

    $('[data-unfold-target="' + activeEls + '"]').addClass('active');

    this.smartPosition(this.target);

    this.target.removeClass('unfold-hidden').stop().slideDown({
      duration: self.config.unfoldDuration,
      easing: self.config.unfoldEasing,
      complete: function () {
        self.config.afterOpen.call(self.target, self.element);
      }
    });

  }

  /**
   * Hides Unfold.
   *
   * @public
   * @return {UnfoldJSlide}
   */
  UnfoldJSlide.prototype.hide = function () {

    var self = this;

    var activeEls = $(this)[0].config.unfoldTarget;

    $('[data-unfold-target="' + activeEls + '"]').removeClass('active');

    this.target.stop().slideUp({
      duration: self.config.unfoldDuration,
      easing: self.config.unfoldEasing,
      complete: function () {
        self.config.afterClose.call(self.target, self.element);
        self.target.addClass('unfold-hidden');
      }
    });

  }

})(jQuery);

Check the javascript console in your browser(Chrome is right click inspect, then click on console tab) and check for messages there. Every time I’ve had an issue with this I’ve found the problem in there.

The HSUnfold code doesn’t show how clicking on an element is transformed into a browser navigation, but my guess is that that code is directly manipulating window.location based on the link’s href.

That’s not going to work (it will always send a GET) with passing method to link, which relies on JS event bindings to transform a click into a form submission.

Found this in the JS code you’ve posted:

$(target).on('click', function (e) {
   e.stopPropagation();
});

Try to comment this out. If that doesn’t do it, there are two other instances where the same happens, but looked less likely to be the culprits. Now, even if this was the problem, by commenting portions of this code out, you’ve most likely introduced other bugs.

The way events work in Javascript is that they bubble up from the target element to all the outer containers up to the window element. Listening to an event closer to the target element gives you priority to act on said event (including to stop its propagation, thus never reaching the end of the chain).

I never use delete method on a href.
As I remember it’s not possible without js event. In general terms, you should avoid using delete in a href. There is some people that block the js execution for their browser.

So you can:

  • use another http verb (not a good idea IMO)
  • use button (should work as link except it accept method precision but create a button in your code. You can do nice things with icons etc)
  • use js to send a delete request (but you can have side effect if js is not allow)

Maybe there is other method. Personally I always use button system. Works everywhere for every browser.

I hope it help you

1 Like

This did the trick! And as far as I can see, it did not introduce a new bug. Thank you so much!