FEATURE: Introduce `api.renderInOutlet` (#23719)
Until now, plugins/themes had to follow very specific directory structures to set up plugin outlet connectors. This commit introduces a new `api.renderInOutlet` API which makes things much more flexible. Any Ember component definition can be passed to this API, and will then be rendered into the named outlet. For example: ```javascript import MyComponent from "discourse/plugins/my-plugin/components/my-component"; api.renderInOutlet('user-profile-primary', MyComponent); ``` When using this API alongside the gjs file format, components can be defined inline like ```javascript api.renderInOutlet('user-profile-primary', <template>Hello world</template>); ```
This commit is contained in:
parent
8a7b5b00ea
commit
c00fd3e17d
|
@ -77,7 +77,10 @@ import { addUsernameSelectorDecorator } from "discourse/helpers/decorate-usernam
|
|||
import { addWidgetCleanCallback } from "discourse/components/mount-widget";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import { disableNameSuppression } from "discourse/widgets/poster-name";
|
||||
import { extraConnectorClass } from "discourse/lib/plugin-connectors";
|
||||
import {
|
||||
extraConnectorClass,
|
||||
extraConnectorComponent,
|
||||
} from "discourse/lib/plugin-connectors";
|
||||
import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
|
||||
import { h } from "virtual-dom";
|
||||
import { includeAttributes } from "discourse/lib/transform-post";
|
||||
|
@ -134,7 +137,7 @@ import { isTesting } from "discourse-common/config/environment";
|
|||
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
||||
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
||||
|
||||
export const PLUGIN_API_VERSION = "1.12.0";
|
||||
export const PLUGIN_API_VERSION = "1.13.0";
|
||||
|
||||
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
|
||||
function canModify(klass, type, resolverName, changes) {
|
||||
|
@ -940,6 +943,31 @@ class PluginApi {
|
|||
extraConnectorClass(`${outletName}/${connectorName}`, klass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a component to be rendered in a particular outlet.
|
||||
*
|
||||
* For example, if the outlet is `user-profile-primary`, you could register
|
||||
* a component like
|
||||
*
|
||||
* ```javascript
|
||||
* import MyComponent from "discourse/plugins/my-plugin/components/my-component";
|
||||
* api.renderInOutlet('user-profile-primary', MyComponent);
|
||||
* ```
|
||||
*
|
||||
* Alternatively, a component could be defined inline using gjs:
|
||||
*
|
||||
* ```javascript
|
||||
* api.renderInOutlet('user-profile-primary', <template>Hello world</template>);
|
||||
* ```
|
||||
*
|
||||
* @param {string} outletName - Name of plugin outlet to render into
|
||||
* @param {Component} klass - Component class definition to be rendered
|
||||
*
|
||||
*/
|
||||
renderInOutlet(outletName, klass) {
|
||||
extraConnectorComponent(outletName, klass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a button to display at the bottom of a topic
|
||||
*
|
||||
|
|
|
@ -10,9 +10,11 @@ import templateOnly from "@ember/component/template-only";
|
|||
let _connectorCache;
|
||||
let _rawConnectorCache;
|
||||
let _extraConnectorClasses = {};
|
||||
let _extraConnectorComponents = {};
|
||||
|
||||
export function resetExtraClasses() {
|
||||
_extraConnectorClasses = {};
|
||||
_extraConnectorComponents = {};
|
||||
}
|
||||
|
||||
// Note: In plugins, define a class by path and it will be wired up automatically
|
||||
|
@ -21,6 +23,17 @@ export function extraConnectorClass(name, obj) {
|
|||
_extraConnectorClasses[name] = obj;
|
||||
}
|
||||
|
||||
export function extraConnectorComponent(outletName, klass) {
|
||||
if (!hasInternalComponentManager(klass)) {
|
||||
throw new Error("klass is not an Ember component");
|
||||
}
|
||||
if (outletName.includes("/")) {
|
||||
throw new Error("invalid outlet name");
|
||||
}
|
||||
_extraConnectorComponents[outletName] ??= [];
|
||||
_extraConnectorComponents[outletName].push(klass);
|
||||
}
|
||||
|
||||
const OUTLET_REGEX =
|
||||
/^discourse(\/[^\/]+)*?(?<template>\/templates)?\/connectors\/(?<outlet>[^\/]+)\/(?<name>[^\/\.]+)$/;
|
||||
|
||||
|
@ -87,7 +100,9 @@ class ConnectorInfo {
|
|||
|
||||
get connectorClass() {
|
||||
if (this.classModule) {
|
||||
return require(this.classModule).default;
|
||||
return this.classModule;
|
||||
} else if (this.classModuleName) {
|
||||
return require(this.classModuleName).default;
|
||||
} else {
|
||||
return _extraConnectorClasses[`${this.outletName}/${this.connectorName}`];
|
||||
}
|
||||
|
@ -101,7 +116,7 @@ class ConnectorInfo {
|
|||
|
||||
get humanReadableName() {
|
||||
return `${this.outletName}/${this.connectorName} (${
|
||||
this.classModule || this.templateModule
|
||||
this.classModuleName || this.templateModule
|
||||
})`;
|
||||
}
|
||||
|
||||
|
@ -160,7 +175,7 @@ function buildConnectorCache() {
|
|||
if (isTemplate) {
|
||||
info.templateModule = moduleName;
|
||||
} else {
|
||||
info.classModule = moduleName;
|
||||
info.classModuleName = moduleName;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -169,6 +184,18 @@ function buildConnectorCache() {
|
|||
_connectorCache[info.outletName] ??= [];
|
||||
_connectorCache[info.outletName].push(info);
|
||||
}
|
||||
|
||||
for (const [outletName, components] of Object.entries(
|
||||
_extraConnectorComponents
|
||||
)) {
|
||||
for (const klass of components) {
|
||||
const info = new ConnectorInfo(outletName);
|
||||
info.classModule = klass;
|
||||
|
||||
_connectorCache[info.outletName] ??= [];
|
||||
_connectorCache[info.outletName].push(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function connectorsExist(outletName) {
|
||||
|
|
|
@ -3,14 +3,16 @@ import { count, exists, query } from "discourse/tests/helpers/qunit-helpers";
|
|||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { click, render, settled } from "@ember/test-helpers";
|
||||
import { action } from "@ember/object";
|
||||
import { extraConnectorClass } from "discourse/lib/plugin-connectors";
|
||||
import {
|
||||
extraConnectorClass,
|
||||
extraConnectorComponent,
|
||||
} from "discourse/lib/plugin-connectors";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper";
|
||||
import { getOwner } from "@ember/application";
|
||||
import Component from "@glimmer/component";
|
||||
import templateOnly from "@ember/component/template-only";
|
||||
import { withSilencedDeprecationsAsync } from "discourse-common/lib/deprecated";
|
||||
import { setComponentTemplate } from "@glimmer/manager";
|
||||
import sinon from "sinon";
|
||||
|
||||
const TEMPLATE_PREFIX = "discourse/plugins/some-plugin/templates/connectors";
|
||||
|
@ -57,13 +59,13 @@ module("Integration | Component | plugin-outlet", function (hooks) {
|
|||
registerTemporaryModule(
|
||||
`${TEMPLATE_PREFIX}/test-name/hello`,
|
||||
hbs`<span class='hello-username'>{{this.username}}</span>
|
||||
<button class='say-hello' {{on "click" (action "sayHello")}}></button>
|
||||
<button class='say-hello-using-this' {{on "click" this.sayHello}}></button>
|
||||
<button type="button" class='say-hello' {{on "click" (action "sayHello")}}></button>
|
||||
<button type="button" class='say-hello-using-this' {{on "click" this.sayHello}}></button>
|
||||
<span class='hello-result'>{{this.hello}}</span>`
|
||||
);
|
||||
registerTemporaryModule(
|
||||
`${TEMPLATE_PREFIX}/test-name/hi`,
|
||||
hbs`<button class='say-hi' {{on "click" (action "sayHi")}}></button>
|
||||
hbs`<button type="button" class='say-hi' {{on "click" (action "sayHi")}}></button>
|
||||
<span class='hi-result'>{{this.hi}}</span>`
|
||||
);
|
||||
registerTemporaryModule(
|
||||
|
@ -427,13 +429,9 @@ module(
|
|||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
const template = hbs`<span class='gjs-test'>Hello world</span>`;
|
||||
const component = templateOnly();
|
||||
setComponentTemplate(template, component);
|
||||
|
||||
registerTemporaryModule(
|
||||
`${CLASS_PREFIX}/test-name/my-connector`,
|
||||
component
|
||||
<template><span class="gjs-test">Hello world</span></template>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -444,3 +442,21 @@ module(
|
|||
});
|
||||
}
|
||||
);
|
||||
|
||||
module(
|
||||
"Integration | Component | plugin-outlet | extraConnectorComponent",
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
extraConnectorComponent("test-name", <template>
|
||||
<span class="gjs-test">Hello world from gjs</span>
|
||||
</template>);
|
||||
});
|
||||
|
||||
test("renders the component in the outlet", async function (assert) {
|
||||
await render(hbs`<PluginOutlet @name="test-name" />`);
|
||||
assert.dom(".gjs-test").hasText("Hello world from gjs");
|
||||
});
|
||||
}
|
||||
);
|
|
@ -7,6 +7,12 @@ in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.13.0] - 2023-10-05
|
||||
|
||||
### Added
|
||||
|
||||
- Introduces `renderInOutlet` API for rendering components into plugin outlets
|
||||
|
||||
## [1.12.0] - 2023-09-06
|
||||
|
||||
### Added
|
||||
|
|
Loading…
Reference in New Issue