DEV: Refactor lazy-video initialization for Glimmer compatibility (#32599)

Modernizes the renderGlimmer API to use Glimmer components, replacing
deprecated ember-cli-htmlbars templates. Adds deprecation warnings for
invalid parameters and improves compatibility with the new Glimmer Post
Stream API.
This commit is contained in:
Sérgio Saquetim 2025-05-07 10:47:02 -03:00 committed by GitHub
parent 5de93abbaa
commit 61de461412
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 162 additions and 26 deletions

View File

@ -1,10 +1,11 @@
import Component from "@glimmer/component";
import { hasInternalComponentManager } from "@glimmer/manager";
import { untrack } from "@glimmer/validator";
import { htmlSafe, isHTMLSafe } from "@ember/template";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import helperFn from "discourse/helpers/helper-fn";
import deprecated from "discourse/lib/deprecated";
import { POST_STREAM_DEPRECATION_OPTIONS } from "discourse/lib/plugin-api";
import { POST_STREAM_DEPRECATION_OPTIONS } from "discourse/widgets/post-stream";
const detachedDocument = document.implementation.createHTMLDocument("detached");
@ -75,8 +76,30 @@ class DecorateHtmlHelper {
this.#context = context;
}
renderGlimmer(element, component, data) {
const info = { element, component, data };
renderGlimmer(targetElement, component, data) {
if (!(targetElement instanceof Element)) {
deprecated(
"Invalid `targetElement` passed to `helper.renderGlimmer` while using `api.decorateCookedElement` with the Glimmer Post Stream. `targetElement` must be a valid HTML element. This call has been ignored to prevent errors.",
POST_STREAM_DEPRECATION_OPTIONS
);
return;
}
if (!hasInternalComponentManager(component)) {
deprecated(
"Invalid `component` passed to `helper.renderGlimmer` while using `api.decorateCookedElement` with the Glimmer Post Stream. `component` must be a valid Glimmer component. If using a template compiled via ember-cli-htmlbars, replace it with the `<template>...</template>` syntax. This call has been ignored to prevent errors.",
POST_STREAM_DEPRECATION_OPTIONS
);
return;
}
const info = {
element: targetElement,
component,
data,
};
this.#renderGlimmerInfos.push(info);
}
@ -107,7 +130,7 @@ class DecorateHtmlHelper {
},
scheduleRerender() {
// This is a no-op when using the new glimmer components.
// the component will rerender automatically when the model changes.
// The component will rerender automatically when the model changes.
},
};
}

View File

@ -145,6 +145,7 @@ import {
} from "discourse/widgets/post-small-action";
import {
addPostTransformCallback,
POST_STREAM_DEPRECATION_OPTIONS,
preventCloak,
} from "discourse/widgets/post-stream";
import { disableNameSuppression } from "discourse/widgets/poster-name";
@ -193,12 +194,6 @@ const DEPRECATED_POST_STREAM_WIDGETS = [
"topic-post-visited-line",
];
export const POST_STREAM_DEPRECATION_OPTIONS = {
since: "v3.5.0.beta1-dev",
id: "discourse.post-stream-widget-overrides",
// url: "", // TODO (glimmer-post-stream) uncomment when the topic is created on meta
};
const blockedModifications = ["component:topic-list"];
const appliedModificationIds = new WeakMap();

View File

@ -1,6 +1,9 @@
import { hasInternalComponentManager } from "@glimmer/manager";
import { h } from "virtual-dom";
import deprecated from "discourse/lib/deprecated";
import Connector from "discourse/widgets/connector";
import PostCooked from "discourse/widgets/post-cooked";
import { POST_STREAM_DEPRECATION_OPTIONS } from "discourse/widgets/post-stream";
import RawHtml from "discourse/widgets/raw-html";
import RenderGlimmer from "discourse/widgets/render-glimmer";
@ -116,18 +119,16 @@ class DecoratorHelper {
* Returns an element containing a rendered glimmer template. For full usage instructions,
* see `widgets/render-glimmer.js`.
*
* DEPRECATION NOTICES:
* - using a string describing a new wrapper element as the `targetElement` parameter is deprecated.
* Use an existing HTML element instead.
* - using a template compiled via `ember-cli-htmlbars` as the `component` parameter is deprecated.
* You should provide a component instead.
*
* Example usage in a `.gjs` file:
*
* ```
* api.decorateCookedElement((cooked, helper) => {
* const iconName = "user-plus";
* // Generate a new element with glimmer rendered inside
* const glimmerElement = helper.renderGlimmer(
* "div.my-wrapper-class",
* <template><DButton @icon={{iconName}} @translatedLabel="Hello world from Glimmer Component" /></template>
* );
* cooked.appendChild(glimmerElement);
*
* // Or append to an existing element
* helper.renderGlimmer(
* cooked.querySelector(".some-container"),
@ -137,15 +138,32 @@ class DecoratorHelper {
* ```
*
*/
renderGlimmer(renderInto, template, data) {
renderGlimmer(targetElement, component, data) {
// Ideally we should throw an error here, but we can't for now to prevent existing incompatible customizations from
// crashing the app. Instead, we will log a deprecation warning while we're in the process of migrating to the new
// Glimmer Post Stream API.
if (!(targetElement instanceof Element)) {
deprecated(
"The `targetElement` parameter provided to `helper.renderGlimmer` is invalid. It must be an existing HTML element. Using a string to describe a new wrapper element is deprecated.",
POST_STREAM_DEPRECATION_OPTIONS
);
}
if (!hasInternalComponentManager(component)) {
deprecated(
"The `component` parameter provided to `helper.renderGlimmer` is invalid. It must be a valid Glimmer component. Using a template compiled via `ember-cli-htmlbars` is deprecated. Use the <template>...</template> syntax or replace it with a proper component.",
POST_STREAM_DEPRECATION_OPTIONS
);
}
if (!this.widget.postContentsDestroyCallbacks) {
throw "renderGlimmer can only be used in the context of a post";
}
const renderGlimmer = new RenderGlimmer(
this.widget,
renderInto,
template,
targetElement,
component,
data
);
renderGlimmer.init();

View File

@ -16,6 +16,12 @@ import RenderGlimmer from "discourse/widgets/render-glimmer";
import { createWidget } from "discourse/widgets/widget";
import { i18n } from "discourse-i18n";
export const POST_STREAM_DEPRECATION_OPTIONS = {
since: "v3.5.0.beta1-dev",
id: "discourse.post-stream-widget-overrides",
// url: "", // TODO (glimmer-post-stream) uncomment when the topic is created on meta
};
export let havePostStreamWidgetExtensions = null;
registerDeprecationHandler((_, opts) => {

View File

@ -1,8 +1,10 @@
import { tracked } from "@glimmer/tracking";
import { htmlSafe } from "@ember/template";
import { render, settled } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import { module, test } from "qunit";
import DecoratedHtml from "discourse/components/decorated-html";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
module("Integration | Component | <DecoratedHtml />", function (hooks) {
@ -48,4 +50,88 @@ module("Integration | Component | <DecoratedHtml />", function (hooks) {
assert.dom("#appended").hasText("Appended");
assert.dom("#render-glimmer").hasText("Hello from Glimmer Component");
});
test("renderGlimmer is ignored if receives invalid arguments", async function (assert) {
const state = new (class {
@tracked html = htmlSafe("<h1>Initial</h1>");
})();
const decorateWithStringTarget = (element, helper) => {
element.innerHTML += "<div id='appended'>Appended</div>";
withSilencedDeprecations("discourse.post-stream-widget-overrides", () => {
helper.renderGlimmer(
"div",
<template>
<div id="render-glimmer">Hello from Glimmer Component</div>
</template>
);
});
};
await render(
<template>
<DecoratedHtml
@html={{state.html}}
@decorate={{decorateWithStringTarget}}
/>
</template>
);
assert
.dom("h1")
.hasText(
"Initial",
"Initial content is rendered when a string is passed as the target element"
);
assert
.dom("#appended")
.hasText(
"Appended",
"Appended content is rendered when a string is passed as the target element"
);
assert
.dom("#render-glimmer")
.doesNotExist(
"Glimmer component is not rendered when a string is passed as the target element"
);
const decorateWithHbsTemplate = (element, helper) => {
element.innerHTML += "<div id='appended'>Appended</div>";
withSilencedDeprecations("discourse.post-stream-widget-overrides", () => {
helper.renderGlimmer(
element,
hbs("<div id='render-glimmer'>Hello from Glimmer Component</div>")
);
});
};
await render(
<template>
<DecoratedHtml
@html={{state.html}}
@decorate={{decorateWithHbsTemplate}}
/>
</template>
);
assert
.dom("h1")
.hasText(
"Initial",
"Initial content is rendered when a template is passed as the component"
);
assert
.dom("#appended")
.hasText(
"Appended",
"Appended content is rendered when a template is passed as the component"
);
assert
.dom("#render-glimmer")
.doesNotExist(
"Glimmer component is not rendered when a template is passed as the component"
);
});
});

View File

@ -1,5 +1,5 @@
import { hbs } from "ember-cli-htmlbars";
import { withPluginApi } from "discourse/lib/plugin-api";
import LazyVideo from "../discourse/components/lazy-video";
import getVideoAttributes from "../lib/lazy-video-attributes";
function initLazyEmbed(api) {
@ -23,9 +23,17 @@ function initLazyEmbed(api) {
}
};
const lazyVideo = helper.renderGlimmer(
"p.lazy-video-wrapper",
hbs`<LazyVideo @videoAttributes={{@data.param}} @onLoadedVideo={{@data.onLoadedVideo}}/>`,
const lazyVideo = document.createElement("p");
lazyVideo.classList.add("lazy-video-wrapper");
helper.renderGlimmer(
lazyVideo,
<template>
<LazyVideo
@videoAttributes={{@data.param}}
@onLoadedVideo={{@data.onLoadedVideo}}
/>
</template>,
{ param: videoAttributes, onLoadedVideo }
);
@ -41,6 +49,6 @@ export default {
name: "discourse-lazy-videos",
initialize() {
withPluginApi("1.6.0", initLazyEmbed);
withPluginApi(initLazyEmbed);
},
};