diff --git a/assets/controllers/comments_mercure_controller.js b/assets/controllers/comments_mercure_controller.js
index c61d946..1ccb4ec 100644
--- a/assets/controllers/comments_mercure_controller.js
+++ b/assets/controllers/comments_mercure_controller.js
@@ -1,135 +1,98 @@
import { Controller } from "@hotwired/stimulus";
-/**
- * Server-driven comments via Mercure + Symfony UX LiveComponent.
- *
- * Usage in Twig (root element is the LiveComponent root):
- *
- * ...
- *
- */
export default class extends Controller {
- static values = {
- coordinate: String,
- };
-
- static targets = ["list", "loading"];
+ static values = { coordinate: String }
+ static targets = ["list", "loading"]
connect() {
- console.log("[comments-mercure] Connecting to Mercure for comments at", this.coordinateValue);
this._debounceId = null;
- this._opened = false;
-
- // Initial paint: ask the LiveComponent to render once (gets cached HTML immediately)
- this._renderLiveComponent();
-
- // Subscribe to Mercure for live updates
- const topic = `/comments/${this.coordinateValue}`;
- const hubUrl =
- window.MercureHubUrl ||
- document.querySelector('meta[name="mercure-hub"]')?.content;
+ this._liveRoot = this._findLiveRoot();
+ console.log(this._liveRoot);
+
+ // If the live controller isn't ready yet, wait for it.
+ if (!this._getLiveController()) {
+ this._onLiveConnect = () => {
+ // Once live connects, do an initial render to paint cached HTML
+ this._renderLiveComponent();
+ };
+ this._liveRoot?.addEventListener('live:connect', this._onLiveConnect, { once: true });
+ } else {
+ // Live controller already attached -> initial render now
+ this._renderLiveComponent();
+ }
+ // Subscribe to Mercure updates
+ const hubUrl = window.MercureHubUrl || document.querySelector('meta[name="mercure-hub"]')?.content;
if (!hubUrl) {
- console.warn(
- "[comments-mercure] Missing Mercure hub URL (meta[name=mercure-hub])"
- );
+ console.warn("[comments-mercure] Missing Mercure hub URL meta");
this._hideLoading();
return;
}
- const url = new URL(hubUrl);
+ const topic = `/comments/${this.coordinateValue}`;
+ const url = new URL(hubUrl);
url.searchParams.append("topic", topic);
this.eventSource = new EventSource(url.toString());
- this.eventSource.onopen = () => {
- this._opened = true;
- // When the connection opens, do a quick refresh to capture anything new
- this._debouncedRefresh();
- };
- this.eventSource.onerror = (e) => {
- console.warn("[comments-mercure] EventSource error", e);
- // Keep the UI usable even if Mercure hiccups
- this._hideLoading();
- };
- this.eventSource.onmessage = (e) => {
- console.log('Mercure MSG', e.data);
- // We ignore the payload; Mercure is just a signal to re-render the live component
- this._debouncedRefresh();
- };
+ this.eventSource.onopen = () => this._debouncedRefresh(50);
+ this.eventSource.onerror = (e) => console.warn("[comments-mercure] EventSource error", e);
+ this.eventSource.onmessage = () => this._debouncedRefresh();
}
disconnect() {
- if (this.eventSource) {
- try {
- this.eventSource.close();
- } catch {}
- }
- if (this._debounceId) {
- clearTimeout(this._debounceId);
+ if (this.eventSource) { try { this.eventSource.close(); } catch {} }
+ if (this._debounceId) { clearTimeout(this._debounceId); }
+ if (this._liveRoot && this._onLiveConnect) {
+ this._liveRoot.removeEventListener('live:connect', this._onLiveConnect);
}
}
// ---- private helpers -----------------------------------------------------
+ _findLiveRoot() {
+ // Works for both modern ("live") and older namespaced identifiers
+ return this.element.closest(
+ '[data-controller~="live"]'
+ );
+ }
+
+ _getLiveController() {
+ if (!this._liveRoot) return null;
+ // Try both identifiers
+ return (
+ this.application.getControllerForElementAndIdentifier(this._liveRoot, 'live') ||
+ this.application.getControllerForElementAndIdentifier(this._liveRoot, 'symfony--ux-live-component--live')
+ );
+ }
+
_debouncedRefresh(delay = 150) {
if (this._debounceId) clearTimeout(this._debounceId);
- this._debounceId = setTimeout(() => {
- this._renderLiveComponent();
- }, delay);
+ this._debounceId = setTimeout(() => this._renderLiveComponent(), delay);
}
_renderLiveComponent() {
- // Show loading spinner (if present) only while we’re actually fetching
- this._showLoading();
-
- // The live component controller is bound to the same root element.
- const liveRoot =
- this.element.closest(
- '[data-controller~="symfony--ux-live-component--live"]'
- ) || this.element;
-
- const liveController =
- this.application.getControllerForElementAndIdentifier(
- liveRoot,
- "symfony--ux-live-component--live"
- );
-
- if (!liveController || typeof liveController.render !== "function") {
- console.warn(
- "[comments-mercure] LiveComponent controller not found on element:",
- liveRoot
- );
- this._hideLoading();
+ const live = this._getLiveController();
+ if (!live || typeof live.render !== 'function') {
+ // Live not ready yet—try again very soon (and don't spam logs)
+ setTimeout(() => this._renderLiveComponent(), 50);
return;
}
- // Ask server for the fresh HTML; morphdom will patch the DOM in place.
- // render() returns a Promise (in recent UX versions). Handle both cases.
- try {
- const maybePromise = liveController.render();
- if (maybePromise && typeof maybePromise.then === "function") {
- maybePromise.finally(() => this._hideLoading());
- } else {
- // Older versions might not return a promise—hide the spinner soon.
- setTimeout(() => this._hideLoading(), 0);
- }
- } catch (e) {
- console.error("[comments-mercure] live.render() failed", e);
- this._hideLoading();
+ this._showLoading();
+ const p = live.render();
+ if (p && typeof p.finally === 'function') {
+ p.finally(() => this._hideLoading());
+ } else {
+ setTimeout(() => this._hideLoading(), 0);
}
}
_showLoading() {
if (this.hasLoadingTarget) this.loadingTarget.style.display = "";
- if (this.hasListTarget) this.listTarget.style.opacity = "0.6";
+ if (this.hasListTarget) this.listTarget.style.opacity = "0.6";
}
-
_hideLoading() {
if (this.hasLoadingTarget) this.loadingTarget.style.display = "none";
- if (this.hasListTarget) this.listTarget.style.opacity = "";
+ if (this.hasListTarget) this.listTarget.style.opacity = "";
}
}
diff --git a/src/Twig/Components/Organisms/Comments.php b/src/Twig/Components/Organisms/Comments.php
index 51fc9bb..c9301d9 100644
--- a/src/Twig/Components/Organisms/Comments.php
+++ b/src/Twig/Components/Organisms/Comments.php
@@ -11,7 +11,7 @@ use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
-#[AsLiveComponent]
+#[AsLiveComponent('Organisms:Comments')]
final class Comments
{
use DefaultActionTrait;
diff --git a/templates/components/Organisms/Comments.html.twig b/templates/components/Organisms/Comments.html.twig
index 12497fe..5b72ef2 100644
--- a/templates/components/Organisms/Comments.html.twig
+++ b/templates/components/Organisms/Comments.html.twig
@@ -1,18 +1,21 @@
Unknown
{% endif %} - {{ item.created_at|date('F j Y') }} -