Theme Developer Tutorial: 6. Using the JS API

In the last couple of chapters, we’ve explored how to use the JavaScript API to render content into outlets. renderInOutlet is the most commonly-used API, but there are a ton more! In this chapter we’ll try out a few of them, and show you how to discover more.

Common API methods

getCurrentUser()

api.getCurrentUser() will return information about the current user, or null if nobody is logged in. This can be used for all sorts of things, including per-group logic, or rendering a user’s username into the UI.

For group membership checks backed by theme settings, use the resolve_group_membership option in your theme’s settings.yml file instead of checking currentUser.groups. This avoids leaking or missing hidden group memberships.

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
  console.log("Current user is", api.getCurrentUser());
});

headerIcons

api.headerIcons will allow you to add, remove and re-arrange icons in the header. For example, to add a new icon before the search icon, you’d do something like

import DButton from "discourse/components/d-button";
import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
  const sayHello = () => {
    alert("Hello, world!");
  };
  api.headerIcons.add(
    "my-unique-icon-name",
    <template>
      <li>
        <DButton
          @action={{sayHello}}
          @icon="wand-magic"
          class="icon btn-flat"
        />
      </li>
    </template>,
    { before: "search" }
  );
});

replaceIcon()

api.replaceIcon(source, destination);

With this method, you can easily replace any Discourse icon with another. For example, we have a theme component that replaces the heart icon for like with a thumbs-up icon

decorateCookedElement()

api.decorateCookedElement() allows you to customize the rendered content of Discourse posts. This can be used for anything from simple formatting changes, all the way up to advanced integrated UIs like the built-in ‘poll’ plugin.

The API should be passed a callback function which will be run for every post when it’s rendered to the screen. The first argument to the callback will be the post’s root HTML element, and the second will be a helper.

A simple example which appends content to every post would look like:

api.decorateCookedElement((element, helper) => {
  const myNewParagraph = document.createElement("p");
  myNewParagraph.textContent = "Hello, this is appended to every post!";
  element.appendChild(myNewParagraph);
});

Or for a more advanced UI, you can render a glimmer component into a post. For example, to render the counter component we authored earlier into every post, you could do something like this:

import { apiInitializer } from "discourse/lib/api";
import CustomWelcomeBanner from "../components/custom-welcome-banner";

export default apiInitializer((api) => {
  api.decorateCookedElement((element, helper) => {
    const counterWrapper = helper.renderGlimmer(
      "div.my-counter",
      CustomWelcomeBanner
    );
    element.appendChild(counterWrapper);
  });
});

helper.getPost() will return the current post, and can be used to build conditional logic into these decorateCookedElement callbacks. console.log the post to see what’s available.

registerValueTransformer()

api.registerValueTransformer allows you to inject logic into predefined parts of the Discourse JavaScript application. For example, you can add a "home-logo-href" transformer to link the logo to example.com:

api.registerValueTransformer("home-logo-href", () => "https://example.com");

For more information on Transformers, check out the dedicated guide

Finding more JS API methods

All the available APIs are listed in the plugin-api.gjs source code in Discourse core, along with a short description and examples.

That’s it for this chapter, and almost the end of the tutorial. Let’s wrap things up in the conclusion.


This document is version controlled - suggest changes on github.

3개의 좋아요

특정 카테고리 내의 게시글에만 영향을 주고 싶습니다. 어떤 요소가 주어진 카테고리에 있는지 어떻게 확인할지 모르겠습니다. element.parentElement을 사용해서 body까지 올라가는 방법을 생각해보았지만, 작동하지 않는 것 같습니다. 이 문제를 해결하기 전에 사용자가 특정 그룹에 속하는지 확인하여 변경할지 결정할 수 있다는 것을 깨달았고, 따라서 currentUser를 사용할 수 있었습니다. 하지만 현재 저는 다음과 같이 처리하고 있습니다:

if (!cooked.innerHTML.includes(newFormText)) {
      console.log("fixSubmittedForm no new form text");
      return;
    }

변경할지 결정하기 위해 모든 게시글 전체를 검색하는 것은 좋지 않은 방법이라고 생각되지 않나요? 호스팅된 고객이라면 플러그인을 통해 이 문제를 여러 가지 방법으로 해결했을 텐데 말입니다.

1개의 좋아요

위 decorateCookedElement 예제들을 보면, 콜백에는 두 개의 인자가 있습니다: element와 helper입니다.

helper.getModel()을 호출하면, 모든 게시물/토픽/카테고리 정보를 포함하고 있는 게시물 모델에 접근할 수 있습니다.

예시를 더 보고 싶으시다면 all-the-*에서 "getModel()"를 검색해 보시길 권장합니다.

2개의 좋아요

아. 하아. 이 말을 처음 들은 게 아닌 것 같은데.

그래도 이번엔 잘 될 거야! 정말 많이 고마워.

다시 안 물어보려고 할게!

수정: 이번엔 좀 배운 것 같아!

async function fixSubmittedForm(element, helper, category_id) {
  if (helper.getModel().topic.category_id !== category_id) {
    return;
  }
 .....
3개의 좋아요