How do we fire scripts after topic HTML is rendered in DOM?

Like @tshenry pointed out, what you’re looking for is decorateCooked

For basic changes, something like this works:

api.decorateCooked($elem => $elem.css("background", "yellow"));

If you need something more complicated, you have two choices, you can either use this syntax:

api.decorateCooked($elem => {
  // 1
  console.log("foo");
  // 2
  $elem.css("background", "yellow");
  // 3
  console.log("bar");
});

or create your own jQuery plugins like so:

$.fn.doSomething = function() {
  // 1
  console.log("foo");
  // 2
  $(this).css("background", "yellow");
  // 3
  console.log("bar");
  // you have to add this at the end for chainability
  return this;
};

api.decorateCooked($elem => $elem.doSomething());

Do note that decorateCooked runs every time

1- a post is loaded (yes, that means once for every post in the stream).
2- the composer preview is updated - this can be very expensive if you don’t watch out.

What that means is that you have to be mindful and only fire scripts on the posts you want to target.

So, if you don’t want the changes to happen in the composer you would add the onlyStream option. You would do that with something like this:

$.fn.doSomething = function() {

  // do your work
  
  return this;
};

api.decorateCooked($elem => $elem.doSomething(), { onlyStream: true });

This also ensures that your script ignores other places where the post might appear like the user post-stream.

And if you only want to target posts that contain specific elements you can use something like this

$.fn.doSomething = function() {
  const targetElement = $(this).children("[data-theme-test]").length;
  if (!targetElement) return;

  // do your work
  
  return this;
};

api.decorateCooked($elem => $elem.doSomething(), { onlyStream: true });

and that would ensure that your script only fires on the posts that contain the element you want to target and also ignore the composer preview.

If you have external scripts you want to load in order to do some work, you can use loadScript and you can do that with something like this:

const loadScript = require("discourse/lib/load-script").default;

$.fn.doSomething = function() {
  const targetElement = $(this).children("[data-theme-test]").length;
  if (!targetElement) return;

  loadScript(
    "//cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.1.1/iframeResizer.min.js"
  ).then(() => {
    // do your work
  });

  return this;
};

api.decorateCooked($elem => $elem.doSomething(), { onlyStream: true });

Do note that if that’s the case, you will also need to watch out for CSP issues

Finally, if you need to transverse the dom, you might need a little bit of Ember. By default, everything in cooked div should be available for jQuery. However, say you want to target one of the parents of cooked, well you can use something like this:

const loadScript = require("discourse/lib/load-script").default;

$.fn.doSomething = function() {
  const targetElement = $(this).children("[data-theme-test]").length;
  if (!targetElement) return;

  Ember.run.scheduleOnce("afterRender", () => {
    loadScript(
      "//cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.1.1/iframeResizer.min.js"
    ).then(() => {
      // do your work
    });
  });

  return this;
};

api.decorateCooked($elem => $elem.doSomething(), { onlyStream: true });

and that will allow you to fire a script after everything renders and after you load an external script while ignoring posts you don’t want to target and ignoring the composer preview.

Let me know if you need any clarification.

16 Likes