Insert a glimmer component after first post

I’ve got a glimmer component. It’s working. But I need it to go after the first post in a topic. There’s no plugin outlet there.

I did figure out how to insert some text in the innerHTML of the <article> tag, and that puts that text where I want, but there doesn’t seem to be a way to do that with a component.

Am I missing something?

I looked at how the adplugin inserts ads between posts, but I couldn’t quite make sense of it.

export default {
  name: "initialize-ad-plugin",
  initialize(container) {
    registerWidgetShim(
      "after-post-ad",
      "div.widget-connector",
      hbs`<PostBottomAd @model={{@data}} />`
    );

    withPluginApi("0.1", (api) => {
      api.decorateWidget("post:after", (helper) => {
        return helper.attach("after-post-ad", helper.widget.model);
      });
    });

....

Let’s add one! Where would you like it?

(there are legacy ways to accomplish what you’re doing, but the’ll break when we modernise the topic view. If we add a plugin outlet, it’ll be much more future-proof)

2 Likes

This outlet would match the functionality of decorateWidget("post:after, which is what the adplugin uses:

2 Likes

That’s awesome! Whine and you shall receive!

Actually, I “know” that it’s “easy” to add plugin outlets, but when I have sort-of known how to add them, it was in an hbs file.

Is that going to be after every post? So I’ll need to figure out some magic to make it be after only the first one?

article:after is what I had contrived at some point in my previous attempts.

1 Like

The post is passed as one of the outlet arguments, so you can check post.post_number == 1

Haha! Always happy to help with these kinds of questions. Much better for everyone if we can get themes and plugins using standard APIs like this rather than widget workarounds (which will be deprecated in the not-too-distant future).

Yeah that’s fair. Adding them inside widgets is a headache on the core side. But from the theme/plugin side it should be super clean :crossed_fingers:


I’ll try and figure out those test failures and get the PR merged tomorrow. (Looks like the extra html element is messing with some fragile selectors in the qunit tests)

2 Likes

Aha! @outletArgs={{hash post=@data.post}}. There’s a reasonable chance I can figure out how to do that. :slight_smile:

Thanks again!

2 Likes

I’m afraid I may have been a bit over-optimistic here @pfaffman, sorry! The PR I made would introduce a new wrapper <div> between every single post, even if the outlet was not being used. That’s not really something we want to do.

There may be ways to avoid the wrapper… but nothing simple which we can do immediately.

So I think the best immediate solution for you is going to be copying the adplugin implementation which you referenced in the OP.

Essentially:

  1. Create a component (Glimmer or classic, doesn’t matter) which renders whatever you want

  2. Use registerWidgetShim to make that component available as a widget. The adplugin example is creating a widget called “after-post-ad”, which renders the PostBottomAd component. It is passing all of the widget attributes (@data) into the @model argument of the component.

  3. Use api.decorateWidget to render your new widget shim in the post:after position. In your case, since you only want it on the first post, you could do something like

    api.decorateWidget("post:after", (helper) => {
      if(helper.widget.model.post_number === 1){
        return helper.attach("my-widget-shim");
      }
    });
    

When we eventually glimmer-ify the topic page, you’ll need to remove the widget shim/decoration, and replace it with a plugin outlet. That should be pretty easy, since all your display logic in the component will be reusable in the plugin outlet.

Let us know how you get on! Happy to help with any follow-up questions - I know there are a lot of moving parts here.

3 Likes

I’m so very close!

The only issue is that the component needs currentUser and I can’t figure out how to pass it. Here’s what doesn’t work.

import { hbs } from "ember-cli-htmlbars";
import { withPluginApi } from "discourse/lib/plugin-api";
import Site from "discourse/models/site";
import { registerWidgetShim } from "discourse/widgets/render-glimmer";
import Banner from "../components/banner";

export default {
  name: "initialize-banner-widget",
  initialize(container) {
    registerWidgetShim(
      "geo-banner-widget",
      "div.widget-connector",
      hbs`<Banner @currentUser={{currentUser}} />`
    );

    // withPluginApi("0.1", (api) => {
    //   console.log("doing plugin");
    //   api.decorateWidget("post:after", (helper) => {
    //     return helper.attach("geo-banner-widget", helper.widget.model);
    //   });
    // });

    withPluginApi("0.1", (api) => {
      const currentUser = container.lookup("service:current-user");
      api.decorateWidget("post:after", (helper) => {
        console.log(`decorate widget ${currentUser.username}`, );
        if(helper.widget.model.post_number === 1){
          return helper.attach("geo-banner-widget", { currentUser });
        }
      });
    });
  },
};
1 Like

In your Banner component, you can inject the currentUser service like this:

class Banner extends Component {
  @service currentUser;
}

then you won’t need to pass it through from the widget.

2 Likes

I knew it was stupid to pass the currentUser, but I couldn’t figure that out!

The other bit, in the unlikely event that anyone else finds this useful, is

import Service, { inject as service } from "@ember/service";
import { hbs } from "ember-cli-htmlbars";
import { withPluginApi } from "discourse/lib/plugin-api";
import { registerWidgetShim } from "discourse/widgets/render-glimmer";

export default {
  name: "initialize-banner-widget",
  initialize(container) {
    registerWidgetShim(
      "geo-banner-widget",
      "div.widget-connector",
      hbs`<Banner/>`
    );

    withPluginApi("0.1", (api) => {
      api.decorateWidget("post:after", (helper) => {
        const currentUser = container.lookup("service:current-user");
        if(helper.widget.model.post_number === 1){
          return helper.attach("geo-banner-widget");
        }
      });
    });
  },
};

Yup!

In this case, I don’t think you’ll need to import the Service class. That would only be needed if you were creating your own Service. You just need the service injection decorator.

And in the latest version of Discourse/Ember, it can be simplified even further to avoid needing the ‘inject as’ alias. Ember now make the injection decorator available directly as service.

import { service } from "@ember/service";

(but the old { inject as service } still works, and I’m not aware of any plans to deprecate it in Ember)

1 Like

That’s what the linter thinks too :slight_smile:

I like that better. The house ads plugin still does it the old way. :slight_smile:

1 Like