Using Plugin Outlet Connectors from a Theme or Plugin

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 connector

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 .gjs connector with a path formatted like this:

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

:electric_plug: {plugin}/assets/javascripts/discourse/connectors/{outlet-name}/{connector-name}.gjs

The content of these files will be rendered as an Ember Component. For general information on Ember and the .gjs format, check out the Ember guides.

For our hypothetical “brand official topics” connector, the file might look like

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

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:

<template>
  <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>
</template>

Adding more complex logic

Sometimes, a simple template is not enough. To add Javascript logic to your connector, upgrade your .gjs file to export a class-based component. This functions just the same as any other component definition, and can include service injections.

In our topic-above-posts example, we may want to render the user differently based on the ‘prioritize username in ux’ site setting. The .gjs file might look something like this:

.../connectors/topic-above-posts/brand-official-topic.gjs:

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;
    }
  }

  <template>
    <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>
  </template>
}

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 class-based .gjs connector as described above. Then, add a static shouldRender() function. Extending our example:

import Component from "@glimmer/component";

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)

  <template>
    {{! ... }}
  </template>
}

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 Development.


This document is version controlled - suggest changes on github.

39개의 좋아요
Using discourse's plugin outlets
Add HTML (Link) Next To Logo
What is the best way to integrate member applications?
Group Semantics
Can I put the search form at the top of our 404 page?
Native theme support
How to show user total post count beside name
Developing Discourse Plugins - Part 2 - Connect to a plugin outlet
Split up theme Javascript into multiple files
Feedback on "on-discourse" javascript for setting up custom JS for each page?
Topic-timeline api.decorateWidget call has stopped working
Developing Discourse Plugins - Part 2 - Connect to a plugin outlet
Developing Discourse Themes & Theme Components
How to add btn before "sign in"
Minimizing Maintenance on Theme Customizations
How to add custom fields to models
Tags at the top of the topic list in a Category
I want to insert images (banner) between the topic answers. How do I start?
How to add a link shortcut to the area under the title
Baidu Search
Add Banner/HTML (Widget) before reply button
Upcoming Header Changes - Preparing Themes and Plugins
Upgrading Discourse to Ember 4
Converting modals from legacy controllers to new DModal component API
Need help integrating code wrote on Edittext to the Discourse
Add link to external SSO profile to profile page
How to add a custom button in user profile card?
How to add a custom button in user profile card?
(not recommended) Overriding Discourse templates from a Theme or Plugin
How to override the site-header.hbs file from custom theme?
Upcoming topic-list changes - how to prepare themes and plugins
Working with .erb templates in a plugin
How to Integrate a Custom Plugin in discourse UI
Templating of my "component" broke. How do I fix it?
Templating of my "component" broke. How do I fix it?
Modernizing inline script tags for templates & JS API
Custom Components -- add button or text at any plugin outlet
(not recommended) Overriding Discourse templates from a Theme or Plugin
Adding "latest topics" header in the main interface
Display Tags inline with thread title, instead of being on the bottom line
Settings not appearing
Discourse view file update does not reflect in browser
Discourse view file update does not reflect in browser
Add likes and views to search display
Using template hbs to add HTML content to a plugin outlet
Adding a billing section in the member section
Adding a billing section in the member section
How to modify the header HTML, but still remaining the default founctions
Removing support for "template overrides" and mobile-specific templates
How can i add image in login and register box
Most “traditional” or classic forum Category listing
Customizing the topic list
Newbie help accessing code
How to add custom html next to logo using discourse plugin methods
Using the DModal API to render Modal windows (aka popups/dialogs) in Discourse
Air Theme
How to create a plugin with backend API calls to populate composer while drafting?
How to add a custom url text link on the login page
Add Text In Header Beside Logo

이제 비권장(deprecated) 처리된 것이 맞나요?
Deprecation notice: Defining connector classes via registerConnectorClass is deprecated. See https://meta.discourse.org/t/32727 for more modern patterns. [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로 어떻게 다시 작성해야 할지 잘 모르겠어요.

  const ajax = require('discourse/lib/ajax').ajax;
  const Topic = require('discourse/models/topic').default;
  // We're using ajax and the Topic model from Discourse

  api.registerConnectorClass('above-main-container', 'featured-topics', {
    // above-main-container is the plugin outlet,
    // featured-topics is your custom component name

    setupComponent(args, component) {

   // rest of code follows

api.registerConnectorClassapi.renderInOutlet로 교체해 보려 했지만 실패했어요. 테마 코딩에는 그다지 전문가가 아니에요. 도움이 주시면 감사하겠습니다.

여기 예시를 확인할 수 있습니다:

StatBannercomponents 디렉터리에 정의된 네이티브 클래스입니다:

질문자의 경우, api.renderInOutlet("above-main-container", YourClass)를 사용해야 합니다.

HEAD 파일에서 이를 수행할 수 있다고 생각하지 않습니다. 코드를 여러 파일로 분리해야 합니다.

Discourse Theme CLI 사용을 권장합니다. Theme 컴포넌트를 개발하는 것이 훨씬 쉬워지니까요!

질문자의 Theme 컴포넌트는 공개되어 있나요?

3개의 좋아요

.js 파일에서 현재 아웃렛을 가져오는 방법이 있을까요?

그것은 불가능하다고 생각합니다.

Classic Components에서는 parentView를 검사할 수 있었습니다.

하지만 해당 프로퍼티는 비추천(deprecated)되었습니다.

Get glimmer component access to stuff from the parent - #5 by david

1개의 좋아요

“현재 아웃렛”이라는 표현이 무슨 뜻인가요? 아웃렛의 이름을 원하시는 건가요? 아니면 다른 무언가를 원하시는 건가요?

내 문제를 해결하는 방법을 찾았어요(여기)인데, 대략 다음과 같은 방식이었어요:

  • 서로 다른 3개의 아웃렛을 위해 .hbs.js 파일을 생성합니다.
  • 각 JS 파일에서 해당 아웃렛이 banner_location 설정 값과 일치하는지 확인합니다.
  • 일치하면 배너를 표시하고, 일치하지 않으면 배너를 숨깁니다.
1개의 좋아요

멋지네요! api.renderInOutlet을 사용하고, 아웃렛 이름에 동적 값을 사용하는 것으로 정하신 것 같습니다 :chefs_kiss:

1개의 좋아요