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 @@
- {% if loading %} -
Loading comments…
- {% endif %} -
- {% for item in list %} -
-