From 14be2eb3a5942bef1a7539df88d78135130e2fdd Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Fri, 20 Mar 2026 18:55:26 -0300 Subject: [PATCH 1/8] refactored load function for readability --- .../vimeo-video-element.js | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/vimeo-video-element/vimeo-video-element.js b/packages/vimeo-video-element/vimeo-video-element.js index 6b5f231..e8f23ba 100644 --- a/packages/vimeo-video-element/vimeo-video-element.js +++ b/packages/vimeo-video-element/vimeo-video-element.js @@ -185,50 +185,54 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? ...this.#config, }; - const onLoaded = async () => { - this.#readyState = 1; // HTMLMediaElement.HAVE_METADATA - this.dispatchEvent(new Event('loadedmetadata')); - - if (this.api) { - this.#muted = await this.api.getMuted(); - this.#volume = await this.api.getVolume(); - this.dispatchEvent(new Event('volumechange')); - - this.#duration = await this.api.getDuration(); - this.dispatchEvent(new Event('durationchange')); - } - - this.dispatchEvent(new Event('loadcomplete')); - this.loadComplete.resolve(); - }; - if (this.#isInit) { this.api = oldApi; await this.api.loadVideo({ ...options, url: this.src, }); - await onLoaded(); + await this.#onLoaded(); await this.loadComplete; return; - } + } else { + this.#isInit = true; - this.#isInit = true; + let iframe = this.shadowRoot?.querySelector('iframe'); - let iframe = this.shadowRoot?.querySelector('iframe'); + if (isFirstLoad && iframe) { + this.#config = JSON.parse(iframe.getAttribute('data-config') || '{}'); + } - if (isFirstLoad && iframe) { - this.#config = JSON.parse(iframe.getAttribute('data-config') || '{}'); + if (!this.shadowRoot) { + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this); + iframe = this.shadowRoot.querySelector('iframe'); + } + + this.api = new VimeoPlayerAPI(iframe, options); + this.#setupApiListeners(); + await this.loadComplete; } + } + + #onLoaded = async () => { + this.#readyState = 1; // HTMLMediaElement.HAVE_METADATA + this.dispatchEvent(new Event('loadedmetadata')); + + if (this.api) { + this.#muted = await this.api.getMuted(); + this.#volume = await this.api.getVolume(); + this.dispatchEvent(new Event('volumechange')); - if (!this.shadowRoot) { - this.attachShadow({ mode: 'open' }); - this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this); - iframe = this.shadowRoot.querySelector('iframe'); + this.#duration = await this.api.getDuration(); + this.dispatchEvent(new Event('durationchange')); } - this.api = new VimeoPlayerAPI(iframe); + this.dispatchEvent(new Event('loadcomplete')); + this.loadComplete.resolve(); + }; + #setupApiListeners() { const textTracksVideo = document.createElement('video'); this.textTracks = textTracksVideo.textTracks; this.api.getTextTracks().then((vimeoTracks) => { @@ -247,7 +251,7 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? const onceLoaded = () => { this.api.off('loaded', onceLoaded); - onLoaded(); + this.#onLoaded(); }; this.api.on('loaded', onceLoaded); @@ -331,8 +335,6 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? this.#videoHeight = videoHeight; this.dispatchEvent(new Event('resize')); }); - - await this.loadComplete; } async attributeChangedCallback(attrName, oldValue, newValue) { From 6a8f3dc74cc3b70b6b31379fa634ad0d9a26e98f Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Fri, 20 Mar 2026 18:58:05 -0300 Subject: [PATCH 2/8] Moved load decoupling to the top of the function --- packages/vimeo-video-element/vimeo-video-element.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/vimeo-video-element/vimeo-video-element.js b/packages/vimeo-video-element/vimeo-video-element.js index e8f23ba..a8e2427 100644 --- a/packages/vimeo-video-element/vimeo-video-element.js +++ b/packages/vimeo-video-element/vimeo-video-element.js @@ -138,17 +138,16 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? } async load() { - if (this.#loadRequested) return; - const isFirstLoad = !this.#hasLoaded; - - if (this.#hasLoaded) this.loadComplete = new PublicPromise(); - this.#hasLoaded = true; + if (this.#loadRequested) return; // Wait 1 tick to allow other attributes to be set. await (this.#loadRequested = Promise.resolve()); this.#loadRequested = null; + if (this.#hasLoaded) this.loadComplete = new PublicPromise(); + this.#hasLoaded = true; // TODO: Identify how hasLoaded differs from isInit + this.#currentTime = 0; this.#duration = NaN; this.#muted = false; From 50ef8965202fcf935556f254c00df7fb0b9cfa9b Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Fri, 20 Mar 2026 18:58:17 -0300 Subject: [PATCH 3/8] Added state cleanup on disconnect --- packages/vimeo-video-element/vimeo-video-element.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/vimeo-video-element/vimeo-video-element.js b/packages/vimeo-video-element/vimeo-video-element.js index a8e2427..261411f 100644 --- a/packages/vimeo-video-element/vimeo-video-element.js +++ b/packages/vimeo-video-element/vimeo-video-element.js @@ -214,6 +214,13 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? } } + disconnectedCallback() { + this.#loadRequested = null; + this.#hasLoaded = null; + this.#isInit = null; + super.disconnectedCallback?.() + } + #onLoaded = async () => { this.#readyState = 1; // HTMLMediaElement.HAVE_METADATA this.dispatchEvent(new Event('loadedmetadata')); From 359a7fc68c585a417e5796962f77886287f74c49 Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Wed, 1 Apr 2026 16:37:00 -0300 Subject: [PATCH 4/8] Reset loadComplete on disconnect --- packages/vimeo-video-element/vimeo-video-element.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vimeo-video-element/vimeo-video-element.js b/packages/vimeo-video-element/vimeo-video-element.js index 261411f..e9907f0 100644 --- a/packages/vimeo-video-element/vimeo-video-element.js +++ b/packages/vimeo-video-element/vimeo-video-element.js @@ -138,15 +138,14 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? } async load() { - const isFirstLoad = !this.#hasLoaded; if (this.#loadRequested) return; - // Wait 1 tick to allow other attributes to be set. await (this.#loadRequested = Promise.resolve()); this.#loadRequested = null; - + + const isFirstLoad = !this.#hasLoaded; if (this.#hasLoaded) this.loadComplete = new PublicPromise(); - this.#hasLoaded = true; // TODO: Identify how hasLoaded differs from isInit + this.#hasLoaded = true; this.#currentTime = 0; this.#duration = NaN; @@ -218,6 +217,7 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? this.#loadRequested = null; this.#hasLoaded = null; this.#isInit = null; + this.loadComplete = new PublicPromise(); super.disconnectedCallback?.() } From adf442f28525f92306462e5be3d74f39e66eed3c Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Thu, 23 Apr 2026 17:34:27 -0300 Subject: [PATCH 5/8] Switched approach to account for SSR and remounts --- .../vimeo-video-element.js | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/vimeo-video-element/vimeo-video-element.js b/packages/vimeo-video-element/vimeo-video-element.js index 404d23e..4b81c58 100644 --- a/packages/vimeo-video-element/vimeo-video-element.js +++ b/packages/vimeo-video-element/vimeo-video-element.js @@ -109,6 +109,10 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? #videoWidth = NaN; #videoHeight = NaN; #config = null; + /** Distinguishes a remount from SSR hydration. + * See load() + */ + #wasDisconnected = false; constructor() { super(); @@ -137,17 +141,18 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? set config(value) { this.#config = value; + this.load(); } async load() { if (this.#loadRequested) return; + + if (this.#hasLoaded) this.loadComplete = new PublicPromise(); + this.#hasLoaded = true; + // Wait 1 tick to allow other attributes to be set. await (this.#loadRequested = Promise.resolve()); this.#loadRequested = null; - - const isFirstLoad = !this.#hasLoaded; - if (this.#hasLoaded) this.loadComplete = new PublicPromise(); - this.#hasLoaded = true; this.#currentTime = 0; this.#duration = NaN; @@ -197,18 +202,39 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? } else { this.#isInit = true; - let iframe = this.shadowRoot?.querySelector('iframe'); - - if (isFirstLoad && iframe) { - this.#config = JSON.parse(iframe.getAttribute('data-config') || '{}'); - } - if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this); - iframe = this.shadowRoot.querySelector('iframe'); + if (!this.#config) { + // SSR: read config serialised into data-config before client-side hydration + const ssrIframe = this.shadowRoot.querySelector('iframe'); + this.#config = JSON.parse(ssrIframe.getAttribute('data-config') || '{}'); + } + } else { + /* + * - On remount we must rebuild the template so the iframe URL + * reflects the current src and config, the Vimeo SDK wraps the + * existing iframe as-is and won't update its URL. + * - On SSR hydration the iframe URL is already correct, so + * rebuilding would destroy the in-flight request and + * cause a visible stutter. + */ + if (this.#wasDisconnected) { + // Remount after disconnect: + // - rebuild so src and config are reflected in the new iframe. + this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this); + } else { + // SSR declarative shadow DOM: + // - preserve iframe to avoid a redundant reload. Read config if not yet set. + if (!this.#config) { + const ssrIframe = this.shadowRoot.querySelector('iframe'); + this.#config = JSON.parse(ssrIframe.getAttribute('data-config') || '{}'); + } + } } + this.#wasDisconnected = false; + const iframe = this.shadowRoot.querySelector('iframe'); this.api = new VimeoPlayerAPI(iframe, options); this.#setupApiListeners(); await this.loadComplete; @@ -216,6 +242,7 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? } disconnectedCallback() { + this.#wasDisconnected = true; this.#loadRequested = null; this.#hasLoaded = null; this.#isInit = null; From 4964d42dd3c45d4ea7224afd92e3f70bb35d838e Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Thu, 23 Apr 2026 17:52:05 -0300 Subject: [PATCH 6/8] Clean up if else chaos --- .../vimeo-video-element.js | 73 +++++++++---------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/packages/vimeo-video-element/vimeo-video-element.js b/packages/vimeo-video-element/vimeo-video-element.js index 4b81c58..ffed6fe 100644 --- a/packages/vimeo-video-element/vimeo-video-element.js +++ b/packages/vimeo-video-element/vimeo-video-element.js @@ -199,46 +199,39 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? await this.#onLoaded(); await this.loadComplete; return; - } else { - this.#isInit = true; - - if (!this.shadowRoot) { - this.attachShadow({ mode: 'open' }); - this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this); - if (!this.#config) { - // SSR: read config serialised into data-config before client-side hydration - const ssrIframe = this.shadowRoot.querySelector('iframe'); - this.#config = JSON.parse(ssrIframe.getAttribute('data-config') || '{}'); - } - } else { - /* - * - On remount we must rebuild the template so the iframe URL - * reflects the current src and config, the Vimeo SDK wraps the - * existing iframe as-is and won't update its URL. - * - On SSR hydration the iframe URL is already correct, so - * rebuilding would destroy the in-flight request and - * cause a visible stutter. - */ - if (this.#wasDisconnected) { - // Remount after disconnect: - // - rebuild so src and config are reflected in the new iframe. - this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this); - } else { - // SSR declarative shadow DOM: - // - preserve iframe to avoid a redundant reload. Read config if not yet set. - if (!this.#config) { - const ssrIframe = this.shadowRoot.querySelector('iframe'); - this.#config = JSON.parse(ssrIframe.getAttribute('data-config') || '{}'); - } - } - } - this.#wasDisconnected = false; + } - const iframe = this.shadowRoot.querySelector('iframe'); - this.api = new VimeoPlayerAPI(iframe, options); - this.#setupApiListeners(); - await this.loadComplete; + this.#isInit = true; + + /* + * Decide whether to build the iframe or adopt an existing one: + * - First client mount or remount after disconnect: build, so the iframe + * URL reflects current src and config. The Vimeo SDK wraps an existing + * iframe as-is and won't update its URL. + * - SSR declarative shadow DOM hydration: adopt the existing iframe. Its + * URL is already correct, and rebuilding would destroy the in-flight + * request and cause a visible stutter. Recover config from the + * data-config attribute so element state matches the DOM. + */ + if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); + + const existingIframe = this.shadowRoot.querySelector('iframe'); + const isSsrHydration = existingIframe && !this.#wasDisconnected; + + if (isSsrHydration) { + if (!this.#config) { + this.#config = JSON.parse(existingIframe.getAttribute('data-config') || '{}'); + } + } else { + this.shadowRoot.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes), this); } + this.#wasDisconnected = false; + + oldApi?.destroy?.(); + const iframe = this.shadowRoot.querySelector('iframe'); + this.api = new VimeoPlayerAPI(iframe); + this.#setupApiListeners(); + await this.loadComplete; } disconnectedCallback() { @@ -247,7 +240,9 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? this.#hasLoaded = null; this.#isInit = null; this.loadComplete = new PublicPromise(); - super.disconnectedCallback?.() + this.api?.destroy?.(); + this.api = null; + super.disconnectedCallback?.(); } #onLoaded = async () => { From 6de507c612ed5cab4641dcb4bf0e7e70a7f0a082 Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Mon, 27 Apr 2026 16:20:11 -0300 Subject: [PATCH 7/8] Added config old === new check to avoid unnecessary reloads; reordered loadComplete renewal to after the src check --- packages/vimeo-video-element/vimeo-video-element.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/vimeo-video-element/vimeo-video-element.js b/packages/vimeo-video-element/vimeo-video-element.js index ffed6fe..b7eeb81 100644 --- a/packages/vimeo-video-element/vimeo-video-element.js +++ b/packages/vimeo-video-element/vimeo-video-element.js @@ -140,16 +140,13 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? } set config(value) { + if (JSON.stringify(this.#config) === JSON.stringify(value)) return; this.#config = value; this.load(); } async load() { if (this.#loadRequested) return; - - if (this.#hasLoaded) this.loadComplete = new PublicPromise(); - this.#hasLoaded = true; - // Wait 1 tick to allow other attributes to be set. await (this.#loadRequested = Promise.resolve()); this.#loadRequested = null; @@ -172,9 +169,15 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? this.api = null; if (!this.src) { + // Nothing to load. Leave loadComplete and #hasLoaded untouched so + // callers awaiting the existing loadComplete aren't orphaned if a + // later load() (e.g. triggered by a subsequent src) replaces it. return; } + if (this.#hasLoaded) this.loadComplete = new PublicPromise(); + this.#hasLoaded = true; + this.dispatchEvent(new Event('loadstart')); // https://developer.vimeo.com/player/sdk/embed From 323907fc327491d18883d6c19c46d4fe44fdf2b6 Mon Sep 17 00:00:00 2001 From: Santiago Puppo Date: Mon, 27 Apr 2026 18:27:11 -0300 Subject: [PATCH 8/8] Added connected callback --- packages/vimeo-video-element/vimeo-video-element.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/vimeo-video-element/vimeo-video-element.js b/packages/vimeo-video-element/vimeo-video-element.js index b7eeb81..bc36891 100644 --- a/packages/vimeo-video-element/vimeo-video-element.js +++ b/packages/vimeo-video-element/vimeo-video-element.js @@ -237,6 +237,13 @@ class VimeoVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement ?? await this.loadComplete; } + connectedCallback() { + if (this.#wasDisconnected) { + this.load(); + } + super.connectedCallback?.(); + } + disconnectedCallback() { this.#wasDisconnected = true; this.#loadRequested = null;