From ca2b2d034f2748d9677bccf6b01e62d3159ce90d Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 2 Feb 2023 16:25:57 +0000 Subject: [PATCH] DEV: Introduce API for rendering Glimmer inside posts (#20140) `helper.renderGlimmer` will return an HTML element which can be added to a post's `cooked` 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" }); ``` See `widgets/render-glimmer.js` for more detailed usage information. --- .../discourse/app/widgets/decorator-helper.js | 39 ++++++++++++++ .../javascripts/discourse/app/widgets/post.js | 8 +++ .../topic-post-decorate-cooked-test.js | 51 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 app/assets/javascripts/discourse/tests/acceptance/topic-post-decorate-cooked-test.js diff --git a/app/assets/javascripts/discourse/app/widgets/decorator-helper.js b/app/assets/javascripts/discourse/app/widgets/decorator-helper.js index fcd58cba03..f2d584d091 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 0fbb7f57cf..496cad2123 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 0000000000..f85096b223 --- /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"]); + }); +});