DEV: Introduce RenderGlimmer for raw hbs (#23592)
A new `rawRenderGlimmer` function is introduced which can be used to render glimmer components inside our legacy 'raw hbs' views. See discourse/lib/raw-render-glimmer for more information. This will help as we work to move away from raw-hbs use.
This commit is contained in:
parent
42070d49da
commit
2e950eb07a
|
@ -82,7 +82,23 @@ function resolveParams(ctx, options) {
|
|||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a helper for Ember and raw-hbs. This exists for
|
||||
* legacy reasons, and should be avoided in new code. Instead, you should
|
||||
* do `export default ...` from a `helpers/*.js` file.
|
||||
*/
|
||||
export function registerUnbound(name, fn) {
|
||||
_helpers[name] = Helper.extend({
|
||||
compute: (params, args) => fn(...params, args),
|
||||
});
|
||||
|
||||
registerRawHelper(name, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a helper for raw-hbs only
|
||||
*/
|
||||
export function registerRawHelper(name, fn) {
|
||||
const func = function (...args) {
|
||||
const options = args.pop();
|
||||
const properties = args;
|
||||
|
@ -99,8 +115,5 @@ export function registerUnbound(name, fn) {
|
|||
return fn.call(this, ...properties, resolveParams(this, options));
|
||||
};
|
||||
|
||||
_helpers[name] = Helper.extend({
|
||||
compute: (params, args) => fn(...params, args),
|
||||
});
|
||||
RawHandlebars.registerHelper(name, func);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default class RenderGlimmerContainer extends Component {
|
||||
<template>
|
||||
{{#each this.renderGlimmer._registrations as |info|}}
|
||||
{{#in-element info.element insertBefore=null}}
|
||||
<info.component
|
||||
@data={{info.data}}
|
||||
@setWrapperElementAttrs={{info.setWrapperElementAttrs}}
|
||||
/>
|
||||
{{/in-element}}
|
||||
{{/each}}
|
||||
</template>
|
||||
|
||||
@service renderGlimmer;
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
import { helperContext, registerUnbound } from "discourse-common/lib/helpers";
|
||||
import { helperContext, registerRawHelper } from "discourse-common/lib/helpers";
|
||||
import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { RUNTIME_OPTIONS } from "discourse-common/lib/raw-handlebars-helpers";
|
||||
import { getOwner, setOwner } from "@ember/application";
|
||||
import Helper from "@ember/component/helper";
|
||||
import { registerDestructor } from "@ember/destroyable";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
function renderRaw(ctx, template, templateName, params) {
|
||||
params = { ...params };
|
||||
|
@ -25,7 +30,7 @@ function renderRaw(ctx, template, templateName, params) {
|
|||
return htmlSafe(template(params, RUNTIME_OPTIONS));
|
||||
}
|
||||
|
||||
registerUnbound("raw", function (templateName, params) {
|
||||
const helperFunction = function (templateName, params) {
|
||||
templateName = templateName.replace(".", "/");
|
||||
|
||||
const template = findRawTemplate(templateName);
|
||||
|
@ -35,4 +40,20 @@ registerUnbound("raw", function (templateName, params) {
|
|||
return;
|
||||
}
|
||||
return renderRaw(this, template, templateName, params);
|
||||
});
|
||||
};
|
||||
|
||||
registerRawHelper("raw", helperFunction);
|
||||
|
||||
export default class RawHelper extends Helper {
|
||||
@service renderGlimmer;
|
||||
|
||||
compute(args, params) {
|
||||
registerDestructor(this, this.cleanup);
|
||||
return helperFunction(...args, params);
|
||||
}
|
||||
|
||||
@bind
|
||||
cleanup() {
|
||||
schedule("afterRender", () => this.renderGlimmer.cleanup());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { schedule } from "@ember/runloop";
|
||||
import { getOwner } from "@ember/application";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
/**
|
||||
* Generate HTML which can be inserted into a raw-hbs template to render a Glimmer component.
|
||||
* The result of this function must be rendered immediately, so that an `afterRender` hook
|
||||
* can access the element in the DOM and attach the glimmer component.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```hbs
|
||||
* {{! raw-templates/something-cool.hbr }}
|
||||
* {{{view.html}}}
|
||||
* ```
|
||||
*
|
||||
* ```gjs
|
||||
* // raw-views/something-cool.gjs
|
||||
* import EmberObject from "@ember/object";
|
||||
* import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
|
||||
*
|
||||
* export default class SomethingCool extends EmberObject {
|
||||
* get html(){
|
||||
* return rawRenderGlimmer(this, "div", <template>Hello {{@data.name}}</template>, { name: this.name });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* And then this can be invoked from any other raw view (including raw plugin outlets) like:
|
||||
*
|
||||
* ```hbs
|
||||
* {{raw "something-cool" name="david"}}
|
||||
* ```
|
||||
*/
|
||||
export default function rawRenderGlimmer(owner, renderInto, component, data) {
|
||||
const renderGlimmerService = getOwner(owner).lookup("service:render-glimmer");
|
||||
|
||||
counter++;
|
||||
const id = `_render_glimmer_${counter}`;
|
||||
const [type, ...classNames] = renderInto.split(".");
|
||||
|
||||
schedule("afterRender", () => {
|
||||
const element = document.getElementById(id);
|
||||
const componentInfo = {
|
||||
element,
|
||||
component,
|
||||
data,
|
||||
};
|
||||
renderGlimmerService.add(componentInfo);
|
||||
});
|
||||
|
||||
return `<${type} id="${id}" class="${classNames.join(" ")}"></${type}>`;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import Service from "@ember/service";
|
||||
import { TrackedSet } from "@ember-compat/tracked-built-ins";
|
||||
|
||||
/**
|
||||
* This service is responsible for rendering glimmer components into HTML generated
|
||||
* by raw-hbs. It is not intended to be used directly.
|
||||
*
|
||||
* See discourse/lib/raw-render-glimmer.js for usage instructions.
|
||||
*/
|
||||
export default class RenderGlimmerService extends Service {
|
||||
_registrations = new TrackedSet();
|
||||
|
||||
add(info) {
|
||||
this._registrations.add(info);
|
||||
}
|
||||
|
||||
remove(info) {
|
||||
this._registrations.delete(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes registrations for elements which are no longer in the DOM.
|
||||
*/
|
||||
cleanup() {
|
||||
this._registrations.forEach((info) => {
|
||||
if (!document.body.contains(info.element)) {
|
||||
this.remove(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -101,6 +101,7 @@
|
|||
<DialogHolder />
|
||||
<TopicEntrance />
|
||||
<ComposerContainer />
|
||||
<RenderGlimmerContainer />
|
||||
|
||||
{{#if this.showFooterNav}}
|
||||
<FooterNav />
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { render, settled } from "@ember/test-helpers";
|
||||
import {
|
||||
addRawTemplate,
|
||||
removeRawTemplate,
|
||||
} from "discourse-common/lib/raw-templates";
|
||||
import raw from "discourse/helpers/raw";
|
||||
import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
|
||||
import RenderGlimmerContainer from "discourse/components/render-glimmer-container";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import Component from "@glimmer/component";
|
||||
|
||||
// We don't have any way to actually compile raw hbs inside tests, so this is only testing
|
||||
// the helper itself, not the actual rendering of templates.
|
||||
module("Integration | Helper | raw", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.afterEach(() => {
|
||||
removeRawTemplate("raw-test");
|
||||
});
|
||||
|
||||
test("can render a template", async function (assert) {
|
||||
addRawTemplate("raw-test", (params) => `raw test ${params.someArg}`);
|
||||
|
||||
await render(<template>
|
||||
<span>{{raw "raw-test" someArg="foo"}}</span>
|
||||
</template>);
|
||||
|
||||
assert.dom(`span`).hasText("raw test foo");
|
||||
});
|
||||
|
||||
test("can render glimmer inside", async function (assert) {
|
||||
let willDestroyCalled = false;
|
||||
|
||||
class MyComponent extends Component {
|
||||
<template>
|
||||
Hello from glimmer {{@data.someArg}}
|
||||
</template>
|
||||
|
||||
willDestroy() {
|
||||
willDestroyCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
addRawTemplate("raw-test", (params) =>
|
||||
rawRenderGlimmer(this, "div", MyComponent, { someArg: params.someArg })
|
||||
);
|
||||
|
||||
class TestState {
|
||||
@tracked showRawTemplate = true;
|
||||
}
|
||||
|
||||
const testState = new TestState();
|
||||
|
||||
const renderGlimmerService = getOwner(this).lookup(
|
||||
"service:render-glimmer"
|
||||
);
|
||||
|
||||
await render(<template>
|
||||
{{! RenderGlimmerContainer is normally rendered by application.hbs
|
||||
but this is not an acceptance test so we gotta include it manually }}
|
||||
<RenderGlimmerContainer />
|
||||
<span>
|
||||
{{#if testState.showRawTemplate}}
|
||||
{{raw "raw-test" someArg="foo"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</template>);
|
||||
|
||||
assert.dom(`span`).hasText("Hello from glimmer foo");
|
||||
assert.strictEqual(
|
||||
renderGlimmerService._registrations.size,
|
||||
1,
|
||||
"renderGlimmer service has one registration"
|
||||
);
|
||||
|
||||
testState.showRawTemplate = false;
|
||||
await settled();
|
||||
|
||||
assert.dom(`span`).hasText("");
|
||||
assert.strictEqual(
|
||||
renderGlimmerService._registrations.size,
|
||||
0,
|
||||
"renderGlimmer service has no registrations"
|
||||
);
|
||||
|
||||
assert.true(willDestroyCalled, "component was cleaned up correctly");
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue