diff --git a/app/assets/javascripts/discourse/app/widgets/decorator-helper.js b/app/assets/javascripts/discourse/app/widgets/decorator-helper.js
index fcd58cba038..f2d584d0910 100644
--- a/app/assets/javascripts/discourse/app/widgets/decorator-helper.js
+++ b/app/assets/javascripts/discourse/app/widgets/decorator-helper.js
@@ -2,6 +2,7 @@ import Connector from "discourse/widgets/connector";
import PostCooked from "discourse/widgets/post-cooked";
import RawHtml from "discourse/widgets/raw-html";
import { h } from "virtual-dom";
+import RenderGlimmer from "discourse/widgets/render-glimmer";
class DecoratorHelper {
constructor(widget, attrs, state) {
@@ -106,6 +107,44 @@ class DecoratorHelper {
connect(details) {
return new Connector(this.widget, details);
}
+
+ /**
+ * Returns an element containing a rendered glimmer template. For full usage instructions,
+ * see `widgets/render-glimmer.js`.
+ *
+ * Example usage:
+ *
+ * ```
+ * import { hbs } from "ember-cli-htmlbars";
+ *
+ * api.decorateCookedElement((cooked, helper) => {
+ * const glimmerElement = helper.renderGlimmer(
+ * "div.my-wrapper-class",
+ * hbs``,
+ * { param: "user-plus" }
+ * );
+ * cooked.appendChild(glimmerElement);
+ * }, { onlyStream: true, id: "my-id" });
+ * ```
+ *
+ */
+ renderGlimmer(tagName, template, data) {
+ if (!this.widget.postContentsDestroyCallbacks) {
+ throw "renderGlimmer can only be used in the context of a post";
+ }
+
+ const renderGlimmer = new RenderGlimmer(
+ this.widget,
+ tagName,
+ template,
+ data
+ );
+ renderGlimmer.init();
+ this.widget.postContentsDestroyCallbacks.push(
+ renderGlimmer.destroy.bind(renderGlimmer)
+ );
+ return renderGlimmer.element;
+ }
}
DecoratorHelper.prototype.h = h;
diff --git a/app/assets/javascripts/discourse/app/widgets/post.js b/app/assets/javascripts/discourse/app/widgets/post.js
index 0fbb7f57cf3..496cad2123f 100644
--- a/app/assets/javascripts/discourse/app/widgets/post.js
+++ b/app/assets/javascripts/discourse/app/widgets/post.js
@@ -629,6 +629,14 @@ createWidget("post-contents", {
controller.setProperties({ topic, post });
});
},
+
+ init() {
+ this.postContentsDestroyCallbacks = [];
+ },
+
+ destroy() {
+ this.postContentsDestroyCallbacks.forEach((c) => c());
+ },
});
createWidget("post-notice", {
diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-post-decorate-cooked-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-post-decorate-cooked-test.js
new file mode 100644
index 00000000000..f85096b2233
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/topic-post-decorate-cooked-test.js
@@ -0,0 +1,51 @@
+import Component from "@glimmer/component";
+import { hbs } from "ember-cli-htmlbars";
+import { setComponentTemplate } from "@ember/component";
+import { test } from "qunit";
+import { acceptance } from "discourse/tests/helpers/qunit-helpers";
+import { visit } from "@ember/test-helpers";
+import { withPluginApi } from "discourse/lib/plugin-api";
+
+acceptance("Acceptance | decorateCookedElement", function () {
+ test("decorator with renderGlimmer works", async function (assert) {
+ class DemoComponent extends Component {
+ static eventLog = [];
+ constructor() {
+ DemoComponent.eventLog.push("created");
+ return super(...arguments);
+ }
+ willDestroy() {
+ DemoComponent.eventLog.push("willDestroy");
+ }
+ }
+ setComponentTemplate(
+ hbs`Hello world`,
+ DemoComponent
+ );
+
+ withPluginApi(0, (api) => {
+ api.decorateCookedElement((cooked, helper) => {
+ if (helper.getModel().post_number !== 1) {
+ return;
+ }
+ cooked.appendChild(
+ helper.renderGlimmer(
+ "div.glimmer-wrapper",
+ hbs`<@data.component />`,
+ { component: DemoComponent }
+ )
+ );
+ });
+ });
+
+ await visit("/t/internationalization-localization/280");
+
+ assert.dom("div.glimmer-wrapper").exists();
+ assert.dom("span.glimmer-component-content").exists();
+ assert.deepEqual(DemoComponent.eventLog, ["created"]);
+
+ await visit("/");
+
+ assert.deepEqual(DemoComponent.eventLog, ["created", "willDestroy"]);
+ });
+});