从主题或插件中使用插件出口连接器

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:

:art: {theme}/javascripts/discourse/connectors/{outlet-name}/{connector-name}.hbs

:electric_plug: {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

<div class="alert alert-info">
  This topic was created by a member of the
  <a href="https://discourse.org/team">Discourse Team</a>
</div>

Some plugin outlets will automatically wrap your content in an HTML element. The element type is defined by @connectorTagName on the <PluginOutlet>.

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:

{{log @outletArgs}}

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:

<div class="alert alert-info">
  This topic was created by
  {{@outletArgs.model.details.created_by.username}}
  (a member of the
  <a href="https://discourse.org/team">Discourse Team</a>)
</div>

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 { 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:

<div class="alert alert-info">
  This topic was created by
  {{this.displayName}}
  (a member of the
  <a href="https://discourse.org/team">Discourse Team</a>)
</div>

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.

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.


This document is version controlled - suggest changes on github.

39 个赞

现在已经被弃用了,对吧?
弃用通知:通过 registerConnectorClass 定义连接器类已被弃用。请参阅 https://meta.discourse.org/t/32727 以了解更现代的模式。[deprecation id: discourse.register-connector-class-legacy]

没错;你可以改用 api.renderInOutlet:slight_smile:

https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L982-L1008

1 个赞

我认为这比那要复杂一点:sweat_smile:
https://github.com/Firepup6500/discourse-custom-profile-link/blob/master/common/head_tag.html

别担心;我稍后会尽力帮助您(我现在需要睡觉)。 :smile:

1 个赞

谢谢! (抱歉代码一团糟,上次修改时我只想让它能工作 :sweat_smile:

1 个赞

我有点力不从心,我的主题 HEAD 文件中使用的组件收到了一个通知。我不确定如何用 api.renderInOutlet 重写。\n\n\n\n const ajax = require('discourse/lib/ajax').ajax;\n const Topic = require('discourse/models/topic').default;\n // 我们正在使用 Discourse 的 ajax 和 Topic 模型\n\n api.registerConnectorClass('above-main-container', 'featured-topics', {\n // above-main-container 是插件出口,\n // featured-topics 是你的自定义组件名称\n\n setupComponent(args, component) {\n\n // 剩余代码如下\n\n我猜我尝试用 api.renderInOutlet 替换 api.registerConnectorClass,但它出错了。我在这方面不是主题编码专家。感谢任何帮助。

您可以在此处看到一个示例:

StatBanner 是在 components 目录中定义的本地类:

在您的情况下,那将是 api.renderInOutlet("above-main-container", YourClass)

我不认为您可以在 HEAD 文件中执行此操作。您应该将代码拆分成多个文件

我鼓励您使用Discourse Theme CLI,因为它将使开发 Theme component 更加容易!

您的 Theme Component 是公开的吗?

3 个赞

我的 .js 文件中有什么方法可以获取当前的 outlet 吗?

我认为那是不可能的。

以前在经典组件中可以检查 parentView

但是该属性已被弃用。

1 个赞

您说的“获取当前插座”是什么意思?您想要插座的名称吗?还是其他什么?

我在我的问题 (这里) 中找到了一个解决方案,它大致是这样的:

  • 为 3 个不同的出口创建 .hbs.js 文件。
  • 在每个 JS 文件中,检查它正在使用的出口是否是设置 banner_location 的值。
  • 如果是,则显示横幅。如果不是,则隐藏横幅。
1 个赞

太棒了!看起来你决定使用 api.renderInOutlet,并为 outlet 名称使用了动态值 :chefs_kiss:

1 个赞