Home > Blockchain >  SVG data elements not updating until after page reload, so can't DELETE favorited item
SVG data elements not updating until after page reload, so can't DELETE favorited item

Time:10-03

In my Rails 7 app, I am building a parks directory app with 'favorites' functionality, where a user should be able to click a heart icon to save a park as a favorite, and click again to unsave the park as favorite.

But the DELETE action to unsave the favorite only works after refreshing the page. I've identified that data-unfavorite-url in the heart SVG doesn't get updated until after page refresh. So the DELETE request can't get sent to the correct favorited park endpoint.

How can I get the data-unfavorite-url endpoint to be updated without a page refresh?

app/controllers/api/favorites_controller.rb

# frozen_string_literal: true

module Api
  class FavoritesController < ApplicationController
    protect_from_forgery with: :null_session

    def create
      favorite = Favorite.create!(favorite_params)

      respond_to do |format|
        format.json do
          render json: favorite.to_json, status: :created
        end
      end
    end

    def destroy
      favorite = Favorite.find(params[:id])
      favorite.destroy!

      respond_to do |format|
        format.json do
          render json: favorite.to_json, status: 204
        end
      end
    end

    private

      def favorite_params
        params.permit(:user_id, :park_id)
      end
  end
end

app/javascript/controllers/favorites_controller.js

import { Controller } from '@hotwired/stimulus';
import axios from 'axios';

export default class extends Controller {

  favorite() {
    if (this.element.dataset.userLoggedIn === 'false' ) {
      return document.querySelector(".sign-in-link").click();
    }

    if (this.element.dataset.favorited === 'true') {
      axios.delete(this.element.dataset.unfavoriteUrl, {
        headers: {
          'ACCEPT': 'application/json'
        }
      })
      .then((response) => {
        this.element.dataset.favorited = 'false'
        this.element.setAttribute('fill', 'none');
        this.element.setAttribute('stroke', '#D3D3D3');
      });
    } else {
      axios.post(this.element.dataset.favoriteUrl, {
        user_id: this.element.dataset.userId,
        park_id: this.element.dataset.parkId
      }, {
        headers: {
          'ACCEPT': 'application/json'
        }
      })
      .then((response) => {
        this.element.dataset.favorited = 'true'
        this.element.setAttribute('fill', '#FF6962');
        this.element.setAttribute('stroke', 'none');
      });
    }
  }
}

app/views/parks/_park_cards.erb

<% @parks.each do |park| %>
  <li
    style="list-style-type: none;"
    data-geolocation-target="park"
    data-latitude="<%= park.latitude %>"
    data-longitude="<%= park.longitude %>"
  >
    <div >
      <div >
        <% if park.images.attached? %>
          <%= link_to image_tag(park.default_image, class: "card-img-top"), park %>
        <% else %>
          <%= link_to image_tag("image-placeholder-icon-6.jpg", class: "card-img-top", style: "height: 250px"), park %>
        <% end %>
        <h5 >
          <%= link_to park.name, park, class: "text-body" %>
          <div >
            <% if park.average_rating? %>
              <span ><svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 3 20 20" fill="currentColor" >
                                  <path fill-rule="evenodd" d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401z" clip-rule="evenodd" />
                                </svg>
              </span>
              <%= park.average_rating.round(2) %>
            <% end %>
          </div>
        </h5>
        <div>
          <div >
            <div>
              <%= park.region.humanize %> region |
                  <%= link_to "See map", park.google_maps_url, target: :_blank %>
            </div>
          </div>
          <div >
            <span data-distance-away>
              <br>
            </span>
            <div>
              <div >
                <svg
                  data-controller="favorites"
                  data-user-logged-in="<%= user_signed_in? %>"
                  data-user-id="<%= current_user&.id %>"
                  data-park-id="<%= park.id %>"
                  data-favorite-url="<%= api_favorites_path %>"
                  data-unfavorite-url="<%= api_favorite_path(current_user&.favorites.find_by(park: park)) if current_user && current_user.favorited_parks.include?(park) %>"
                  data-favorite-id="<%= current_user.favorites.find_by(park: park).id if current_user && current_user.favorited_parks.include?(park) %>"
                  data-favorited="<%= current_user && current_user.favorites.where(park: park).exists? %>"
                  data-action="click->favorites#favorite"
                  fill="<%= current_user && current_user.favorited_parks.include?(park) ? '#FF6962' : 'none' %>"
                  stroke="<%= current_user && current_user.favorited_parks.include?(park) ? 'none' : '#D3D3D3' %>"
                  width="24"
                  height="24"
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 24 24"
                  stroke-width="1.5"
                  >
                    <path
                      stroke-linecap="round"
                      stroke-linejoin="round"
                      d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597
                        1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1
                        3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
                </svg>
              </div>
            </div>
          </div>
          <br>
          <br>
        </div>
      </div>
    </div>
  </li>
<% end %>

CodePudding user response:

There are several ways to solve this. The most appropriate one for this case I think would be the following. In the successful JS callback of sending a POST request to create a Favorite record you should also update data-unfavorite-url attribute of your SVG. Something like this:

axios.post(this.element.dataset.favoriteUrl, ...)
     .then((response) => {
       this.element.dataset.unfavoriteUrl = `/api/favorites/${response.id}`;
       ...
     });

The downside of this is that JS code now has to know how to build the URL for a destroy action. You can avoid this by also sending the URL to destroy the newly created Favorite record in the create action of Rails controller

def create
  favorite = Favorite.create!(favorite_params)

  respond_to do |format|
    format.json do
      render json: { 
        record: favorite.to_json, 
        destroy_url: api_favorite_url(favorite) 
      }, status: :created
    end
  end
end

and then changing the line in the JS controller to this.element.dataset.unfavoriteUrl = response.destroyUrl;.

  • Related