I’m trying to implement share functionality with rails7, stimulus and navigator.share, which is not supported in every browser. For those cases, I need to copy a link to a clipboard but show a flash message. I have a problem with the flash message. What is the correct approach to solving this problem, and is it even possible to do so?
Share button
<div data-controller="share"
data-share-target="trigger"
data-share-url-value="<%= post_url(@post) %>"
data-share-name-value="<%= @post.name %>">
<%= button_to "", data: { action: "click->share#share" } do %>
<%= inline_svg_tag "svg/share.svg", class: 'h-6 w-6' %>
<% end %>
</div>
Stimulus controller
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["trigger"];
static values = ["name"];
connect() {}
async share(e) {
e.preventDefault();
const shareData = {
name: this.nameValue,
url: this.data.get("urlValue"),
text: "some cool text",
title: "title here",
};
try {
if (navigator.share) {
await navigator.share(shareData);
} else {
navigator.clipboard.writeText(this.data.get("urlValue"));
// Trigger Flash message here
}
} catch (err) {
console.log(err);
}
}
}
My flash
app/views/layouts/application.html.erb
<body class="min-h-full flex flex-col h-full">
<main class="flex-1">
<div id="flash" class="w-full">
<% flash.each do |key, value| %>
<div>
<p><%= value %></p>
</div>
<% end %>
</div>
<%= yield %>
</main>
</body>
Is this even possible? If not, how would you implement this? I would also appreciate any feedback regarding the implementation.
There are a few approaches to solving this problem, the simplest would be to add the ability to dynamically inject the message in the same Share
controller.
Here is a full code example below.
template
element to the inside the #flash
element to house the client-side HTML content to copy for each message.data-share-target="trigger"
as you can always access the controlled element with this.element
but it looks like you were not using it anyway.button
, remember that adding type="button"
avoids the need to use event.preventDefault
and it works better for accessibility.aria-label
but you could also add a span with sr-only
(looks like you are using Tailwind).<body>
<div id="flash" class="w-full" aria-live="polite">
<!-- ...flash.each do etc -->
<template data-template>
<div><p data-value></p></div>
</template>
</div>
<section>
<h4>Share stuff</h4>
<div
data-controller="share"
data-share-name-value="some-name"
data-share-url-value="path/to/post/"
>
<button type="button" data-action="share#share" aria-label="share">
Share
</button>
</div>
</section>
</body>
addMessage
that will find the element with the id flash
and then template element inside. It will clone that element and then find where to put the message content. Then it will append this node into the flash container.addMessage
method, you can use this method to show the 'clipboard updated' message and any errors if you need them to be shown to the user.then
callback on the promise is more than sufficient for your use case.import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static values = {
messageTimeout: { default: 2.5 * 1000, type: Number },
name: String,
url: String,
};
addMessage(content, { type = "error" } = {}) {
const flashContainer = document.getElementById("flash");
if (!flashContainer) return;
const template = flashContainer.querySelector("[data-template]");
const node = template.content.firstElementChild.cloneNode(true);
node.querySelector("[data-value]").innerText = content.message;
flashContainer.append(node);
// optional - add timeout to remove after 2.5 seconds
window.setTimeout(() => {
node.remove();
}, this.messageTimeoutValue);
}
share() {
const name = this.nameValue;
const url = this.urlValue;
const shareData = {
name,
text: "some cool text",
title: "title here",
url,
};
try {
if (navigator.share) {
navigator.share(shareData).catch((error) => this.handleError(error));
} else {
navigator.clipboard.writeText(url);
this.addMessage({ message: "copied to clipboard" }, { type: 'info' });
}
} catch (error) {
this.handleError(error);
}
}
}
Another way to do this would be to split out the flash message behaviour to a new controller. This would be useful if you expect other code to need to create messages dynamically.
Here is a reference implementation you could use for this.
https://github.com/wagtail/wagtail/blob/main/client/src/controllers/MessagesController.ts (Wagtail, a CMS built on Django, has a similar global messages use case).
You could also use a Stimulus approach for the 'remove after a delay' part only, here is a reference implementation for the 'toast' style removal.
https://github.com/stimulus-components/stimulus-notification/blob/master/src/index.ts
Best to start simple and only add this separate controller behaviour only if you need it though.