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"]); + }); +});