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;
.