# Setting default tag per category in a theme component

**URL:** https://meta.discourse.org/t/setting-default-tag-per-category-in-a-theme-component/206458
**Category:** Development
**Created:** [October 19, 2021, 5:28pm UTC](https://meta.discourse.org/t/setting-default-tag-per-category-in-a-theme-component/206458 "2021-10-19T17:28:12Z")
**Posts on this page:** 4
**Page:** 1

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [October 19, 2021, 5:28pm UTC](https://meta.discourse.org/t/setting-default-tag-per-category-in-a-theme-component/206458/1 "2021-10-19T17:28:12Z")

</div>

If you’re visiting `/tags/category-slug/tag-name` and click the New Topic button, the composer has the tag pre-set, as described here:

> [@Default tagging](https://meta.discourse.org/t/default-tagging/85372/3):
>
> his already works, try creating new topic from: [Topics tagged faq-material](https://meta.discourse.org/tags/c/support/faq-material)

This is awesome. But now I (and at least one other person) want to be able to set this behavior with a default tag when visiting `/c/cat-slug/cat-id`. It seems like a theme component should be able to target that button and either modify it, or hide it and add a new button (there’s a plugin outlet right there that I can’t find now, but saw a minute ago).

Can someone give me a hint?

---

<div class="post-metadata">

### Author: ![Johani](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/johani/32/176920_2.png) [@Johani](https://meta.discourse.org/u/Johani)
#### Post date: [October 20, 2021, 10:01pm UTC](https://meta.discourse.org/t/setting-default-tag-per-category-in-a-theme-component/206458/2 "2021-10-20T22:01:00Z")

</div>

Is it supposed to only work on one particular category, or would you need it to support a “default tag” for many categories where that tag is different for each of them?

---

<div class="post-metadata">

### Author: ![pfaffman](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pfaffman/32/120154_2.png) [@pfaffman](https://meta.discourse.org/u/pfaffman)
#### Post date: [October 21, 2021, 5:07am UTC](https://meta.discourse.org/t/setting-default-tag-per-category-in-a-theme-component/206458/3 "2021-10-21T05:07:03Z")

</div>

I imagine that I’d make a setting to have a default tag for a few categories. I probably can do that, but don’t know where/how to change the create topic button to have it include the default tag.

---

<div class="post-metadata">

### Author: ![Johani](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/johani/32/176920_2.png) [@Johani](https://meta.discourse.org/u/Johani)
#### Post date: [October 22, 2021, 3:17pm UTC](https://meta.discourse.org/t/setting-default-tag-per-category-in-a-theme-component/206458/5 "2021-10-22T15:17:45Z")

</div>

TL;DR skip to working code [here](#heading--final)

* * *

When you’re viewing a page that has the + New Topic, you can check the HTML via the inspector.

If you do that, you’ll notice that it has an id.

 ![devtools new topic button markup](https://global.discourse-cdn.com/meta/original/3X/6/a/6a22ace0dde1bc2ecadecdd1b15c89dfb3587af6.png)

Ids on HTML elements are supposed to be unique, as in… no two elements in the same view can share an HTML id attribute. So, this is enough to get us started.

If I search `"create-topic"` on Github, here’s what I see…

[Repository search results · GitHub](https://github.com/discourse/discourse/search?q=%22create-topic%22)

Note the filter on the left.

 ![github search filter](https://global.discourse-cdn.com/meta/original/3X/a/0/a0e89de5bf324548477ae4a342a199a5da651deb.png)

I know that I want to track down the HTML for the button so, handlebars, because I’m trying to track down the action it sends.

So, I select handlebars; then I see this.

[Repository search results · GitHub](https://github.com/discourse/discourse/search?l=Handlebars&q=%22create-topic%22)

There’s only one result in there, so we’re lucky. If there are more results, there are things you can do to narrow the list further, but that’s out of the scope for this topic.

So, let’s check that file.

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/templates/components/create-topic-button.hbs>

You will then see that the action that the button has is set like so

```plaintext
action=action

```

Well… that’s not very helpful… So, what now?

When you see `action=action` it means that the action is being passed to the component from a parent template.

Let’s try to see which templates have that component. So, we go to Github and search for the component name like it would be used in a template. For this example, we would use something like this `"{{create-topic-button"`

Not that I only added `{{COMPONENT_NAME` and skipped the rest. We don’t know the other arguments passed to it, so we want a generic search.

Here’s the result

[Repository search results · GitHub](https://github.com/discourse/discourse/search?q=%22%7B%7Bcreate-topic-button%22)

We get two results… one of them is in the styleguide plugin, so we simply ignore it. The other is in core. So, let’s see what that looks like

[discourse/app/assets/javascripts/discourse/app/templates/components/d-navigation.hbs at 292412f19610d49944f3e109aa7546ccd0553d6a · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/292412f19610d49944f3e109aa7546ccd0553d6a/app/assets/javascripts/discourse/app/templates/components/d-navigation.hbs#L48-L55)

```handlebars
  {{create-topic-button
    canCreateTopic=canCreateTopic
    action=(action "clickCreateTopicButton")
    disabled=createTopicButtonDisabled
    label=createTopicLabel
    btnClass=createTopicClass
    canCreateTopicOnTag=canCreateTopicOnTag
  }}

```

Ahh… we’re getting closer. Now you see that the action for the button is

```plaintext
action=(action "clickCreateTopicButton")

```

Now we need to find out what that action does. So, we search for the name of the action. Then filter down to `.js` files because we now want to see the definition of that action in the component’s js file.

[Repository search results · GitHub](https://github.com/discourse/discourse/search?l=JavaScript&q=%22clickCreateTopicButton%22)

Again, we get only one result, so let’s look at it.

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/components/d-navigation.js#L121-L127>

So, it looks like the action does one of two things. If the category is read-only and the user doesn’t already have a draft, it shows an alert. Otherwise, it calls a `createTopic()` method.

We’re interested in the latter, so let’s look at that.

If you search for `createTopic()` in that file (inline search, not Github)… you’ll notice that there’s only one reference for it. What gives? How is this component calling a method that’s not defined?

Well, the answer is higher up in the file.

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/components/d-navigation.js#L8>

What does this mean?

I don’t want to spend a lot of time here, but Ember uses Classes. Think of classes like reusable code bundles. All the line highlighted above means is:

Take the Ember `Component` bundle, add the `FilterModeMixin` bundle to it and let me add some more methods, or override some of the existing ones, to the result to create a new Ember component for my application.

So, now let’s go back to the action that we’re trying to track.

```javascript
clickCreateTopicButton() {
  if (this.categoryReadOnlyBanner && !this.hasDraft) {
    bootbox.alert(this.categoryReadOnlyBanner);
  } else {
    this.createTopic();
  }
},

```

It calls `this.createTopic()`. this is not a default Ember component method. It’s a custom Discourse method, so it has to come from `FilterModeMixin` What is `FilterModeMixin`? Well…it’s defined at the top of the file.

```javascript
import FilterModeMixin from "discourse/mixins/filter-mode";

```

So, I guess we have to go there.

[discourse/app/assets/javascripts/discourse/app/mixins/filter-mode.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/mixins/filter-mode.js)

Pause for a second and do an inline search for `createTopic()` in that file. I mean it. Stop reading and do it. I’ll wait… don’t cheat… I have my 👀 on you.

* * *

OK. You searched, and there were no results. What now?

What I described above is only one method for passing things down. If you don’t find what you’re looking for. Take a step back and try a different approach.

So, let’s recap… where are we right now? Before we got stuck, we were looking at the JS file for the `d-navigation` component. Let’s look at its template.

Again, we use `"{{COMPONENT_NAME"` and search.

[Repository search results · GitHub](https://github.com/discourse/discourse/search?q=%22%7B%7Bd-navigation%22)

This gives us four results…

Does this matter? Maybe. Does it matter for this case? No. We’re just trying to figure out where `createTopic()` comes from or what it is. So, let’s just go with the first result.

[discourse/app/assets/javascripts/discourse/app/templates/navigation/default.hbs at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/templates/navigation/default.hbs)

Would you look at that…

```javascript
createTopic=(route-action "createTopic")

```

Great…more jargon… _because everyone loves that_

Seriously though, let’s talk about route actions. What are they? Well. They’re route…actions? As in actions defined on the route. Why are they nice? Because routes in Discourse can be nested

Look at it this way

```plaintext
- route-1
  - route-1-1
  - route-1-2
  - route-1-3

```

If I have a shared component that I need to work on routes 111, 112, and 113 with different parameters, wouldn’t it be easier if I just defined use the same component in all of them and pass the same action? Then modify it for each route if needed?

That’s what route-actions do.

OK, let’s get back to the question. We were looking at

```javascript
createTopic=(route-action "createTopic")

```

in the `navigation/default` component.

Now we just have to figure out what the route is to check what that route action does.

You want to modify the behavior of the new topic button in the `/c/cat-slug/cat-id` pages. So, let’s visit one of those pages. For example: `http://localhost:4200/c/meta/6`

What is this route? Unless you’re really familiar with Discourse, you wouldn’t be able to tell. So, what now?

This is where the ember extension for your browser becomes handy.

[Install it here](https://github.com/emberjs/ember-inspector) if you don’t already have it. I’ll wait.  
(the link is a Github repository, but the description has the extension links for different browsers)

* * *

OK, now you have it installed, visit that page again `/c/cat-slug/cat-id` and look at the extension page.

Once that’s loaded, click on Routes, then toggle "Current Route only`

 ![Ember inspector route checker](https://global.discourse-cdn.com/meta/original/3X/9/2/92779111a9ef49b268b33c42c0323abb9b4ce16a.png)

Ahhh… look at that. We now know what route we’re on. We’re on `discovery.category`

But that’s not the whole story… it’s

`application > discovery > discovery.category`

Remember, routes are nested. So, what now?

I usually start at the very top. In this case, it would be the `application` route. Find the file for that route and search to see if the action is defined there.

[discourse/app/assets/javascripts/discourse/app/routes/application.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/routes/application.js)

It turns out that it isn’t… so we move down the nesting tree to the `discovery` route.

[discourse/app/assets/javascripts/discourse/app/routes/discovery.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/routes/discovery.js)

Search there…and… bingo!

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/routes/discovery.js#L75-L81>

Ok, so now we know what

```javascript
createTopic=(route-action "createTopic")

```

refers to. So let’s look at that action.

```javascript
createTopic() {
  if (this.get("currentUser.has_topic_draft")) {
    this.openTopicDraft();
  } else {
    this.openComposer(this.controllerFor("discovery/topics"));
  }
},

```

It looks like it’s doing one of two things. If the user has a draft, it opens it. If not, it calls `openComposer()` with a parameter. What’s next? We’ll you should already know the answer by now. We need to find out where `openComposer()` comes from or what it does.

So we search the file for `openComposer()` and… of course we don’t get any results. There’s no method in that route called `openComposer()`

What next? Remember the bit about Ember Classes? Let’s try that.

We have this at the top of the route file.

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/routes/discovery.js#L12>

This means that this route inherits all the methods from the `DiscourseRoute` “bundle” as well as those defined in the `OpenComposer` “bundle”

The `openComposer` is more likely to be what we want, so let’s look at that. Before we do that though… we need to look at how `openComposer` is defined in that file.

```javascript
import OpenComposer from "discourse/mixins/open-composer";

```

Look at the URL. It’s not an Ember component. It’s not a route; it’s not a model. It’s a mixin. What the hell is a mixin? The really, really short answer… it’s a bundle of reusable functions.

You define these in your mixin.

```javascript
add(number) {
  return number + 1
}

substract(number) {
  return number - 1
}

```

then add the mixin to your Ember component, then you can do something like this

```javascript
// starting value is 1
myMethod () {
  this.add(value) // returns 2
  this.substract(value) // returns 0 
}

```

So, how does this relate to what we’re trying to do?

Well, `open-composer` here.

```javascript
import OpenComposer from "discourse/mixins/open-composer";

```

is a mixin. One of the methods in that mixin is `OpenComposer()`

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/mixins/open-composer.js#L6-L23>

It’s okay if you feel confused about this. They share the same name - except that one starts with a capital letter, which indicates that it’s a Class.

They mean different things.

To understand this, you would need to understand that the name you give to your imported modules don’t matter (in this particular case), as long as they’re exported as “default”

Explaining this is a bit beyond the scope of this topic. All you need to know is that this.

`OpenComposer` here

[discourse/app/assets/javascripts/discourse/app/routes/discovery.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/routes/discovery.js#L12)

and `openComposer()` here

[discourse/app/assets/javascripts/discourse/app/mixins/open-composer.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/mixins/open-composer.js#L6)

are not the same thing.

OK… let’s recap.

```plaintext
New topic button HTML id < New topic button action < d-navigation component action < discovery-route action < OpenComposer mixin < openComposer() method

```

So… this is the method that eventually gets called when you click the + New Topic button on that route.

```javascript
openComposer(controller) {
  let categoryId = controller.get("category.id");
  if (
    categoryId &&
    controller.category.isUncategorizedCategory &&
    !this.siteSettings.allow_uncategorized_topics
  ) {
    categoryId = null;
  }
  this.controllerFor("composer").open({
    prioritizedCategoryId: categoryId,
    topicCategoryId: categoryId,
    action: Composer.CREATE_TOPIC,
    draftKey: controller.get("model.draft_key") || Composer.NEW_TOPIC_KEY,
    draftSequence: controller.get("model.draft_sequence") || 0,
  });
},

```

So let’s go back to your question.

We’ve established how you can figure out the action for that button on the `/c/cat-slug/cat-id`, but it seems different from what happens when you visit `/tags/category-slug/tag-name`, which is what you want to do.

So what’s the next step? Let’s look at what that route does to handle the `createTopic()` action.

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/routes/tag-show.js#L182-L208>

Well… you’ll notice that it handles the action differently.

for `/c/cat-slug/cat-id` it looks like this

```javascript
createTopic() {
  if (this.get("currentUser.has_topic_draft")) {
    this.openTopicDraft();
  } else {
    this.openComposer(this.controllerFor("discovery/topics"));
  }
},

```

for `/tags/category-slug/tag-name` it looks like this

```javascript
createTopic() {
  if (this.get("currentUser.has_topic_draft")) {
    this.openTopicDraft();
  } else {
    const controller = this.controllerFor("tag.show");
    const composerController = this.controllerFor("composer");
    composerController
      .open({
        categoryId: controller.get("category.id"),
        action: Composer.CREATE_TOPIC,
        draftKey: Composer.NEW_TOPIC_KEY
      })
      .then(() => {
        // Pre-fill the tags input field
        if (composerController.canEditTags && controller.get("model.id")) {
          const composerModel = this.controllerFor("composer").get("model");
          composerModel.set(
            "tags",
            [
              controller.get("model.id"),
              ...makeArray(controller.additionalTags)
            ].filter(Boolean)
          );
        }
      });
  }
}

```

This difference is pretty much what you’re asking for here.

> [@pfaffman](#):
>
> If you’re visiting `/tags/category-slug/tag-name` and click the New Topic button, the composer has the tag pre-set

> [@pfaffman](#):
>
> I want to be able to set this behavior with a default tag when visiting `/c/cat-slug/cat-id`

So, all you have to do is… modify the `createTopic()` action in the `discovery` route to make it work like it does on the `tag-show` route. So how do you do that?

Remember how we talked about Ember using Classes? Yeah, we’re going to have to go back to that again.

The plugin API allows you to modify Ember classes via this method.

[https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L166-L195](https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L166-L195)

So, what are we trying to modify here? The `discovery` route… because… remember, that’s where the `createTopic()` is defined when you’re on a page like `/c/cat-slug/cat-id`

We start with this

```javascript
api.modifyClass("route:discovery", {
  pluginId: "prefill-composer-tags",
  actions: {
    createTopic() {
      console.log("fires");
    }
  }
});

```

What does that do? It breaks the + New Topic button; however, it tells us that we’re in the right direction. If you try adding the snippet above, you’ll notice that clicking the button no longer opens the composer. Instead, it just prints a message to the console. This is a good thing because it means we’ve targeted the right Class and the right action - `route:discovery` and `createTopic()`

So, what’s next? Well, remember that the button on `/tags/category-slug/tag-name` does exactly what we want. So, let’s copy the code from that route - and add the required imports.

```javascript
const Composer = require("discourse/models/composer");
const { makeArray } = require("discourse-common/lib/helpers");
api.modifyClass("route:discovery", {
  pluginId: "prefill-composer-tags",
  actions: {
    createTopic() {
      if (this.get("currentUser.has_topic_draft")) {
        this.openTopicDraft();
      } else {
        const controller = this.controllerFor("tag.show");
        const composerController = this.controllerFor("composer");
        composerController
          .open({
            categoryId: controller.get("category.id"),
            action: Composer.CREATE_TOPIC,
            draftKey: Composer.NEW_TOPIC_KEY
          })
          .then(() => {
            // Pre-fill the tags input field
            if (composerController.canEditTags && controller.get("model.id")) {
              const composerModel = this.controllerFor("composer").get("model");
              composerModel.set(
                "tags",
                [
                  controller.get("model.id"),
                  ...makeArray(controller.additionalTags)
                ].filter(Boolean)
              );
            }
          });
      }
    }
  }
});

```

Will that work? No, but we’re one step close. Why doesn’t it work? Because the tags it adds when the composer opens are not defined. Why? Because they’re loaded from the `tag.show` controller - which is not what we want. Let’s modify the code to make it work with the route that we’re on.

Before we do that, though, we need some sort of index for our desired default tags. Let’s go with a new object like so

```javascript
// category-slug: [DEFAULT_TAGS_ARRAY]
const defaultTagIndex = {
  // single word slug
  meta: ["a", "b", "c"],
  core: ["g", "h"],
  // slug with a dash
  ["general-chat"]: ["d", "e", "f"]
};

```

This basically means if the composer is opened on the `meta` category page, add tags “a,b,c”  
If the composer is opened on the `core` category page, add tags “g,h” and so on.

Now that we have that, we can modify the action to make it look like this.

#### Final code

```javascript
const Composer = require("discourse/models/composer");
const { makeArray } = require("discourse-common/lib/helpers");

// category-slug: [DEFAULT_TAGS_ARRAY]
const defaultTagIndex = {
  // single word slug
  meta: ["a", "b", "c"],
  core: ["g", "h"],
  // slug with a dash
  ["general-chat"]: ["d", "e", "f"]
};

api.modifyClass("route:discovery", {
  pluginId: "prefill-composer-tags",
  actions: {
    createTopic() {
      try {
        const hasDraft = this.currentUser?.has_topic_draft;
        if (hasDraft) {
          this._super(...arguments);
          return;
        } else {
          const controller = this.controllerFor("discovery/topics");
          const composerController = this.controllerFor("composer");
          const categoryId = controller.category?.id;
          const categorySlug = controller.category?.slug;

          if (!categoryId) {
            this._super(...arguments);
            return;
          }

          composerController
            .open({
              categoryId: categoryId,
              action: Composer.CREATE_TOPIC,
              draftKey: Composer.NEW_TOPIC_KEY
            })
            .then(() => {
              // Pre-fill the tags input field
              if (composerController.canEditTags && categoryId) {
                const composerModel = composerController.model;
                composerModel.set(
                  "tags",
                  makeArray(defaultTagIndex[categorySlug]).filter(Boolean)
                );
              }
            });
        }
      } catch {
        this._super(...arguments);
        return;
      }
    }
  }
});

```

Notes:

1. I wrapped everything in a [try…catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) block. If the code fails, we execute `this._super(...arguments)`

2. If you’re familiar with Ember, you would know what `this._super(...arguments)` does. If not, here’s a simple explanation. We’re overriding the `createTopic()` so if the overrides fail due to an error - maybe core got updated - then fallback to the method in core as defined [here](https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/routes/discovery.js#L75-L81)

3. if the user has a new topic draft, we just fall back to `this._super(...arguments)` and let core do its thing.

That should be enough. All you need to add now is a way to create the default tag index via theme settings.
