How to use api.onPageChange with api.createWidget?

Hi there, I’m trying to add an image banner to all pages that picks randomly from two random images. It works when I add it to my theme’s Head section and hard reload the homepage:

EDIT: My attempt that gets closer to the solution is here:
How to use api.onPageChange with api.createWidget? - #2 by rahim123

Any idea what I’m doing wrong? Thanks!

This is my more proper attempt. When I run it without the api.onPageChange(() function it works, but doesn’t randomize the image until I do a hard page refresh. When I add the api.onPageChange(() function it doesn’t show any image at all. Any tips on what I’m doing wrong?

<script type="text/discourse-plugin" version="0.8">

api.onPageChange(() => {
        doStuff();
});

function doStuff() {

const h = require("virtual-dom").h;

let banners = new Array();
banners[0]="https://example.com/uploads/default/original/1X/9d9762a4129c78c478d14ff857c3bd1f2be0322a.jpg";
banners[1]="https://example.com/uploads/default/original/1X/04ede2abd9ac117c64988a5b0bcf033474381527.jpg";

let GoTo = new Array();
GoTo[0]="https://google.com";
GoTo[1]="https://brave.com";

let Number = Math.round(1 * Math.random());
let TheLink = GoTo[Number];
let TheImage = banners[Number];

  api.createWidget("top-banner-widget", {
    tagName: "div.top-banner",
    html() {
    return h("div#top-banner", [
      h(
        "a.custom-ad-link",
        { href: TheLink },
        h("img", { src: TheImage})
      )
    ]);
    }
  });
}
</script>

<script type="text/x-handlebars" data-template-name="/connectors/above-main-container/inject-widget">
  {{mount-widget widget="top-banner-widget"}}
</script>
1 Like

I think the missing piece was to trigger a widget rerender… if I recall correctly widgets rerender on interaction, so I’ve added this.scheduleRerender(); to make it rerender on page change.

This works for me locally:

api.createWidget("top-banner-widget", {
    tagName: "div.top-banner",

    html() {
        let Number = Math.round(1 * Math.random());
        
        let banners = new Array();
        banners[0]="https://placekitten.com/500/500";
        banners[1]="https://placekitten.com/500/300";

        let GoTo = new Array();
        GoTo[0]="https://google.com";
        GoTo[1]="https://brave.com";
  
        let TheLink = GoTo[Number];
        let TheImage = banners[Number];
        
        api.onPageChange(() => {
            this.scheduleRerender();
        });
  
        return h("div#top-banner", [
          h(
            "a.custom-ad-link",
            { href: TheLink },
            h("img", { src: TheImage})
          )
        ]);
    }
});
3 Likes

Brilliant! Thanks so much, I never would have figured that out!

Here’s the complete Head code for the theme component:

<script type="text/discourse-plugin" version="0.8">

const h = require("virtual-dom").h;

api.createWidget("top-banner-widget", {
    tagName: "div.top-banner",

    html() {
        let Number = Math.round(1 * Math.random());
        
        let banners = new Array();
        banners[0]="https://example.com/uploads/default/original/1X/9d9762a4129c78c478d14ff857c3bd1f2be0322a.jpg";
        banners[1]="https://example.com/uploads/default/original/1X/04ede2abd9ac117c64988a5b0bcf033474381527.jpg";

        let GoTo = new Array();
        GoTo[0]="https://google.com";
        GoTo[1]="https://brave.com";
  
        let TheLink = GoTo[Number];
        let TheImage = banners[Number];
        
        api.onPageChange(() => {
            this.scheduleRerender();
        });
  
        return h("div#top-banner", [
          h(
            "a.custom-ad-link",
            { href: TheLink },
            h("img", { src: TheImage})
          )
        ]);
    }
});
</script>

<script type="text/x-handlebars" data-template-name="/connectors/above-main-container/inject-widget">
  {{mount-widget widget="top-banner-widget"}}
</script>
1 Like

It’s also worth mentioning that this could be an Ember component. In the long-term we’re going to start relying on Ember more than our custom widget system. We’re still in the process of working on Ember updates, so our documentation on customizing Discourse is a bit behind on this.

I’ve put together a theme component that shows an example of how this can work as an Ember component: GitHub - awesomerobot/discourse-component-example

There are 3 important files here:

  1. The connector (/javascripts/discourse/connectors/custom-header-banner-connector.hbs)

  2. The component (/javascripts/discourse/components/custom-header-banner.js)

  3. The component’s template (/javascripts/discourse/templates/components/custom-header-banner.hbs)

The connector is where the component is added, so this only needs to contain <CustomHeaderBanner />. The component is where we detect a page changes (route changes) and setup the logic for which image/link randomly appears. The component’s template is how the data is presented in HTML.

2 Likes

Ah, good to know, many thanks for that too. I confirm that it works when installed from your repo as a theme component on my site.

Alternatively, could I manually add it as an Ember theme component via the Customize > Install > Create New interface and then Edit CSS / HTML ? That would make it easier to quickly change the images and links as needed. I assume the contents of custom-header-banner.js would go under Head, and then <CustomHeaderBanner /> in After Header or Body , but not sure where to add the contents of custom-header-banner.hbs .

1 Like

It’s a little more complicated than that to translate to a locally installed theme component, but one way to add some flexibility to a remote theme would be using theme settings (⚙ Add settings to your Discourse theme).

That involves adding a settings.yml file and updating a few values in the component and/or template. Then you get settings on your theme component admin page like this:

It’s also fine to stick with the widget implementation above and update via the admin panel, but we tend to recommend using git when possible; it’s easier to share if you need to troubleshoot, and it makes it easy to track your changes.

1 Like

Excellent, really appreciate your patient and clear explanation, great to have that option too.

1 Like

Hi again, I’m trying to figure out the best way to only make the widget show on a specific URL, such as the homepage.

The easy way is simply to use a plugin outlet that only exists on the homepage, which works for what I need for now (specifically, the discovery-navigation-bar-above). But I’m still curious about how to do this programmatically in a way that is sensitive to the specific page URL.

I found this very helpful topic, also by @awesomerobot :

I tried to adapt this into the solution from earlier in this post:

        api.onPageChange((url) => {
            if (url === "/" || url === homeRoute ){
               this.scheduleRerender();
            }
        });

But this still makes the image appear on all pages. I also tried to put my variables and random selection code inside the if clause, but that doesn’t work at all.

There’s also the <script type="text/x-handlebars" ... section of the example, but it seems to only allow for HTML, and I don’t know how to get the variables into it from the previous script.