A tour of how the Widget (Virtual DOM) code in Discourse works

That’s unusual. You should be able to place the widget in the widgets directory within a plugin and it should be discovered. I just tried locally with a plugin and it worked.

Did you remember to rm -rf tmp after you added the widgets folder? That might help.

1 Like

That’s interesting. My typical routine when I make changes is:

  • Ctrl+c
  • rm -rf tmp
  • rails s
  • check for changes

The only time I don’t go through that process is when it’s on the Ember side as those don’t normally need tmp cleared. But in this case I still cleared it. I just tried it again and got the same error. I updated my local discourse right before checking. :confused:

Is your plugin code public? If I could try it locally I could try to figure out what is incorrect.

Thanks for being willing to debug with me. I have the widget working through the initializer in the master branch. I broke it out into it’s own file in the widgets directory in the widget-breakout branch. As it is, that branch is giving me the error I mentioned above.

Aha, I see the issue! You weren’t exporting the widget from your ES6 module.

export default createWidget('who-voted', {

Thinking about it, you shouldn’t need to export something from a widget’s module to get it to load since you name the widget in createWidget. I’ve just committed a fix to autoload all widgets:

That should work if you’re on tests-passed, but if you want to support more versions of Discourse just export default from your widget file and it should work out.


Got it! That makes a lot of sense. The voting plugin is already dependent on v1.5.0.beta11. Most aren’t likely to use it until v1.5 is stable. If I leave it without the export that would make it dependent on v1.5.0.beta14?

Is there much difference performance-wise and functionally in one over the other? I’m still getting my head around Ember.

If you leave it without the export it will depend on v1.5.0.beta14 yes, or tests passed.

From your plugin’s point of view there should be no performance difference exporting versus not. In the Discourse codebase there is the question of when to parse the Javascript in the file. The fix means I’m doing it on boot versus when it is embedded in a template which might make loading discourse slower, but in practice probably makes no difference. I might revisit when widgets are loaded later to be more dynamic, but either way your plugin will be fine!


Is it possible to mount a widget into a post from decorateCooked? The polls plugin seems to do some funky stuff with Ember components/controllers/templates, and a set of widgets seem like they might be easier to work with.

The polls code is definitely not something to imitate right now. When it was first created it made sense as a way to mount more ember code into something dynamically rendered, but now it is a bit of a hack. When I created the widget stuff it seemed vastly easier to maintain the old ember code than re-write it as vdom as there is quite a lot involved in rendering a poll.

If I had to do it again I’d just do it via widgets somehow. There is currently no way to render a widget in decorateCooked that I am aware of, but you could use attach other widgets before and after the cooked content to achieve a similar effect.


Ran into another hang up here. I have two widgets each created in separate files with export default createWidget(). In the first there is an action testAction(). I’m attempting to call that action from the second widget using this.sendWidgetAction("testAction") but I’m continually getting an error saying that the action testAction cannot be found. Is it possible to call an action across widgets?

Actions are meant to bubble up. If a widget embeds another widget using this.attach, then the attached widget will send the action up to the parent, its parent and so on.

There is no way to call a widget action for siblings. Like if B and C are attached to A, B cannot send an action to C.


@eviltrout, I’m hoping you can bear with me another time on this. I ran my development environment a couple hours after pulling from master to upgrade my local discourse instance and something happened to the voting plugin. It ran fine yesterday and as far as I know there have been no changes to it. Now none of the widgets render and all I can find for errors is this:

I’m not even sure where to start on this one but it seems as though something changed in the widget creation process. But they aren’t being mounted. It errors out in the attempt to mount them in the handlebars templates. Any guesses on what to look for?

Edit: It looks like this may be the source of the issue:

Why is it that when you post a question or a bug that you find the issue shortly afterwards?

Sorry for the trouble. Turns out this line led to the issue:

If you define defaultState for a widget, but then leave it empty it will cause the plugin to fail. Makes sense but took a bit to find it. ¯_(ツ)_/¯


Ah I see! That was me trying to be smart and tell people always to use a key when using state for a widget. defaultState should never return null though, so I’ve added another check to warn about that if that’s the case:


So building off of those new warnings, what keys are needed?

Whenever a widget has state it needs a key so that the virtual dom can apply the state to the same element the next time it is repainted.

It’s pretty easy to do:

createWidget('my-widget', {
  buildKey: attrs => `my-widget-${attrs.id}`

That will build a key using the id of the thing being rendered. If it’s a thing attached to a post you’ll want to use the post’s id for example.

If there is only ever one of your widget, feel free to not even include an id and just return a string, like I do for the header: buildKey: () => 'header'


Thanx for the great explanation. One question though. How du I pass the model to a click action?

Solved it by ‘model=post’


I can see the plugin-outlets in the header are gone. Any tips on how to add stuff to the header now?

The header uses the same apis as the new post stream, so you can just use decorateWidget to add code before or after any widget in the header for your needs.