Plugin Outlets
Discourse includes hundreds of Plugin Outlets which can be used to inject new content or replace existing contend in the Discourse UI. 'Outlet arguments' are made available so that content can be customized based on the context.
Choosing an outletâ
To find the name of a plugin outlet, search Discourse core for "<PluginOutlet
", or use the plugin outlet locations theme component. (e.g. topic-above-posts
).
Wrapper outletsâ
Some outlets in core look like <PluginOutlet @name="foo" />
. These allow you to inject new content. Other outlets will 'wrap' an existing core implementation like this
<PluginOutlet @name="foo">
core implementation
</PluginOutlet>
Defining a connector for this kind of 'wrapper' outlet will replace the core implementation. Only one active theme/plugin can contribute a connector for a wrapper plugin outlet.
For wrapper plugin outlets, you can render the original core implementation using the {{yield}}
keyword. This can be helpful if you only want to replace the core implementation under certain conditions, or if you would like to wrap it in something.
Defining the templateâ
Once you've chosen an outlet, decide on a name for your connector. This needs to be unique across all themes / plugins installed on a given community. e.g. brand-official-topics
In your theme / plugin, define a new handlebars template with a path formatted like this:
- đ¨ Theme
- đ Plugin
{theme}/javascripts/discourse/connectors/{outlet-name}/{connector-name}.hbs
{plugin}/assets/javascripts/discourse/connectors/{outlet-name}/{connector-name}.hbs
The content of these files will be rendered as an Ember Component. For general information on Ember / Handlebars, check out the Ember guides.
For our hypothetical "brand official topics" connector, the template might look like
Some plugin outlets will automatically wrap your content in an HTML element. The element type is defined by @connectorTagName
on the <PluginOutlet>
.
âšī¸ Removing the automatic wrapper element
To use this new rendering technique on existing plugin outlets, create an adjacent JS file with this content:
import templateOnly from "@ember/component/template-only";
export default templateOnly();
If you need custom logic as well, see the other âšī¸ sections below.
âšī¸ Defining template via theme
We recommend using a dedicated file for your connector template. However, Discourse does still support defining a template via a <script>
tag in your theme's </head>
section. In that case, a definition would look like
<script type='text/x-handlebars' data-template-name='/connectors/{outlet-name}/{connector-name}'>
Template content here
</script>
Using outlet argumentsâ
Plugin Outlets provide information about the surrounding context via @outletArgs
. The arguments passed to each outlet vary. An easy way to view the arguments is to add this to your template:
This will log the arguments to your browser's developer console. They will appear as a Proxy
object - to explore the list of arguments, expand the [[Target]]
of the proxy.
In our topic-above-posts
example, the rendered topic is available under @outletArgs.model
. So we can add the username of the team member like this:
âšī¸ Legacy ways to access arguments
In many Plugin Outlets, by default it is possible to access arguments using {{argName}}
or {{this.argName}}
. For now, this still works in existing outlets.
New plugin outlets (with @defaultGlimmer={{true}}
) render connectors as 'template only glimmer components', which do not have a this
context. Eventually, existing Plugin Outlets will also be migrated to this pattern. The @outletArgs
technique is best because it will work consistently in both classic and glimmer plugin outlets.
Adding more complex logicâ
Sometimes, a simple handlebars template is not enough. To add Javascript logic to your connector, you can define a Javascript file adjacent to your handlebars template. This file should export a component definition. This functions just the same as any other component definition, and can include service injections.
Defining a component like this will remove the automatic connectorTagName
wrapper element, so you may want to re-introduce an element of the same type in your hbs file.
In our topic-above-posts
example, we may want to render the user differently based on the 'prioritize username in ux' site setting. A component definition for that might look something like this:
.../connectors/topic-above-posts/brand-official-topic.js
:
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
export default class BrandOfficialTopics extends Component {
@service siteSettings;
get displayName() {
const user = this.args.outletArgs.model.details.created_by;
if (this.siteSettings.prioritize_username_in_ux) {
return user.username;
} else {
return user.name;
}
}
}
We can then update the template to reference the new getter:
âšī¸ Legacy ways to define complex logic
Conditional renderingâ
If you only want your content to be rendered under certain conditions, it's often enough to wrap your template with a handlebars {{#if}}
block. If that's not enough, you may want to use the shouldRender
hook to control whether your connector template is rendered at all.
Firstly, ensure you have a .js
connector definition as described above. Then, add a static shouldRender()
function. Extending our example:
import Component from "@glimmer/component";
import { getOwner } from "discourse-common/lib/get-owner";
export default class BrandOfficialTopics extends Component {
static shouldRender(outletArgs, helper){
const firstPost = outletArgs.model.postStream.posts[0];
return firstPost.primary_group_name === "team";
}
// ... (any other logic)
}
Now the connector will only be rendered when the first post of the topic was created by a team member.
shouldRender
is evaluated in a Glimmer autotracking context. Future changes to any referenced properties (e.g. outletArgs
) will cause the function to be re-evaluated.
âšī¸ shouldRender for templateOnly connectors
If you'd like to define a shouldRender
function without the overhead of a full component, you can do something like this:
import templateOnly from "@ember/component/template-only";
export default Object.assign(templateOnly(), {
shouldRender(outletArgs, helper){
// Logic here
}
});
âšī¸ Legacy shouldRender implementations
Autotracking: Before Discourse 3.1, shouldRender
would only be evaluated during initial render. Changes to referenced properties would not cause the function to be re-evaluated.
Non-class syntax: For now, defining a shouldRender
function in a plain (non-class) javascript object is still supported, but we recommend moving towards a class-based or templateOnly-based syntax going forward.
Introducing new outletsâ
If you need an outlet that doesn't yet exist, please feel free to make a pull request, or open a topic in #dev.